]> Sergey Matveev's repositories - public-inbox.git/commitdiff
view: wire up diff and vcs viewers with solver
authorEric Wong <e@80x24.org>
Thu, 17 Jan 2019 11:51:34 +0000 (11:51 +0000)
committerEric Wong <e@80x24.org>
Sat, 19 Jan 2019 03:34:54 +0000 (03:34 +0000)
MANIFEST
lib/PublicInbox/Config.pm
lib/PublicInbox/View.pm
lib/PublicInbox/ViewDiff.pm [new file with mode: 0644]
lib/PublicInbox/ViewVCS.pm [new file with mode: 0644]
lib/PublicInbox/WWW.pm

index 95ad0c6e04beb700103f4fff53f32a6807e84a69..5e980fe61130c36d0fa0cd300d90d9255b33dc8e 100644 (file)
--- 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
index bea26176608e3cb30a1c958fdc52173d5887ddf2..355e64bfec6a19036519054b17a716641d2c7310 100644 (file)
@@ -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
 }
 
index 470e3ab7557a8df48f0cf95e9e8eb568389d71f0..0187ec332a58faaa4c89b6bb349a5fbb07f9c5fa 100644 (file)
@@ -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 (file)
index 0000000..ee450fa
--- /dev/null
@@ -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 (file)
index 0000000..49fb1c5
--- /dev/null
@@ -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;
index 3562e46c92a367a3ac318f8e0a79b7ac901f5448..c73370f9a233d82d7c89fc10e7937e87154465e6 100644 (file)
@@ -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};