user/dev discussion of public-inbox itself
 help / color / Atom feed
From: Eric Wong <e@80x24.org>
To: meta@public-inbox.org
Subject: [PATCH 09/37] view: wire up diff and vcs viewers with solver
Date: Mon, 21 Jan 2019 20:52:25 +0000
Message-ID: <20190121205253.10455-10-e@80x24.org> (raw)
In-Reply-To: <20190121205253.10455-1-e@80x24.org>

---
 MANIFEST                    |   2 +
 lib/PublicInbox/Config.pm   |  59 ++++++++++++++-
 lib/PublicInbox/View.pm     |  47 +++++++++---
 lib/PublicInbox/ViewDiff.pm | 147 ++++++++++++++++++++++++++++++++++++
 lib/PublicInbox/ViewVCS.pm  |  87 +++++++++++++++++++++
 lib/PublicInbox/WWW.pm      |  18 ++++-
 6 files changed, 345 insertions(+), 15 deletions(-)
 create mode 100644 lib/PublicInbox/ViewDiff.pm
 create mode 100644 lib/PublicInbox/ViewVCS.pm

diff --git a/MANIFEST b/MANIFEST
index 95ad0c6..5e980fe 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -109,6 +109,8 @@ lib/PublicInbox/SpawnPP.pm
 lib/PublicInbox/Unsubscribe.pm
 lib/PublicInbox/V2Writable.pm
 lib/PublicInbox/View.pm
+lib/PublicInbox/ViewDiff.pm
+lib/PublicInbox/ViewVCS.pm
 lib/PublicInbox/WWW.pm
 lib/PublicInbox/WWW.pod
 lib/PublicInbox/WatchMaildir.pm
diff --git a/lib/PublicInbox/Config.pm b/lib/PublicInbox/Config.pm
index bea2617..355e64b 100644
--- a/lib/PublicInbox/Config.pm
+++ b/lib/PublicInbox/Config.pm
@@ -2,12 +2,19 @@
 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
 #
 # Used throughout the project for reading configuration
+#
+# Note: I hate camelCase; but git-config(1) uses it, but it's better
+# than alllowercasewithoutunderscores, so use lc('configKey') where
+# applicable for readability
+
 package PublicInbox::Config;
 use strict;
 use warnings;
 require PublicInbox::Inbox;
 use PublicInbox::Spawn qw(popen_rd);
 
+sub _array ($) { ref($_[0]) eq 'ARRAY' ? $_[0] : [ $_[0] ] }
+
 # returns key-value pairs of config directives in a hash
 # if keys may be multi-value, the value is an array ref containing all values
 sub new {
@@ -22,6 +29,7 @@ sub new {
 	$self->{-by_newsgroup} ||= {};
 	$self->{-no_obfuscate} ||= {};
 	$self->{-limiters} ||= {};
+	$self->{-code_repos} ||= {}; # nick => PublicInbox::Git object
 
 	if (my $no = delete $self->{'publicinbox.noobfuscate'}) {
 		$no = [ $no ] if ref($no) ne 'ARRAY';
@@ -169,6 +177,41 @@ sub valid_inbox_name ($) {
 	1;
 }
 
+# parse a code repo
+# Only git is supported at the moment, but SVN and Hg are possibilities
+sub _fill_code_repo {
+	my ($self, $nick) = @_;
+	my $pfx = "coderepo.$nick";
+
+	my $dir = $self->{"$pfx.dir"}; # aka "GIT_DIR"
+	unless (defined $dir) {
+		warn "$pfx.repodir unset";
+		return;
+	}
+
+	my $git = PublicInbox::Git->new($dir);
+	foreach my $t (qw(blob commit tree tag)) {
+		$git->{$t.'_url_format'} =
+				_array($self->{lc("$pfx.${t}UrlFormat")});
+	}
+
+	if (my $cgits = $self->{lc("$pfx.cgitUrl")}) {
+		$git->{cgit_url} = $cgits = _array($cgits);
+
+		# cgit supports "/blob/?id=%s", but it's only a plain-text
+		# display and requires an unabbreviated id=
+		foreach my $t (qw(blob commit tag)) {
+			$git->{$t.'_url_format'} ||= map {
+				"$_/$t/?id=%s"
+			} @$cgits;
+		}
+	}
+	# TODO: support gitweb and other repository viewers?
+	# TODO: parse cgitrc
+
+	$git;
+}
+
 sub _fill {
 	my ($self, $pfx) = @_;
 	my $rv = {};
@@ -192,9 +235,9 @@ sub _fill {
 	}
 	# TODO: more arrays, we should support multi-value for
 	# more things to encourage decentralization
-	foreach my $k (qw(address altid nntpmirror)) {
+	foreach my $k (qw(address altid nntpmirror coderepo)) {
 		if (defined(my $v = $self->{"$pfx.$k"})) {
-			$rv->{$k} = ref($v) eq 'ARRAY' ? $v : [ $v ];
+			$rv->{$k} = _array($v);
 		}
 	}
 
@@ -224,6 +267,18 @@ sub _fill {
 		$rv->{-no_obfuscate_re} = $self->{-no_obfuscate_re};
 		each_inbox($self, sub {}); # noop to populate -no_obfuscate
 	}
+
+	if (my $ibx_code_repos = $rv->{coderepo}) {
+		my $code_repos = $self->{-code_repos};
+		my $repo_objs = $rv->{-repo_objs} = [];
+		foreach my $nick (@$ibx_code_repos) {
+			valid_inbox_name($nick) or next;
+			my $repo = $code_repos->{$nick} ||=
+						_fill_code_repo($self, $nick);
+			push @$repo_objs, $repo if $repo;
+		}
+	}
+
 	$rv
 }
 
diff --git a/lib/PublicInbox/View.pm b/lib/PublicInbox/View.pm
index 470e3ab..0187ec3 100644
--- a/lib/PublicInbox/View.pm
+++ b/lib/PublicInbox/View.pm
@@ -14,6 +14,7 @@ use PublicInbox::MsgIter;
 use PublicInbox::Address;
 use PublicInbox::WwwStream;
 use PublicInbox::Reply;
+use PublicInbox::ViewDiff qw(flush_diff);
 require POSIX;
 use Time::Local qw(timegm);
 
@@ -28,7 +29,7 @@ sub msg_html {
 	my ($ctx, $mime, $more, $smsg) = @_;
 	my $hdr = $mime->header_obj;
 	my $ibx = $ctx->{-inbox};
-	my $obfs_ibx = $ctx->{-obfs_ibx} = $ibx->{obfuscate} ? $ibx : undef;
+	$ctx->{-obfs_ibx} = $ibx->{obfuscate} ? $ibx : undef;
 	my $tip = _msg_html_prepare($hdr, $ctx, $more, 0);
 	my $end = 2;
 	PublicInbox::WwwStream->response($ctx, 200, sub {
@@ -36,7 +37,7 @@ sub msg_html {
 		if ($nr == 1) {
 			# $more cannot be true w/o $smsg being defined:
 			my $upfx = $more ? '../'.mid_escape($smsg->mid).'/' : '';
-			$tip . multipart_text_as_html($mime, $upfx, $obfs_ibx) .
+			$tip . multipart_text_as_html($mime, $upfx, $ibx) .
 				'</pre><hr>'
 		} elsif ($more && @$more) {
 			++$end;
@@ -81,15 +82,15 @@ sub msg_html_more {
 	my $str = eval {
 		my ($id, $prev, $smsg) = @$more;
 		my $mid = $ctx->{mid};
-		$smsg = $ctx->{-inbox}->smsg_mime($smsg);
+		my $ibx = $ctx->{-inbox};
+		$smsg = $ibx->smsg_mime($smsg);
 		my $next = $ctx->{srch}->next_by_mid($mid, \$id, \$prev);
 		@$more = $next ? ($id, $prev, $next) : ();
 		if ($smsg) {
 			my $mime = $smsg->{mime};
 			my $upfx = '../' . mid_escape($smsg->mid) . '/';
 			_msg_html_prepare($mime->header_obj, $ctx, $more, $nr) .
-				multipart_text_as_html($mime, $upfx,
-							$ctx->{-obfs_ibx}) .
+				multipart_text_as_html($mime, $upfx, $ibx) .
 				'</pre><hr>'
 		} else {
 			'';
@@ -260,7 +261,8 @@ sub index_entry {
 	$rv .= "\n";
 
 	# scan through all parts, looking for displayable text
-	msg_iter($mime, sub { $rv .= add_text_body($mhref, $obfs_ibx, $_[0]) });
+	my $ibx = $ctx->{-inbox};
+	msg_iter($mime, sub { $rv .= add_text_body($mhref, $ibx, $_[0]) });
 
 	# add the footer
 	$rv .= "\n<a\nhref=#$id_m\nid=e$id>^</a> ".
@@ -488,11 +490,11 @@ sub thread_html {
 }
 
 sub multipart_text_as_html {
-	my ($mime, $upfx, $obfs_ibx) = @_;
+	my ($mime, $upfx, $ibx) = @_;
 	my $rv = "";
 
 	# scan through all parts, looking for displayable text
-	msg_iter($mime, sub { $rv .= add_text_body($upfx, $obfs_ibx, $_[0]) });
+	msg_iter($mime, sub { $rv .= add_text_body($upfx, $ibx, $_[0]) });
 	$rv;
 }
 
@@ -545,7 +547,8 @@ sub attach_link ($$$$;$) {
 }
 
 sub add_text_body {
-	my ($upfx, $obfs_ibx, $p) = @_;
+	my ($upfx, $ibx, $p) = @_;
+	my $obfs_ibx = $ibx->{obfuscate} ? $ibx : undef;
 	# $p - from msg_iter: [ Email::MIME, depth, @idx ]
 	my ($part, $depth) = @$p; # attachment @idx is unused
 	my $ct = $part->content_type || 'text/plain';
@@ -554,6 +557,19 @@ sub add_text_body {
 
 	return attach_link($upfx, $ct, $p, $fn) unless defined $s;
 
+	my ($diff, $spfx);
+	if ($ibx->{-repo_objs} && $s =~ /^(?:diff|---|\+{3}) /ms) {
+		$diff = [];
+		my $n_slash = $upfx =~ tr!/!/!;
+		if ($n_slash == 0) {
+			$spfx = '../';
+		} elsif ($n_slash == 1) {
+			$spfx = '';
+		} else { # nslash == 2
+			$spfx = '../../';
+		}
+	};
+
 	my @lines = split(/^/m, $s);
 	$s = '';
 	if (defined($fn) || $depth > 0 || $err) {
@@ -568,19 +584,26 @@ sub add_text_body {
 			# show the previously buffered quote inline
 			flush_quote(\$s, $l, \@quot) if @quot;
 
-			# regular line, OK
-			$l->linkify_1($cur);
-			$s .= $l->linkify_2(ascii_html($cur));
+			if ($diff) {
+				push @$diff, $cur;
+			} else {
+				# regular line, OK
+				$l->linkify_1($cur);
+				$s .= $l->linkify_2(ascii_html($cur));
+			}
 		} else {
+			flush_diff(\$s, $spfx, $l, $diff) if $diff && @$diff;
 			push @quot, $cur;
 		}
 	}
 
 	if (@quot) { # ugh, top posted
 		flush_quote(\$s, $l, \@quot);
+		flush_diff(\$s, $spfx, $l, $diff) if $diff && @$diff;
 		obfuscate_addrs($obfs_ibx, $s) if $obfs_ibx;
 		$s;
 	} else {
+		flush_diff(\$s, $spfx, $l, $diff) if $diff && @$diff;
 		obfuscate_addrs($obfs_ibx, $s) if $obfs_ibx;
 		if ($s =~ /\n\z/s) { # common, last line ends with a newline
 			$s;
diff --git a/lib/PublicInbox/ViewDiff.pm b/lib/PublicInbox/ViewDiff.pm
new file mode 100644
index 0000000..ee450fa
--- /dev/null
+++ b/lib/PublicInbox/ViewDiff.pm
@@ -0,0 +1,147 @@
+# Copyright (C) 2019 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+#
+# used by PublicInbox::View
+package PublicInbox::ViewDiff;
+use strict;
+use warnings;
+use base qw(Exporter);
+our @EXPORT_OK = qw(flush_diff);
+
+use PublicInbox::Hval qw(ascii_html);
+use PublicInbox::Git qw(git_unquote);
+
+sub DSTATE_INIT () { 0 }
+sub DSTATE_STAT () { 1 } # TODO
+sub DSTATE_HEAD () { 2 } # /^diff --git /, /^index /, /^--- /, /^\+\+\+ /
+sub DSTATE_HUNK () { 3 } # /^@@ /
+sub DSTATE_CTX () { 4 } # /^ /
+sub DSTATE_ADD () { 5 } # /^\+/
+sub DSTATE_DEL () { 6 } # /^\-/
+
+my $OID_NULL = '0{7,40}';
+my $OID_BLOB = '[a-f0-9]{7,40}';
+my $PATH_A = '"?a/.+|/dev/null';
+my $PATH_B = '"?b/.+|/dev/null';
+
+sub to_html ($$) {
+	$_[0]->linkify_1($_[1]);
+	$_[0]->linkify_2(ascii_html($_[1]));
+}
+
+# link to line numbers in blobs
+sub diff_hunk ($$$$) {
+	my ($dctx, $spfx, $ca, $cb) = @_;
+	my $oid_a = $dctx->{oid_a};
+	my $oid_b = $dctx->{oid_b};
+
+	(defined($oid_a) && defined($oid_b)) or return "@@ $ca $cb @@";
+
+	my ($n) = ($ca =~ /^-(\d+)/);
+	$n = defined($n) ? do { ++$n; "#n$n" } : '';
+
+	my $rv = qq(@@ <a\nhref=$spfx$oid_a/s$n>$ca</a>);
+
+	($n) = ($cb =~ /^\+(\d+)/);
+	$n = defined($n) ? do { ++$n; "#n$n" } : '';
+
+	$rv .= qq( <a\nhref=$spfx$oid_b/s$n>$cb</a> @@);
+}
+
+sub flush_diff ($$$$) {
+	my ($dst, $spfx, $linkify, $diff) = @_;
+	my $state = DSTATE_INIT;
+	my $dctx; # {}, keys: oid_a, oid_b, path_a, path_b
+
+	foreach my $s (@$diff) {
+		if ($s =~ /^ /) {
+			if ($state == DSTATE_HUNK || $state == DSTATE_ADD ||
+			    $state == DSTATE_DEL || $state == DSTATE_HEAD) {
+				$$dst .= "</span><span\nclass=ctx>";
+				$state = DSTATE_CTX;
+			}
+			$$dst .= to_html($linkify, $s);
+		} elsif ($s =~ /^-- $/) { # email signature begins
+			if ($state != DSTATE_INIT) {
+				$state = DSTATE_INIT;
+				$$dst .= '</span>';
+			}
+			$$dst .= $s;
+		} elsif ($s =~ m!^diff --git ($PATH_A) ($PATH_B)$!x) {
+			if ($state != DSTATE_HEAD) {
+				my ($pa, $pb) = ($1, $2);
+				$$dst .= '</span>' if $state != DSTATE_INIT;
+				$$dst .= "<span\nclass=head>";
+				$state = DSTATE_HEAD;
+				$pa = (split('/', git_unquote($pa), 2))[1];
+				$pb = (split('/', git_unquote($pb), 2))[1];
+				$dctx = { path_a => $pa, path_b => $pb };
+			}
+			$$dst .= to_html($linkify, $s);
+		} elsif ($s =~ s/^(index $OID_NULL\.\.)($OID_BLOB)\b//o) {
+			$$dst .= qq($1<a\nhref=$spfx$2/s>$2</a>);
+			$$dst .= to_html($linkify, $s) ;
+		} elsif ($s =~ s/^index ($OID_NULL)(\.\.$OID_BLOB)\b//o) {
+			$$dst .= 'index ';
+			$$dst .= qq(<a\nhref=$spfx$1/s>$1</a>$2);
+			$$dst .= to_html($linkify, $s);
+		} elsif ($s =~ /^index ($OID_BLOB)\.\.($OID_BLOB)/o) {
+			$dctx->{oid_a} = $1;
+			$dctx->{oid_b} = $2;
+			$$dst .= to_html($linkify, $s);
+		} elsif ($s =~ s/^@@ (\S+) (\S+) @@//) {
+			my ($ca, $cb) = ($1, $2);
+			if ($state == DSTATE_HEAD || $state == DSTATE_CTX ||
+			    $state == DSTATE_ADD || $state == DSTATE_DEL) {
+				$$dst .= "</span><span\nclass=hunk>";
+				$state = DSTATE_HUNK;
+				$$dst .= diff_hunk($dctx, $spfx, $ca, $cb);
+			} else {
+				$$dst .= to_html($linkify, "@@ $ca $cb @@");
+			}
+			$$dst .= to_html($linkify, $s);
+		} elsif ($s =~ m!^--- $PATH_A!) {
+			if ($state == DSTATE_INIT) { # color only (no oid link)
+				$state = DSTATE_HEAD;
+				$$dst .= "<span\nclass=head>";
+			}
+			$$dst .= to_html($linkify, $s);
+		} elsif ($s =~ m!^\+{3} $PATH_B!)  {
+			if ($state == DSTATE_INIT) { # color only (no oid link)
+				$state = DSTATE_HEAD;
+				$$dst .= "<span\nclass=head>";
+			}
+			$$dst .= to_html($linkify, $s);
+		} elsif ($s =~ /^\+/) {
+			if ($state != DSTATE_ADD && $state != DSTATE_INIT) {
+				$$dst .= "</span><span\nclass=add>";
+				$state = DSTATE_ADD;
+			}
+			$$dst .= to_html($linkify, $s);
+		} elsif ($s =~ /^-/) {
+			if ($state != DSTATE_DEL && $state != DSTATE_INIT) {
+				$$dst .= "</span><span\nclass=del>";
+				$state = DSTATE_DEL;
+			}
+			$$dst .= to_html($linkify, $s);
+		# ignore the following lines in headers:
+		} elsif ($s =~ /^(?:dis)similarity index/ ||
+			 $s =~ /^(?:old|new) mode/ ||
+			 $s =~ /^(?:deleted|new) file mode/ ||
+			 $s =~ /^(?:copy|rename) (?:from|to) / ||
+			 $s =~ /^(?:dis)?similarity index /) {
+			$$dst .= to_html($linkify, $s);
+		} else {
+			if ($state != DSTATE_INIT) {
+				$$dst .= '</span>';
+				$state = DSTATE_INIT;
+			}
+			$$dst .= to_html($linkify, $s);
+		}
+	}
+	@$diff = ();
+	$$dst .= '</span>' if $state != DSTATE_INIT;
+	undef;
+}
+
+1;
diff --git a/lib/PublicInbox/ViewVCS.pm b/lib/PublicInbox/ViewVCS.pm
new file mode 100644
index 0000000..49fb1c5
--- /dev/null
+++ b/lib/PublicInbox/ViewVCS.pm
@@ -0,0 +1,87 @@
+# Copyright (C) 2019 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# show any VCS object, similar to "git show"
+package PublicInbox::ViewVCS;
+use strict;
+use warnings;
+use Encode qw(find_encoding);
+use PublicInbox::SolverGit;
+use PublicInbox::WwwStream;
+use PublicInbox::Linkify;
+use PublicInbox::Hval qw(ascii_html);
+my %QP_MAP = ( A => 'oid_a', B => 'oid_b', a => 'path_a', b => 'path_b' );
+my $max_size = 1024 * 1024; # TODO: configurable
+my $enc_utf8 = find_encoding('UTF-8');
+
+sub html_page ($$$) {
+	my ($ctx, $code, $strref) = @_;
+	$ctx->{-upfx} = '../'; # from "/$INBOX/$OID/s"
+	PublicInbox::WwwStream->response($ctx, $code, sub {
+		my ($nr, undef) =  @_;
+		$nr == 1 ? $$strref : undef;
+	});
+}
+
+sub show ($$;$) {
+	my ($ctx, $oid_b, $fn) = @_;
+	my $ibx = $ctx->{-inbox};
+	my $inboxes = [ $ibx ];
+	my $solver = PublicInbox::SolverGit->new($ibx->{-repo_objs}, $inboxes);
+	my $qp = $ctx->{qp};
+	my $hints = {};
+	while (my ($from, $to) = each %QP_MAP) {
+		defined(my $v = $qp->{$from}) or next;
+		$hints->{$to} = $v;
+	}
+
+	open my $log, '+>', undef or die "open: $!";
+	my $res = $solver->solve($log, $oid_b, $hints);
+
+	seek($log, 0, 0) or die "seek: $!";
+	$log = do { local $/; <$log> };
+
+	my $l = PublicInbox::Linkify->new;
+	$l->linkify_1($log);
+	$log = '<pre>debug log:</pre><hr /><pre>' .
+		$l->linkify_2(ascii_html($log)) . '</pre>';
+
+	$res or return html_page($ctx, 404, \$log);
+
+	my ($git, $oid, $type, $size, $di) = @$res;
+	if ($size > $max_size) {
+		# TODO: stream the raw file if it's gigantic, at least
+		$log = '<pre><b>Too big to show</b></pre>' . $log;
+		return html_page($ctx, 500, \$log);
+	}
+
+	my $blob = $git->cat_file($oid);
+	if (!$blob) { # WTF?
+		my $e = "Failed to retrieve generated blob ($oid)";
+		$ctx->{env}->{'psgi.errors'}->print("$e ($git->{git_dir})\n");
+		$log = "<pre><b>$e</b></pre>" . $log;
+		return html_page($ctx, 500, \$log);
+	}
+
+	if (index($$blob, "\0") >= 0) {
+		$log = "<pre>$oid $type $size bytes (binary)</pre>" . $log;
+		return html_page($ctx, 200, \$log);
+	}
+
+	$$blob = $enc_utf8->decode($$blob);
+	my $nl = ($$blob =~ tr/\n/\n/);
+	my $pad = length($nl);
+
+	# using some of the same CSS class names and ids as cgit
+	$log = "<pre>$oid $type $size bytes</pre><hr /><table\nclass=blob>".
+		"<tr><td\nclass=linenumbers><pre>" . join('', map {
+			sprintf("<a id=n$_ href=#n$_>% ${pad}u</a>\n", $_)
+		} (1..$nl)) . '</pre></td>' .
+		'<td><pre> </pre></td>'. # pad for non-CSS users
+		"<td\nclass=lines><pre><code>" .  ascii_html($$blob) .
+		'</pre></td></tr></table>' . $log;
+
+	html_page($ctx, 200, \$log);
+}
+
+1;
diff --git a/lib/PublicInbox/WWW.pm b/lib/PublicInbox/WWW.pm
index 3562e46..c73370f 100644
--- a/lib/PublicInbox/WWW.pm
+++ b/lib/PublicInbox/WWW.pm
@@ -25,6 +25,7 @@ our $INBOX_RE = qr!\A/([\w\-][\w\.\-]*)!;
 our $MID_RE = qr!([^/]+)!;
 our $END_RE = qr!(T/|t/|t\.mbox(?:\.gz)?|t\.atom|raw|)!;
 our $ATTACH_RE = qr!(\d[\.\d]*)-([[:alnum:]][\w\.-]+[[:alnum:]])!i;
+our $OID_RE = qr![a-f0-9]{7,40}!;
 
 sub new {
 	my ($class, $pi_config) = @_;
@@ -117,7 +118,10 @@ sub call {
 		r301($ctx, $1, $2);
 	} elsif ($path_info =~ m!$INBOX_RE/_/text(?:/(.*))?\z!o) {
 		get_text($ctx, $1, $2);
-
+	} elsif ($path_info =~ m!$INBOX_RE/($OID_RE)/s\z!o) {
+		get_vcs_object($ctx, $1, $2);
+	} elsif ($path_info =~ m!$INBOX_RE/($OID_RE)/_([\w\.\-]+)\z!o) {
+		get_vcs_object($ctx, $1, $2, $3);
 	# convenience redirects order matters
 	} elsif ($path_info =~ m!$INBOX_RE/([^/]{2,})\z!o) {
 		r301($ctx, $1, $2);
@@ -259,6 +263,18 @@ sub get_text {
 	PublicInbox::WwwText::get_text($ctx, $key);
 }
 
+# show git objects (blobs and commits)
+# /$INBOX/_/$OBJECT_ID/show
+# /$INBOX/_/${OBJECT_ID}_${FILENAME}
+# KEY may contain slashes
+sub get_vcs_object ($$$;$) {
+	my ($ctx, $inbox, $oid, $filename) = @_;
+	my $r404 = invalid_inbox($ctx, $inbox);
+	return $r404 if $r404;
+	require PublicInbox::ViewVCS;
+	PublicInbox::ViewVCS::show($ctx, $oid, $filename);
+}
+
 sub ctx_get {
 	my ($ctx, $key) = @_;
 	my $val = $ctx->{$key};
-- 
EW


  parent reply index

Thread overview: 38+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2019-01-21 20:52 [PATCH 00/37] viewvcs: diff highlighting and more Eric Wong
2019-01-21 20:52 ` [PATCH 01/37] view: disable bold in topic display Eric Wong
2019-01-21 20:52 ` [PATCH 02/37] hval: force monospace for <form> elements, too Eric Wong
2019-01-21 20:52 ` [PATCH 03/37] t/perf-msgview: add test to check msg_html performance Eric Wong
2019-01-21 20:52 ` [PATCH 04/37] solver: initial Perl implementation Eric Wong
2019-01-21 20:52 ` [PATCH 05/37] git: support multiple URL endpoints Eric Wong
2019-01-21 20:52 ` [PATCH 06/37] git: add git_quote Eric Wong
2019-01-21 20:52 ` [PATCH 07/37] git: check saves error on disambiguation Eric Wong
2019-01-21 20:52 ` [PATCH 08/37] solver: various bugfixes and cleanups Eric Wong
2019-01-21 20:52 ` Eric Wong [this message]
2019-01-21 20:52 ` [PATCH 10/37] git: disable abbreviations with cat-file hints Eric Wong
2019-01-21 20:52 ` [PATCH 11/37] solver: operate directly on git index Eric Wong
2019-01-21 20:52 ` [PATCH 12/37] view: enable naming hints for raw blob downloads Eric Wong
2019-01-21 20:52 ` [PATCH 13/37] git: support 'ambiguous' result from --batch-check Eric Wong
2019-01-21 20:52 ` [PATCH 14/37] solver: more verbose blob resolution Eric Wong
2019-01-21 20:52 ` [PATCH 15/37] solver: break up patch application steps Eric Wong
2019-01-21 20:52 ` [PATCH 16/37] solver: switch patch application to use a callback Eric Wong
2019-01-21 20:52 ` [PATCH 17/37] solver: simplify control flow for initial loop Eric Wong
2019-01-21 20:52 ` [PATCH 18/37] solver: break @todo loop into a callback Eric Wong
2019-01-21 20:52 ` [PATCH 19/37] solver: note the synchronous nature of index preparation Eric Wong
2019-01-21 20:52 ` [PATCH 20/37] solver: add a TODO note about making this fully evented Eric Wong
2019-01-21 20:52 ` [PATCH 21/37] view: enforce trailing slash for /$INBOX/$OID/s/ endpoints Eric Wong
2019-01-21 20:52 ` [PATCH 22/37] solver: restore diagnostics and deal with CRLF Eric Wong
2019-01-21 20:52 ` [PATCH 23/37] www: admin-configurable CSS via "publicinbox.css" Eric Wong
2019-01-21 20:52 ` [PATCH 24/37] $INBOX/_/text/color/ and sample user-side CSS Eric Wong
2019-01-21 20:52 ` [PATCH 25/37] viewdiff: support diff-highlighting w/o coderepo Eric Wong
2019-01-21 20:52 ` [PATCH 26/37] viewdiff: cleanup state transitions a bit Eric Wong
2019-01-21 20:52 ` [PATCH 27/37] viewdiff: quote attributes for Atom feed Eric Wong
2019-01-21 20:52 ` [PATCH 28/37] t/check-www-inbox: use xmlstarlet to validate Atom if available Eric Wong
2019-01-21 20:52 ` [PATCH 29/37] viewdiff: do not link to 0{7,40} blobs (again) Eric Wong
2019-01-21 20:52 ` [PATCH 30/37] viewvcs: disable white-space prewrap in blob view Eric Wong
2019-01-21 20:52 ` [PATCH 31/37] solver: force quoted-printable bodies to LF Eric Wong
2019-01-21 20:52 ` [PATCH 32/37] solver: remove extra "^index $OID..$OID" line Eric Wong
2019-01-21 20:52 ` [PATCH 33/37] config: each_inbox iteration preserves config order Eric Wong
2019-01-21 20:52 ` [PATCH 34/37] t/check-www-inbox: warn on missing Content-Type Eric Wong
2019-01-21 20:52 ` [PATCH 35/37] highlight: initial wrapper and PSGI service Eric Wong
2019-01-21 20:52 ` [PATCH 36/37] hval: split out escape sequences to a separate table Eric Wong
2019-01-21 20:52 ` [PATCH 37/37] t/check-www-inbox: trap SIGINT for File::Temp destruction Eric Wong

Reply instructions:

You may reply publically to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: http://public-inbox.org/README

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20190121205253.10455-10-e@80x24.org \
    --to=e@80x24.org \
    --cc=meta@public-inbox.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

user/dev discussion of public-inbox itself

Archives are clonable:
	git clone --mirror http://public-inbox.org/meta
	git clone --mirror http://czquwvybam4bgbro.onion/meta
	git clone --mirror http://hjrcffqmbrq6wope.onion/meta
	git clone --mirror http://ou63pmih66umazou.onion/meta

Newsgroups are available over NNTP:
	nntp://news.public-inbox.org/inbox.comp.mail.public-inbox.meta
	nntp://ou63pmih66umazou.onion/inbox.comp.mail.public-inbox.meta
	nntp://czquwvybam4bgbro.onion/inbox.comp.mail.public-inbox.meta
	nntp://hjrcffqmbrq6wope.onion/inbox.comp.mail.public-inbox.meta
	nntp://news.gmane.org/gmane.mail.public-inbox.general

 note: .onion URLs require Tor: https://www.torproject.org/

AGPL code for this site: git clone https://public-inbox.org/ public-inbox