From 1e7a2bbd2c7b0c1d5f989c0e225d22276055eff1 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Tue, 12 Jan 2016 21:32:33 +0000 Subject: repobrowse: change Perl capitalization to "Repobrowse" We mainly call it "repobrowse" (all lowercase), so do not imply it is two separate words by capitalizing "Browse". --- lib/PublicInbox/RepoBrowse.pm | 106 --------- lib/PublicInbox/RepoBrowseBase.pm | 66 ------ lib/PublicInbox/RepoBrowseGit.pm | 68 ------ lib/PublicInbox/RepoBrowseGitBlob.pm | 78 ------- lib/PublicInbox/RepoBrowseGitCommit.pm | 363 ------------------------------- lib/PublicInbox/RepoBrowseGitFallback.pm | 87 -------- lib/PublicInbox/RepoBrowseGitLog.pm | 118 ---------- lib/PublicInbox/RepoBrowseGitPatch.pm | 47 ---- lib/PublicInbox/RepoBrowseGitPlain.pm | 81 ------- lib/PublicInbox/RepoBrowseGitTag.pm | 187 ---------------- lib/PublicInbox/RepoBrowseGitTree.pm | 187 ---------------- lib/PublicInbox/RepoBrowseQuery.pm | 42 ---- lib/PublicInbox/RepoConfig.pm | 57 ----- lib/PublicInbox/Repobrowse.pm | 106 +++++++++ lib/PublicInbox/RepobrowseBase.pm | 66 ++++++ lib/PublicInbox/RepobrowseConfig.pm | 57 +++++ lib/PublicInbox/RepobrowseGit.pm | 68 ++++++ lib/PublicInbox/RepobrowseGitBlob.pm | 78 +++++++ lib/PublicInbox/RepobrowseGitCommit.pm | 363 +++++++++++++++++++++++++++++++ lib/PublicInbox/RepobrowseGitFallback.pm | 87 ++++++++ lib/PublicInbox/RepobrowseGitLog.pm | 118 ++++++++++ lib/PublicInbox/RepobrowseGitPatch.pm | 47 ++++ lib/PublicInbox/RepobrowseGitPlain.pm | 81 +++++++ lib/PublicInbox/RepobrowseGitTag.pm | 187 ++++++++++++++++ lib/PublicInbox/RepobrowseGitTree.pm | 187 ++++++++++++++++ lib/PublicInbox/RepobrowseQuery.pm | 42 ++++ 26 files changed, 1487 insertions(+), 1487 deletions(-) delete mode 100644 lib/PublicInbox/RepoBrowse.pm delete mode 100644 lib/PublicInbox/RepoBrowseBase.pm delete mode 100644 lib/PublicInbox/RepoBrowseGit.pm delete mode 100644 lib/PublicInbox/RepoBrowseGitBlob.pm delete mode 100644 lib/PublicInbox/RepoBrowseGitCommit.pm delete mode 100644 lib/PublicInbox/RepoBrowseGitFallback.pm delete mode 100644 lib/PublicInbox/RepoBrowseGitLog.pm delete mode 100644 lib/PublicInbox/RepoBrowseGitPatch.pm delete mode 100644 lib/PublicInbox/RepoBrowseGitPlain.pm delete mode 100644 lib/PublicInbox/RepoBrowseGitTag.pm delete mode 100644 lib/PublicInbox/RepoBrowseGitTree.pm delete mode 100644 lib/PublicInbox/RepoBrowseQuery.pm delete mode 100644 lib/PublicInbox/RepoConfig.pm create mode 100644 lib/PublicInbox/Repobrowse.pm create mode 100644 lib/PublicInbox/RepobrowseBase.pm create mode 100644 lib/PublicInbox/RepobrowseConfig.pm create mode 100644 lib/PublicInbox/RepobrowseGit.pm create mode 100644 lib/PublicInbox/RepobrowseGitBlob.pm create mode 100644 lib/PublicInbox/RepobrowseGitCommit.pm create mode 100644 lib/PublicInbox/RepobrowseGitFallback.pm create mode 100644 lib/PublicInbox/RepobrowseGitLog.pm create mode 100644 lib/PublicInbox/RepobrowseGitPatch.pm create mode 100644 lib/PublicInbox/RepobrowseGitPlain.pm create mode 100644 lib/PublicInbox/RepobrowseGitTag.pm create mode 100644 lib/PublicInbox/RepobrowseGitTree.pm create mode 100644 lib/PublicInbox/RepobrowseQuery.pm (limited to 'lib/PublicInbox') diff --git a/lib/PublicInbox/RepoBrowse.pm b/lib/PublicInbox/RepoBrowse.pm deleted file mode 100644 index ec54ed05..00000000 --- a/lib/PublicInbox/RepoBrowse.pm +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (C) 2015 all contributors -# License: AGPL-3.0+ - -# Version control system (VCS) repository viewer like cgit or gitweb, -# but with optional public-inbox archive integration. -# This uses cgit-compatible PATH_INFO URLs. -# This may be expanded to support other Free Software VCSes such as -# Subversion and Mercurial, so not just git -# -# Same web design principles as PublicInbox::WWW for supporting the -# lowest common denominators (see bottom of Documentation/design_www.txt) -# -# This allows an M:N relationship between "normal" repos for project -# and public-inbox (ssoma) git repositories where N may be zero. -# In other words, RepoBrowse must work for repositories without -# any public-inbox at all; or with multiple public-inboxes. -# And the rest of public-inbox will always work without a "normal" -# code repo for the project. - -package PublicInbox::RepoBrowse; -use strict; -use warnings; -use URI::Escape qw(uri_escape_utf8 uri_unescape); -use PublicInbox::RepoConfig; - -my %CMD = map { lc($_) => $_ } qw(Log Commit Tree Patch Blob Plain Tag); -my %VCS = (git => 'Git'); -my %LOADED; - -sub new { - my ($class, $file) = @_; - bless { rconfig => PublicInbox::RepoConfig->new($file) }, $class; -} - -# simple response for errors -sub r { [ $_[0], ['Content-Type' => 'text/plain'], [ join(' ', @_, "\n") ] ] } - -sub run { - my ($self, $cgi, $method) = @_; - return r(405, 'Method Not Allowed') if ($method !~ /\AGET|HEAD\z/); - - # URL syntax: / repo [ / cmd [ / path ] ] - # cmd: log | commit | diff | tree | view | blob | snapshot - # repo and path (@extra) may both contain '/' - my $rconfig = $self->{rconfig}; - my $path_info = uri_unescape($cgi->path_info); - my (undef, $repo_path, @extra) = split(m{/+}, $path_info, -1); - - return r404() unless $repo_path; - my $repo_info; - until ($repo_info = $rconfig->lookup($repo_path)) { - my $p = shift @extra or last; - $repo_path .= "/$p"; - } - return r404() unless $repo_info; - - my $req = { - repo_info => $repo_info, - extra => \@extra, # path - cgi => $cgi, - rconfig => $rconfig, - tslash => 0, - }; - - my $cmd = shift @extra; - if (defined $cmd && length $cmd) { - my $vcs_lc = $repo_info->{vcs}; - my $vcs = $VCS{$vcs_lc} or return r404(); - my $mod = $CMD{$cmd}; - unless ($mod) { - unshift @extra, $cmd; - $mod = 'Fallback'; - } - $mod = load_once("PublicInbox::RepoBrowse$vcs$mod"); - $vcs = load_once("PublicInbox::$vcs"); - $repo_info->{$vcs_lc} ||= $vcs->new($repo_info->{path}); - $req->{relcmd} = '../' x scalar(@extra); - while (@extra && $extra[-1] eq '') { - pop @extra; - ++$req->{tslash}; - } - $req->{expath} = join('/', @extra); - my $rv = eval { $mod->new->call($cmd, $req) }; - $rv || r404(); - } else { - $req->{relcmd} = defined $cmd ? '' : './'; - summary($req); - } -} - -sub summary { - r404(); -} - -sub r404 { r(404, 'Not Found') } - -sub load_once { - my ($mod) = @_; - - return $mod if $LOADED{$mod}; - eval "require $mod"; - $LOADED{$mod} = 1 unless $@; - $mod; -} - -1; diff --git a/lib/PublicInbox/RepoBrowseBase.pm b/lib/PublicInbox/RepoBrowseBase.pm deleted file mode 100644 index 2ff5680a..00000000 --- a/lib/PublicInbox/RepoBrowseBase.pm +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (C) 2015 all contributors -# License: AGPL-3.0+ -package PublicInbox::RepoBrowseBase; -use strict; -use warnings; -require PublicInbox::RepoBrowseQuery; -use PublicInbox::Hval; - -sub new { bless {}, shift } - -sub call { - my ($self, $cmd, $req) = @_; - my $vcs = $req->{repo_info}->{vcs}; - my $rv = eval { - no strict 'refs'; - my $sub = "call_${vcs}_$cmd"; - $self->$sub($req); - }; - $@ ? [ 500, ['Content-Type'=>'text/plain'], [] ] : $rv; -} - -sub mime_load { - my ($self, $file) = @_; - my %rv; - open my $fh, '<', $file or return \%rv; - foreach (<$fh>) { - next if /^#/; # no comments - my ($type, @ext) = split(/\s+/); - - if (defined $type) { - $rv{$_} = $type foreach @ext; - } - } - \%rv; -} - -# returns undef if missing, so users can scan the blob if needed -sub mime_type_unsafe { - my ($self, $fn) = @_; - $fn =~ /\.([^\.]+)\z/ or return; - my $ext = $1; - my $m = $self->{mime_types} ||= $self->mime_load('/etc/mime.types'); - $m->{$ext}; -} - -sub mime_type { - my ($self, $fn) = @_; - my $ct = $self->mime_type_unsafe($fn); - - # XSS protection. Assume the browser knows what to do - # with images/audio/video; but don't allow random HTML from - # a repository to be served - (defined($ct) && $ct =~ m!\A(?:image|audio|video)/!) ? $ct : undef; -} - -# starts an HTML page for Repobrowse in a consistent way -sub html_start { - my ($self, $req, $title_html) = @_; - my $desc = $req->{repo_info}->{desc_html}; - - "$title_html" . - PublicInbox::Hval::STYLE . - "
$desc";
-}
-
-1;
diff --git a/lib/PublicInbox/RepoBrowseGit.pm b/lib/PublicInbox/RepoBrowseGit.pm
deleted file mode 100644
index 498b82c7..00000000
--- a/lib/PublicInbox/RepoBrowseGit.pm
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright (C) 2015 all contributors 
-# License: AGPL-3.0+ (https://www.gnu.org/licenses/agpl-3.0.txt)
-
-# common functions used by other RepoBrowseGit* modules
-package PublicInbox::RepoBrowseGit;
-use strict;
-use warnings;
-use base qw(Exporter);
-our @EXPORT_OK = qw(git_unquote git_commit_title git_dec_links);
-use PublicInbox::Hval qw(utf8_html);
-
-my %GIT_ESC = (
-	a => "\a",
-	b => "\b",
-	f => "\f",
-	n => "\n",
-	r => "\r",
-	t => "\t",
-	v => "\013",
-);
-
-sub git_unquote {
-	my ($s) = @_;
-	return $s unless ($s =~ /\A"(.*)"\z/);
-	$s = $1;
-	$s =~ s/\\([abfnrtv])/$GIT_ESC{$1}/g;
-	$s =~ s/\\([0-7]{1,3})/chr(oct($1))/ge;
-	$s;
-}
-
-sub git_commit_title {
-	my ($git, $obj) = @_; # PublicInbox::Git, $sha1hex
-	my $rv;
-	eval {
-		my $buf = $git->cat_file($obj);
-		($rv) = ($$buf =~ /\r?\n\r?\n([^\r\n]+)\r?\n?/);
-	};
-	$rv;
-}
-
-# example inputs: "HEAD -> master", "tag: v1.0.0",
-sub git_dec_links {
-	my ($rel, $D) = @_;
-	my @l;
-	foreach (split /, /, $D) {
-		if (/\A(\S+) -> (\S+)/) { # 'HEAD -> master'
-			my ($s, $h) = ($1, $2);
-			$s = utf8_html($s);
-			$h = PublicInbox::Hval->utf8($h);
-			my $r = $h->as_href;
-			$h = $h->as_html;
-			push @l, qq($s -> $h);
-		} elsif (s/\Atag: //) {
-			my $h = PublicInbox::Hval->utf8($_);
-			my $r = $h->as_href;
-			$h = $h->as_html;
-			push @l, qq($h);
-		} else {
-			my $h = PublicInbox::Hval->utf8($_);
-			my $r = $h->as_href;
-			$h = $h->as_html;
-			push @l, qq($h);
-		}
-	}
-	@l;
-}
-
-1;
diff --git a/lib/PublicInbox/RepoBrowseGitBlob.pm b/lib/PublicInbox/RepoBrowseGitBlob.pm
deleted file mode 100644
index 3be069f7..00000000
--- a/lib/PublicInbox/RepoBrowseGitBlob.pm
+++ /dev/null
@@ -1,78 +0,0 @@
-# Copyright (C) 2015-2016 all contributors 
-# License: AGPL-3.0+ 
-
-# Show a blob as-is
-package PublicInbox::RepoBrowseGitBlob;
-use strict;
-use warnings;
-use base qw(PublicInbox::RepoBrowseBase);
-use base qw(Exporter);
-our @EXPORT = qw(git_blob_mime_type git_blob_stream_response);
-
-sub call_git_blob {
-	my ($self, $req) = @_;
-	my $git = $req->{repo_info}->{git};
-	my $q = PublicInbox::RepoBrowseQuery->new($req->{cgi});
-	my $id = $q->{id};
-	$id eq '' and $id = 'HEAD';
-
-	if (length(my $expath = $req->{expath})) {
-		$id .= ":$expath";
-	}
-	my ($cat, $hex, $type, $size) = $git->cat_file_begin($id);
-	return unless defined $cat;
-
-	my ($r, $buf);
-	my $left = $size;
-	if ($type eq 'blob') {
-		$type = git_blob_mime_type($self, $req, $cat, \$buf, \$left);
-	} elsif ($type eq 'commit' || $type eq 'tag') {
-		$type = 'text/plain';
-	} else {
-		$type = 'application/octet-stream';
-	}
-	git_blob_stream_response($git, $cat, $size, $type, $buf, $left);
-}
-
-sub git_blob_mime_type {
-	my ($self, $req, $cat, $buf, $left) = @_;
-	my $base = $req->{extra}->[-1];
-	my $type = $self->mime_type($base) if defined $base;
-	return $type if $type;
-
-	my $to_read = 8000; # git uses this size to detect binary files
-	$to_read = $$left if $to_read > $$left;
-	my $r = read($cat, $$buf, $to_read);
-	if (!defined $r || $r <= 0) {
-		my $git = $req->{repo_info}->{git};
-		$git->cat_file_finish($$left);
-		return;
-	}
-	$$left -= $r;
-	(index($buf, "\0") < 0) ?  'text/plain' : 'application/octet-stream';
-}
-
-sub git_blob_stream_response {
-	my ($git, $cat, $size, $type, $buf, $left) = @_;
-
-	sub {
-		my ($res) = @_;
-		my $to_read = 8192;
-		eval {
-			my $fh = $res->([ 200, ['Content-Length' => $size,
-						'Content-Type' => $type]]);
-			$fh->write($buf) if defined $buf;
-			while ($left > 0) {
-				$to_read = $left if $to_read > $left;
-				my $r = read($cat, $buf, $to_read);
-				last if (!defined $r || $r <= 0);
-				$left -= $r;
-				$fh->write($buf);
-			}
-			$fh->close;
-		};
-		$git->cat_file_finish($left);
-	}
-}
-
-1;
diff --git a/lib/PublicInbox/RepoBrowseGitCommit.pm b/lib/PublicInbox/RepoBrowseGitCommit.pm
deleted file mode 100644
index 6278c53a..00000000
--- a/lib/PublicInbox/RepoBrowseGitCommit.pm
+++ /dev/null
@@ -1,363 +0,0 @@
-# Copyright (C) 2015 all contributors 
-# License: AGPL-3.0+ 
-
-# shows the /commit/ endpoint for git repositories
-package PublicInbox::RepoBrowseGitCommit;
-use strict;
-use warnings;
-use base qw(PublicInbox::RepoBrowseBase);
-use PublicInbox::Hval qw(utf8_html);
-use PublicInbox::RepoBrowseGit qw(git_unquote git_commit_title);
-
-use constant GIT_FMT => '--pretty=format:'.join('%n',
-	'%H', '%h', '%s', '%an <%ae>', '%ai', '%cn <%ce>', '%ci',
-	'%t', '%p', '%D', '%b%x00');
-
-sub git_commit_stream {
-	my ($req, $q, $H, $log, $fh) = @_;
-	chomp(my $h = <$log>); # abbreviated commit
-	my $l;
-	chomp(my $s = utf8_html($l = <$log>)); # subject
-	chomp(my $au = utf8_html($l = <$log>)); # author
-	chomp(my $ad = <$log>);
-	chomp(my $cu = utf8_html($l = <$log>));
-	chomp(my $cd = <$log>);
-	chomp(my $t = <$log>); # tree
-	chomp(my $p = <$log>); # parents
-	my @p = split(' ', $p);
-	chomp(my $D = <$log>); # TODO: decorate
-	my $git = $req->{repo_info}->{git};
-
-	my $rel = $req->{relcmd};
-	my $qs = $q->qs(id => $h);
-	chomp $H;
-	my $x = "$s" .
-		PublicInbox::Hval::STYLE . '
' .
-		"   commit $H (patch)\n" .
-		"     tree $t";
-
-	# extra show path information, if any
-	my $extra = $req->{extra};
-	my $path = '';
-	if (@$extra) {
-		my @t;
-		my $ep;
-		$x .= ' -- ';
-		$x .= join('/', map {
-			push @t, $_;
-			my $e = PublicInbox::Hval->utf8($_, join('/', @t));
-			$ep = $e->as_path;
-			my $eh = $e->as_html;
-			"$eh";
-		} @$extra);
-		$path = "/$ep";
-	}
-
-	$x .= "\n   author $au\t$ad\ncommitter $cu\t$cd\n";
-	my $np = scalar @p;
-	if ($np == 1) {
-		my $p = $p[0];
-		$x .= git_parent_line('   parent', $p, $q, $git, $rel, $path);
-	} elsif ($np > 1) {
-		my @common = ($q, $git, $rel, $path);
-		my @t = @p;
-		my $p = shift @t;
-		$x .= git_parent_line('  parents', $p, @common);
-		foreach $p (@t) {
-			$x .= git_parent_line('         ', $p, @common);
-		}
-	}
-	$fh->write($x .= "\n$s\n\n");
-
-	# body:
-	local $/ = "\0";
-	$l = <$log>;
-	chomp $l;
-	$fh->write(utf8_html($l)."---\n");
-	git_show_diffstat($req, $h, $fh, $log);
-	my $help;
-	$help = " This is a merge, showing combined diff:\n\n" if ($np > 1);
-
-	# diff
-	local $/ = "\n";
-	my $cmt = '[a-f0-9]+';
-	my $diff = { h => $h, p => \@p, rel => $rel };
-	my $cc_add;
-	while (defined($l = <$log>)) {
-		if ($help) {
-			$fh->write($help);
-			$help = undef;
-		}
-		if ($l =~ m{^diff --git ("?a/.+) ("?b/.+)$}) { # regular
-			$l = git_diff_ab_hdr($diff, $1, $2) . "\n";
-		} elsif ($l =~ m{^diff --(cc|combined) (.+)$}) {
-			$l = git_diff_cc_hdr($diff, $1, $2) . "\n";
-		} elsif ($l =~ /^index ($cmt)\.\.($cmt)(.*)$/o) { # regular
-			$l = git_diff_ab_index($diff, $1, $2, $3) . "\n";
-		} elsif ($l =~ /^@@ (\S+) (\S+) @@(.*)$/) { # regular
-			$l = git_diff_ab_hunk($diff, $1, $2, $3) . "\n";
-		} elsif ($l =~ /^\+/ || ($cc_add && $l =~ $cc_add)) {
-			$l = git_diff_add($l) . "\n";
-		} elsif ($l =~ /^index ($cmt,[^\.]+)\.\.($cmt)(.*)$/o) { # --cc
-			$l = git_diff_cc_index($diff, $1, $2, $3) . "\n";
-			$cc_add ||= $diff->{cc_add};
-		} elsif ($l =~ /^(@@@+) (\S+.*\S+) @@@+(.*)$/) { # --cc
-			$l = git_diff_cc_hunk($diff, $1, $2, $3) . "\n";
-		} else {
-			$l = utf8_html($l);
-		}
-		$fh->write($l);
-	}
-
-	if ($help) {
-		$fh->write(" This is a merge, combined diff is empty.\n");
-	}
-	$fh->write('
'); -} - -sub call_git_commit { - my ($self, $req) = @_; - - my $q = PublicInbox::RepoBrowseQuery->new($req->{cgi}); - my $id = $q->{id}; - $id eq '' and $id = 'HEAD'; - my $git = $req->{repo_info}->{git}; - my @cmd = qw(show -z --numstat -p --encoding=UTF-8 - --no-notes --no-color --abbrev=10 -c); - my @path; - - # kill trailing slash - my $extra = $req->{extra}; - if (@$extra) { - pop @$extra if $extra->[-1] eq ''; - @path = (join('/', @$extra)); - push @cmd, '--follow'; - } - - my $log = $git->popen(@cmd, GIT_FMT, $id, '--', @path); - my $H = <$log>; - - # maybe the path didn't exist, yet, zip them back up - return git_commit_404($req, $q, $path[0]) unless defined $H; - sub { - my ($res) = @_; # Plack callback - my $fh = $res->([200, ['Content-Type'=>'text/html']]); - git_commit_stream($req, $q, $H, $log, $fh); - $fh->close; - } -} - -sub git_commit_404 { - my ($req, $q, $path) = @_; - my $x = 'Missing commit or path'; - my $pfx = "$req->{relcmd}commit"; - - # print STDERR "path: $path\n"; - my $try = 'try'; - $x = "$x
$x\n\n";
-	if (defined $path) {
-		my $qs = $q->qs;
-		$x .= "" .
-			"try without the path $path\n";
-		$try = 'or';
-	}
-	my $qs = $q->qs(id => '');
-	$x .= "$try the latest commit in HEAD\n";
-	$x .= '
'; - - [ 404, ['Content-Type'=>'text/html'], [ $x ] ]; -} - -sub git_show_diffstat { - my ($req, $h, $fh, $log) = @_; - local $/ = "\0\0"; - my $l = <$log>; - chomp $l; - my @stat = split("\0", $l); - my $nr = 0; - my ($nadd, $ndel) = (0, 0); - my $rel = $req->{relcmd}; - while (defined($l = shift @stat)) { - $l =~ s/\n?(\S+)\t+(\S+)\t+// or next; - my ($add, $del) = ($1, $2); - if ($add =~ /\A\d+\z/) { - $nadd += $add; - $ndel += $del; - $add = "+$add"; - $del = "-$del"; - } - my $num = sprintf('% 6s/%-6s', $del, $add); - if (length $l) { - $l = PublicInbox::Hval->utf8($l); - my $lp = $l->as_path; - my $lh = $l->as_html; - $l = "$lh"; - - } else { - my $from = shift @stat; - my $to = shift @stat; - $l = git_diffstat_rename($rel, $h, $from, $to); - } - ++$nr; - $fh->write(' '.$num."\t".$l."\n"); - } - $l = "\n $nr "; - $l .= $nr == 1 ? 'file changed, ' : 'files changed, '; - $l .= $nadd; - $l .= $nadd == 1 ? ' insertion(+), ' : ' insertions(+), '; - $l .= $ndel; - $l .= $ndel == 1 ? " deletion(-)\n\n" : " deletions(-)\n\n"; - $fh->write($l); -} - -# index abcdef89..01234567 -sub git_diff_ab_index { - my ($diff, $xa, $xb, $end) = @_; - # not wasting bandwidth on links here, yet - # links in hunk headers are far more useful with line offsets - $end = utf8_html($end); - "index $xa..$xb$end"; -} - -# diff --git a/foo.c b/bar.c -sub git_diff_ab_hdr { - my ($diff, $fa, $fb) = @_; - my $html_a = utf8_html($fa); - my $html_b = utf8_html($fb); - $fa = git_unquote($fa); - $fb = git_unquote($fb); - $fa =~ s!\Aa/!!; - $fb =~ s!\Ab/!!; - $fa = $diff->{fa} = PublicInbox::Hval->utf8($fa); - $fb = $diff->{fb} = PublicInbox::Hval->utf8($fb); - $diff->{path_a} = $fa->as_path; - $diff->{path_b} = $fb->as_path; - - # not wasting bandwidth on links here, yet - # links in hunk headers are far more useful with line offsets - "diff --git $html_a $html_b"; -} - -# @@ -1,2 +3,4 @@ (regular diff) -sub git_diff_ab_hunk { - my ($diff, $ca, $cb, $ctx) = @_; - my ($na) = ($ca =~ /\A-(\d+)/); - my ($nb) = ($cb =~ /\A\+(\d+)/); - - my $rel = $diff->{rel}; - my $rv = '@@ '; - if ($na == 0) { # new file - $rv .= $ca; - } else { - my $p = $diff->{p}->[0]; - $rv .= "{path_a}?id=$p#n$na\">"; - $rv .= "$ca"; - } - $rv .= ' '; - if ($nb == 0) { # deleted file - $rv .= $cb; - } else { - my $h = $diff->{h}; - $rv .= "{path_b}?id=$h#n$nb\">"; - $rv .= "$cb"; - } - $rv . ' @@' . utf8_html($ctx); -} - -sub git_diff_cc_hdr { - my ($diff, $combined, $path) = @_; - my $html_path = utf8_html($path); - my $cc = $diff->{cc} = PublicInbox::Hval->utf8(git_unquote($path)); - $diff->{path_cc} = $cc->as_path; - "diff --$combined $html_path"; -} - -# index abcdef09,01234567..76543210 -sub git_diff_cc_index { - my ($diff, $before, $last, $end) = @_; - $end = utf8_html($end); - my @before = split(',', $before); - $diff->{pobj_cc} = \@before; - $diff->{cc_add} ||= eval { - my $n = scalar(@before) - 1; - qr/^ {0,$n}[\+]/; - }; - - # not wasting bandwidth on links here, yet - # links in hunk headers are far more useful with line offsets - "index $before..$last$end"; -} - -# @@@ -1,2 -3,4 +5,6 @@@ (combined diff) -sub git_diff_cc_hunk { - my ($diff, $at, $offs, $ctx) = @_; - my @offs = split(' ', $offs); - my $last = pop @offs; - my @p = @{$diff->{p}}; - my @pobj = @{$diff->{pobj_cc}}; - my $path = $diff->{path_cc}; - my $rel = $diff->{rel}; - my $rv = $at; - - # special 'cc' action as we don't have reliable paths from parents - my $ppath = "${rel}cc/$path"; - foreach my $off (@offs) { - my $p = shift @p; - my $obj = shift @pobj; # blob SHA-1 - my ($n) = ($off =~ /\A-(\d+)/); # line number - - if ($n == 0) { # new file (does this happen with --cc?) - $rv .= " $off"; - } else { - $rv .= " "; - $rv .= "$off"; - } - } - - # we can use the normal 'tree' endpoint for the result - my ($n) = ($last =~ /\A\+(\d+)/); # line number - if ($n == 0) { # deleted file (does this happen with --cc?) - $rv .= " $last"; - } else { - my $h = $diff->{h}; - $rv .= " "; - $rv .= "$last"; - } - $rv .= " $at" . utf8_html($ctx); -} - -sub git_diffstat_rename { - my ($rel, $h, $from, $to) = @_; - my @from = split('/', $from); - my @to = split('/', $to); - my $orig_to = $to; - my ($base, @base); - while (@to && @from && $to[0] eq $from[0]) { - push @base, shift(@to); - shift @from; - } - - $base = utf8_html(join('/', @base)) if @base; - $from = utf8_html(join('/', @from)); - $to = PublicInbox::Hval->utf8(join('/', @to), $orig_to); - my $tp = $to->as_path; - my $th = $to->as_html; - $to = "$th"; - @base ? "$base/{$from => $to}" : "$from => $to"; -} - -sub git_diff_add { - my ($l) = @_; - chomp $l; - ''.utf8_html($l).''; -} - -sub git_parent_line { - my ($pfx, $p, $q, $git, $rel, $path) = @_; - my $qs = $q->qs(id => $p); - my $t = git_commit_title($git, $p); - $t = defined $t ? utf8_html($t) : ''; - $pfx . " $p ". $t . "\n"; -} - -1; diff --git a/lib/PublicInbox/RepoBrowseGitFallback.pm b/lib/PublicInbox/RepoBrowseGitFallback.pm deleted file mode 100644 index 03b282ef..00000000 --- a/lib/PublicInbox/RepoBrowseGitFallback.pm +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (C) 2015 all contributors -# License: AGPL-3.0+ (https://www.gnu.org/licenses/agpl-3.0.txt) - -# when no endpoints match, fallback to this and serve a static file -# This can serve Smart HTTP in the future. -package PublicInbox::RepoBrowseGitFallback; -use strict; -use warnings; -use base qw(PublicInbox::RepoBrowseBase); -use Fcntl qw(:seek); - -# overrides PublicInbox::RepoBrowseBase::call -sub call { - my ($self, undef, $req) = @_; - my $expath = $req->{expath}; - return if index($expath, '..') >= 0; # prevent path traversal - - my $git = $req->{repo_info}->{git}; - my $f = "$git->{git_dir}/$expath"; - return unless -f $f && -r _; - my @st = stat(_); - my ($size, $mtime) = ($st[7], $st[9]); - # TODO: if-modified-since and last-modified... - open my $in, '<', $f or return; - my $code = 200; - my $len = $size; - my @h; - - # FIXME: this is Plack-only - my $range = eval { $req->{cgi}->{env}->{HTTP_RANGE} }; - if (defined $range && $range =~ /\bbytes=(\d*)-(\d*)\z/) { - ($code, $len) = prepare_range($req, $in, \@h, $1, $2, $size); - } - - # we use the unsafe variant since we assume the server admin - # would not place untrusted HTML/JS/CSS in the git directory - my $type = $self->mime_type_unsafe($expath) || 'text/plain'; - push @h, 'Content-Type', $type, 'Content-Length', $len; - sub { - my ($res) = @_; # Plack callback - my $fh = $res->([ $code, \@h ]); - my $buf; - my $n = 8192; - while ($size > 0) { - $n = $size if $size < $n; - my $r = read($in, $buf, $n); - last if (!defined($r) || $r <= 0); - $fh->write($buf); - } - $fh->close; - } -} - -sub bad_range { [ 416, [], [] ] } - -sub prepare_range { - my ($req, $in, $h, $beg, $end, $size) = @_; - my $code = 200; - my $len = $size; - if ($beg eq '') { - if ($end ne '') { # last N bytes - $beg = $size - $end; - $beg = 0 if $beg < 0; - $end = $size - 1; - $code = 206; - } - } else { - if ($end eq '' || $end >= $size) { - $end = $size - 1; - $code = 206; - } elsif ($end < $size) { - $code = 206; - } - } - if ($code == 206) { - $len = $end - $beg + 1; - seek($in, $beg, SEEK_SET) or return [ 500, [], [] ]; - push @$h, qw(Accept-Ranges bytes), - 'Content-Range', "bytes $beg-$end/$size"; - - # FIXME: Plack::Middleware::Deflater bug? - $req->{cgi}->{env}->{'psgix.no-compress'} = 1; - } - ($code, $len); -} - -1; diff --git a/lib/PublicInbox/RepoBrowseGitLog.pm b/lib/PublicInbox/RepoBrowseGitLog.pm deleted file mode 100644 index 9192c7fc..00000000 --- a/lib/PublicInbox/RepoBrowseGitLog.pm +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (C) 2015 all contributors -# License: AGPL-3.0+ - -package PublicInbox::RepoBrowseGitLog; -use strict; -use warnings; -use PublicInbox::Hval qw(utf8_html); -use base qw(PublicInbox::RepoBrowseBase); -use PublicInbox::RepoBrowseGit qw(git_dec_links git_commit_title); -# cannot rely on --date=format-local:... yet, it is too new (September 2015) -my $LOG_FMT = '--pretty=tformat:'. - join('%x00', qw(%h %p %s D%D)); -my $MSG_FMT = join('%x00', '', qw(%ai a%an b%b)); - -sub call_git_log { - my ($self, $req) = @_; - my $repo_info = $req->{repo_info}; - my $max = $repo_info->{max_commit_count} || 50; - $max = int($max); - $max = 50 if $max == 0; - - my $q = PublicInbox::RepoBrowseQuery->new($req->{cgi}); - my $h = $q->{h}; - $h eq '' and $h = 'HEAD'; - - my $fmt = $LOG_FMT; - $fmt .= $MSG_FMT if $q->{showmsg}; - $fmt .= '%x00%x00'; - - my $git = $repo_info->{git}; - my $log = $git->popen(qw(log --no-notes --no-color - --abbrev-commit --abbrev=12), - $fmt, "-$max", $h); - sub { - my ($res) = @_; # Plack callback - my $fh = $res->([200, ['Content-Type'=>'text/html']]); - git_log_stream($req, $q, $log, $fh, $git); - $fh->close; - } -} - -sub git_log_stream { - my ($req, $q, $log, $fh, $git) = @_; - my $desc = $req->{repo_info}->{desc_html}; - my $showmsg = $q->{showmsg}; - - my $x = 'commit log '; - if ($showmsg) { - $showmsg = "&showmsg=1"; - my $qs = $q->qs(showmsg => ''); - $qs = $req->{cgi}->path_info if ($qs eq ''); - $x .= qq{[oneline|expand]}; - } else { - my $qs = $q->qs(showmsg => 1); - $x .= qq{[oneline|expand]}; - } - - my $rel = $req->{relcmd}; - $fh->write('' . PublicInbox::Hval::STYLE . - "$desc
$desc\n\n".
-		qq!commit\t\t$x\n!);
-	$fh->write($showmsg ? '
' : "\n"); - my %acache; - local $/ = "\0\0\n"; - my $nr = 0; - my (@parents, %seen); - while (defined(my $line = <$log>)) { - my ($id, $p, $s, $D, $ai, $an, $b) = split("\0", $line); - $seen{$id} = 1; - my @p = split(' ', $p); - push @parents, @p; - - $s = utf8_html($s); - $s = qq($s); - if ($D =~ /\AD(.+)/) { - $s .= ' ('. join(', ', git_dec_links($rel, $1)) . ')'; - } - - if (defined $b) { - $an =~ s/\Aa//; - $b =~ s/\Ab//; - $b =~ s/\s*\z//s; - - my $ah = $acache{$an} ||= utf8_html($an); - my $x = "
$id";
-			my $nl = $b eq '' ? '' : "\n"; # empty bodies :<
-			$b = $x . '  
' .
-				"$s\n- $ah @ $ai\n$nl" .
-				utf8_html($b) . '
'; - } else { - $b = qq($id\t$s\n); - } - $fh->write($b); - ++$nr; - } - - my $m = ''; - my $np = 0; - foreach my $p (@parents) { - next if $seen{$p}; - $seen{$p} = ++$np; - my $s = git_commit_title($git, $p); - $m .= qq(\n$p\t); - $s = defined($s) ? utf8_html($s) : ''; - $m .= qq($s); - } - my $foot = $showmsg ? "
\t\t$x\n\n" : "\n\t\t$x\n\n";
-	if ($np == 0) {
-		$foot .= "No commits follow";
-	} elsif ($np > 1) {
-		$foot .= "Unseen parent commits to follow (multiple choice):\n";
-	} else {
-		$foot .= "Next parent to follow:\n";
-	}
-	$fh->write($foot .= $m . '
'); -} - -1; diff --git a/lib/PublicInbox/RepoBrowseGitPatch.pm b/lib/PublicInbox/RepoBrowseGitPatch.pm deleted file mode 100644 index 432bbd3c..00000000 --- a/lib/PublicInbox/RepoBrowseGitPatch.pm +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (C) 2015 all contributors -# License: AGPL-3.0+ - -# shows the /patch/ endpoint for git repositories -# usage: /repo.git/patch?id=COMMIT_ID -package PublicInbox::RepoBrowseGitPatch; -use strict; -use warnings; -use base qw(PublicInbox::RepoBrowseBase); - -# try to be educational and show the command-line used in the signature -my @CMD = qw(format-patch -M --stdout); -my $sig = '--signature=git '.join(' ', @CMD); - -sub call_git_patch { - my ($self, $req) = @_; - my $git = $req->{repo_info}->{git}; - my $q = PublicInbox::RepoBrowseQuery->new($req->{cgi}); - my $id = $q->{id}; - $id =~ /\A[\w-]+([~\^][~\^\d])*\z/ or $id = 'HEAD'; - - # limit scope, don't take extra args to avoid wasting server - # resources buffering: - my $range = "$id~1..$id^0"; - my @cmd = (@CMD, $sig." $range", $range, '--'); - if (defined(my $expath = $req->{expath})) { - push @cmd, $expath; - } - my $fp = $git->popen(@cmd); - my ($buf, $n); - - $n = read($fp, $buf, 8192); - return unless (defined $n && $n > 0); - sub { - my ($res) = @_; # Plack callback - my $fh = $res->([200, ['Content-Type' => 'text/plain']]); - $fh->write($buf); - while (1) { - $n = read($fp, $buf, 8192); - last unless (defined $n && $n > 0); - $fh->write($buf); - } - $fh->close; - } -} - -1; diff --git a/lib/PublicInbox/RepoBrowseGitPlain.pm b/lib/PublicInbox/RepoBrowseGitPlain.pm deleted file mode 100644 index e16195dd..00000000 --- a/lib/PublicInbox/RepoBrowseGitPlain.pm +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (C) 2015-2016 all contributors -# License: AGPL-3.0+ -package PublicInbox::RepoBrowseGitPlain; -use strict; -use warnings; -use base qw(PublicInbox::RepoBrowseBase); -use PublicInbox::RepoBrowseGitBlob; -use PublicInbox::Hval qw(utf8_html); - -sub call_git_plain { - my ($self, $req) = @_; - my $git = $req->{repo_info}->{git}; - my $q = PublicInbox::RepoBrowseQuery->new($req->{cgi}); - my $id = $q->{id}; - $id eq '' and $id = 'HEAD'; - - if (length(my $expath = $req->{expath})) { - $id .= ":$expath"; - } else { - $id .= ':'; - } - my ($cat, $hex, $type, $size) = $git->cat_file_begin($id); - return unless defined $cat; - - my ($r, $buf); - my $left = $size; - if ($type eq 'blob') { - $type = git_blob_mime_type($self, $req, $cat, \$buf, \$left); - } elsif ($type eq 'commit' || $type eq 'tag') { - $type = 'text/plain'; - } elsif ($type eq 'tree') { - $git->cat_file_finish($left); - return git_tree_plain($req, $git, $hex); - } else { - $type = 'application/octet-stream'; - } - git_blob_stream_response($git, $cat, $size, $type, $buf, $left); -} - -# This should follow the cgit DOM structure in case anybody depends on it, -# not using
 here as we don't expect people to actually view it much
-sub git_tree_plain {
-	my ($req, $git, $hex) = @_;
-
-	my @ex = @{$req->{extra}};
-	my $rel = $req->{relcmd};
-	my $title = utf8_html(join('/', '', @ex, ''));
-	my $tslash = $req->{tslash};
-	my $pfx = $tslash ? './' : 'plain/';
-	my $t = "

$title

    "; - if (@ex) { - if ($tslash) { - $t .= qq(
  • ../
  • ); - } else { - $t .= qq(
  • ../
  • ); - my $last = PublicInbox::Hval->utf8($ex[-1])->as_href; - $pfx = "$last/"; - } - } - my $ls = $git->popen(qw(ls-tree --name-only -z --abbrev=12), $hex); - sub { - my ($res) = @_; - my $fh = $res->([ 200, ['Content-Type' => 'text/html']]); - $fh->write("$title". - $t); - - local $/ = "\0"; - while (defined(my $n = <$ls>)) { - chomp $n; - $n = PublicInbox::Hval->utf8($n); - my $ref = $n->as_path; - $n = $n->as_html; - - $fh->write(qq(
  • $n
  • )) - } - $fh->write('
'); - $fh->close; - } -} - -1; diff --git a/lib/PublicInbox/RepoBrowseGitTag.pm b/lib/PublicInbox/RepoBrowseGitTag.pm deleted file mode 100644 index 2019dd6f..00000000 --- a/lib/PublicInbox/RepoBrowseGitTag.pm +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright (C) 2016 all contributors -# License: AGPL-3.0+ - -# shows the /tag/ endpoint for git repositories -package PublicInbox::RepoBrowseGitTag; -use strict; -use warnings; -use base qw(PublicInbox::RepoBrowseBase); -use POSIX qw(strftime); -use PublicInbox::Hval qw(utf8_html); - -my %cmd_map = ( # type => action - commit => 'commit', - tag => 'tag', - # tree/blob fall back to 'show' -); - -sub call_git_tag { - my ($self, $req) = @_; - - my $q = PublicInbox::RepoBrowseQuery->new($req->{cgi}); - my $h = $q->{h}; - $h eq '' and return sub { - my ($res) = @_; - git_tag_list($self, $req, $res); - }; - sub { - my ($res) = @_; - git_tag_show($self, $req, $h, $res); - } -} - -sub read_err { - my ($fh, $type, $hex) = @_; - - $fh->write("

error reading $type $hex");
-}
-
-sub git_show_tag_as_tag {
-	my ($self, $fh, $req, $h, $cat, $left, $type, $hex) = @_;
-	my $buf = '';
-	my $offset = 0;
-	while ($$left > 0) {
-		my $r = read($cat, $buf, $$left, $offset);
-		unless (defined $r) {
-			read_err($fh, $type, $hex);
-			last;
-		}
-		$offset += $r;
-		$$left -= $r;
-	}
-	my $head;
-	($head, $buf) = split(/\r?\n\r?\n/, $buf, 2);
-
-	my %h = map { split(/[ \t]/, $_, 2) } split(/\r?\n/, $head);
-	my $tag = utf8_html($h{tag});
-	$type = $h{type} || '(unknown)';
-	my $obj = $h{object};
-	$h = $self->html_start($req, 'tag: ' . $tag);
-	my $label = "$type $obj";
-	my $cmd = $cmd_map{$type} || 'show';
-	my $rel = $req->{relcmd};
-	my $obj_link = qq($label);
-	$head = $h . "\n\n   tag $tag\nobject $obj_link\n";
-	if (my $tagger = $h{tagger}) {
-		$head .= 'tagger ' . join("\t", creator_split($tagger)) . "\n";
-	}
-	$fh->write($head . "\n");
-
-	# n.b. tag subjects may not have a blank line after them,
-	# but we bold the first line anyways
-	my @buf = split(/\r?\n/s, $buf);
-	if (defined(my $subj = shift @buf)) {
-		$fh->write('' . utf8_html($subj) . "\n");
-
-		$fh->write(utf8_html($_) . "\n") foreach @buf;
-	}
-}
-
-sub git_tag_show {
-	my ($self, $req, $h, $res) = @_;
-	my $git = $req->{repo_info}->{git};
-	my $fh;
-	my $hdr = ['Content-Type', 'text/html; charset=UTF-8'];
-
-	# yes, this could still theoretically show anything,
-	# but a tag could also point to anything:
-	$git->cat_file("refs/tags/$h", sub {
-		my ($cat, $left, $type, $hex) = @_;
-		$fh = $res->([200, $hdr]);
-		$h = PublicInbox::Hval->utf8($h);
-		my $m = "git_show_${type}_as_tag";
-
-		# git_show_tag_as_tag, git_show_commit_as_tag,
-		# git_show_tree_as_tag, git_show_blob_as_tag
-		if ($self->can($m)) {
-			$self->$m($fh, $req, $h, $cat, $left, $type, $hex);
-		} else {
-			$self->unknown_tag_type($fh, $req, $h, $type, $hex);
-		}
-	});
-	unless ($fh) {
-		$fh = $res->([404, $hdr]);
-		$fh->write(invalid_tag_start($req, $h));
-	}
-	$fh->write('
'); - $fh->close; -} - -sub invalid_tag_start { - my ($self, $req, $h) = @_; - my $rel = $req->{relcmd}; - $h = 'missing tag: ' . utf8_html($h); - $self->html_start($req, $h) . "\n\n\t$h\n\n" . - qq(see tag list for valid tags.); -} - -sub git_tag_list { - my ($self, $req, $res) = @_; - my $repo_info = $req->{repo_info}; - my $git = $repo_info->{git}; - my $desc = $repo_info->{desc_html}; - - # TODO: use Xapian so we can more easily handle offsets/limits - # for pagination instead of limiting - my $nr = 0; - my $count = 50; - my @cmd = (qw(for-each-ref --sort=-creatordate), - '--format=%(refname) %(creatordate:short) %(subject)', - "--count=$count", 'refs/tags/'); - my $refs = $git->popen(@cmd); - my $fh = $res->([200, ['Content-Type', 'text/html; charset=UTF-8']]); - - # tag names are unpredictable in length and requires tables :< - $fh->write($self->html_start($req, - "$repo_info->{path_info}: tag list") . - '
' . - join('', map { "" } qw(tag subject date)). - ''); - - foreach (<$refs>) { - my ($ref, $date, $s) = split(' ', $_, 3); - ++$nr; - $ref =~ s!\Arefs/tags/!!; - $ref = PublicInbox::Hval->utf8($ref); - my $h = $ref->as_html; - $ref = $ref->as_href; - $fh->write(qq("); - } - my $end = ''; - if ($nr == $count) { - $end = "
Showing the latest $nr tags
"; - } - $fh->write("
$_
$h) . - utf8_html($s) . "$date
$end"); - $fh->close; -} - -sub unknown_tag_type { - my ($self, $fh, $req, $h, $type, $hex) = @_; - my $repo_info = $req->{repo_info}; - $h = $h->as_html; - my $rel = $req->{relcmd}; - my $label = "$type $hex"; - my $cmd = $cmd_map{$type} || 'show'; - my $obj_link = qq($label\n); - - $fh->write($self->html_start($req, - "$repo_info->{path_info}: ref: $h") . - "\n\n $h (lightweight tag)\nobject $obj_link\n"); -} - -sub creator_split { - my ($tagger) = @_; - $tagger =~ s/\s*(\d+)(?:\s+([\+\-])?([ \d]{1,2})(\d\d))\z// or - return ($tagger, 0); - my ($tz_sign, $tz_H, $tz_M) = ($2, $3, $4); - my $sec = $1; - my $off = $tz_H * 3600 + $tz_M * 60; - $off *= -1 if $tz_sign eq '-'; - my @time = gmtime($sec + $off); - my $time = strftime('%Y-%m-%d %H:%M:%S', @time)." $tz_sign$tz_H$tz_M"; - - (utf8_html($tagger), $time); -} - -1; diff --git a/lib/PublicInbox/RepoBrowseGitTree.pm b/lib/PublicInbox/RepoBrowseGitTree.pm deleted file mode 100644 index 6d5d4b8c..00000000 --- a/lib/PublicInbox/RepoBrowseGitTree.pm +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright (C) 2015 all contributors -# License: AGPL-3.0+ -package PublicInbox::RepoBrowseGitTree; -use strict; -use warnings; -use base qw(PublicInbox::RepoBrowseBase); -use PublicInbox::Hval qw(utf8_html); - -my %GIT_MODE = ( - '100644' => ' ', # blob - '100755' => 'x', # executable blob - '040000' => 'd', # tree - '120000' => 'l', # symlink - '160000' => 'g', # commit (gitlink) -); - -my $BINARY_MSG = "Binary file, save using the 'plain' link above"; - -sub git_tree_stream { - my ($self, $req, $res) = @_; # res: Plack callback - my @extra = @{$req->{extra}}; - my $git = $req->{repo_info}->{git}; - my $q = PublicInbox::RepoBrowseQuery->new($req->{cgi}); - my $id = $q->{id}; - if ($id eq '') { - chomp($id = $git->qx(qw(rev-parse --short=10 HEAD))); - $q->{id} = $id; - } - - my $obj = "$id:$req->{expath}"; - my ($hex, $type, $size) = $git->check($obj); - - if (!defined($type) || ($type ne 'blob' && $type ne 'tree')) { - return $res->([404, ['Content-Type'=>'text/html'], - ['Not Found']]); - } - - my $fh = $res->([200, ['Content-Type'=>'text/html; charset=UTF-8']]); - $fh->write(''. PublicInbox::Hval::STYLE . - ''); - - if ($type eq 'tree') { - git_tree_show($req, $fh, $git, $hex, $q); - } elsif ($type eq 'blob') { - git_blob_show($req, $fh, $git, $hex, $q); - } else { - # TODO - } - $fh->write(''); - $fh->close; -} - -sub call_git_tree { - my ($self, $req) = @_; - sub { git_tree_stream($self, $req, @_) }; -} - -sub cur_path { - my ($req, $q) = @_; - my $qs = $q->qs; - my @ex = @{$req->{extra}} or return 'root'; - my $s; - - my $rel = $req->{relcmd}; - # avoid relative paths, here, we don't want to propagate - # trailing-slash URLs although we tolerate them - $s = "root/"; - my $cur = pop @ex; - my @t; - $s .= join('/', (map { - push @t, $_; - my $e = PublicInbox::Hval->utf8($_, join('/', @t)); - my $ep = $e->as_path; - my $eh = $e->as_html; - "$eh"; - } @ex), ''.utf8_html($cur).''); -} - -sub git_blob_show { - my ($req, $fh, $git, $hex, $q) = @_; - # ref: buffer_is_binary in git.git - my $to_read = 8000; # git uses this size to detect binary files - my $text_p; - my $n = 0; - - my $rel = $req->{relcmd}; - my $plain = join('/', "${rel}plain", @{$req->{extra}}); - $plain = PublicInbox::Hval->utf8($plain)->as_path . $q->qs; - my $t = cur_path($req, $q); - my $h = qq{
path: $t\n\nblob: $hex (plain)};
-	my $end = '';
-
-	$git->cat_file($hex, sub {
-		my ($cat, $left) = @_; # $$left == $size
-		$to_read = $$left if $to_read > $$left;
-		my $r = read($cat, my $buf, $to_read);
-		return unless defined($r) && $r > 0;
-		$$left -= $r;
-
-		if (index($buf, "\0") >= 0) {
-			$fh->write("$h\n$BINARY_MSG
"); - return; - } - $fh->write($h . '
');
-		$text_p = 1;
-
-		while (1) {
-			my @buf = split(/\r?\n/, $buf, -1);
-			$buf = pop @buf; # last line, careful...
-			foreach my $l (@buf) {
-				++$n;
-				$fh->write("". utf8_html($l).
-						"\n");
-			}
-			# no trailing newline:
-			if ($$left == 0 && $buf ne '') {
-				++$n;
-				$buf = utf8_html($buf);
-				$fh->write("". $buf ."");
-				$end = '
\ No newline at end of file
'; - last; - } - - last unless defined($buf); - - $to_read = $$left if $to_read > $$left; - my $off = length $buf; # last line from previous read - $r = read($cat, $buf, $to_read, $off); - return unless defined($r) && $r > 0; - $$left -= $r; - } - 0; - }); - - # line numbers go in a second column: - $fh->write('
');
-	$fh->write(qq($_\n)) foreach (1..$n);
-	$fh->write("

$end"); -} - -sub git_tree_show { - my ($req, $fh, $git, $hex, $q) = @_; - $fh->write('
');
-	my $ls = $git->popen(qw(ls-tree --abbrev=16 -l -z), $hex);
-	my $t = cur_path($req, $q);
-	my $pfx;
-	$fh->write("path: $t\n\n");
-	my $qs = $q->qs;
-
-	if ($req->{tslash}) {
-		$pfx = './';
-	} elsif (defined(my $last = $req->{extra}->[-1])) {
-		$pfx = PublicInbox::Hval->utf8($last)->as_path . '/';
-	} else {
-		$pfx = 'tree/';
-	}
-
-	local $/ = "\0";
-	$fh->write("mode\tsize\tname\n");
-	while (defined(my $l = <$ls>)) {
-		chomp $l;
-		my ($m, $t, $x, $s, $path) =
-			($l =~ /\A(\S+) (\S+) (\S+)( *\S+)\t(.+)\z/s);
-		$m = $GIT_MODE{$m} or next;
-		$path = PublicInbox::Hval->utf8($path);
-		my $ref = $path->as_path;
-		$path = $path->as_html;
-
-		if ($m eq 'g') {
-			# TODO: support cross-repository gitlinks
-			$fh->write('g' . (' ' x 18) . "$path @ $x\n");
-			next;
-		}
-		elsif ($m eq 'd') { $path = "$path/" }
-		elsif ($m eq 'x') { $path = "$path" }
-		elsif ($m eq 'l') { $path = "$path" }
-		$s =~ s/\s+//g;
-
-		# 'plain' and 'log' links intentionally omitted for brevity
-		# and speed
-		$fh->write(qq($m\t).
-			qq($s\t$path\n));
-	}
-	$fh->write('
'); -} - -1; diff --git a/lib/PublicInbox/RepoBrowseQuery.pm b/lib/PublicInbox/RepoBrowseQuery.pm deleted file mode 100644 index 861e587b..00000000 --- a/lib/PublicInbox/RepoBrowseQuery.pm +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (C) 2015 all contributors -# License: AGPL-3.0+ - -# query parameter management for repobrowse -package PublicInbox::RepoBrowseQuery; -use strict; -use warnings; -use PublicInbox::Hval; -my @KNOWN_PARAMS = qw(id id2 h showmsg ofs); - -sub new { - my ($class, $cgi) = @_; - my $self = bless {}, $class; - - foreach my $k (@KNOWN_PARAMS) { - my $v = $cgi->param($k); - $self->{$k} = defined $v ? $v : ''; - } - $self; -} - -sub qs { - my ($self, %over) = @_; - - if (keys %over) { - my $tmp = bless { %$self }, ref($self); - foreach my $k (keys %over) { $tmp->{$k} = $over{$k}; } - $self = $tmp; - } - - my @qs; - foreach my $k (@KNOWN_PARAMS) { - my $v = $self->{$k}; - - next if ($v eq ''); - $v = PublicInbox::Hval->new($v)->as_href; - push @qs, "$k=$v"; - } - scalar(@qs) ? ('?' . join('&', @qs)) : ''; -} - -1; diff --git a/lib/PublicInbox/RepoConfig.pm b/lib/PublicInbox/RepoConfig.pm deleted file mode 100644 index d55a27d2..00000000 --- a/lib/PublicInbox/RepoConfig.pm +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (C) 2015 all contributors -# License: AGPL-3.0+ -package PublicInbox::RepoConfig; -use strict; -use warnings; -use PublicInbox::Config qw/try_cat/; -require PublicInbox::Hval; - -sub new { - my ($class, $file) = @_; - $file = default_file() unless defined($file); - my $self = bless PublicInbox::Config::git_config_dump($file), $class; - $self->{-cache} = {}; - $self; -} - -sub default_file { - my $f = $ENV{PI_REPO_CONFIG}; - return $f if defined $f; - PublicInbox::Config::config_dir() . '/repo_config'; -} - -# Returns something like: -# { -# path => '/home/git/foo.git', -# description => 'foo repo', -# cloneurl => "git://example.com/foo.git\nhttp://example.com/foo.git", -# publicinbox => '/home/pub/foo-public.git', -# } -sub lookup { - my ($self, $repo_path) = @_; # "git.git" - my $rv; - - $rv = $self->{-cache}->{$repo_path} and return $rv; - - my $path = $self->{"repo.$repo_path.path"}; - (defined $path && -d $path) or return; - $rv->{path} = $path; - $rv->{path_info} = $repo_path; - - foreach my $key (qw(description cloneurl)) { - $rv->{$key} = try_cat("$path/$key"); - } - - $rv->{desc_html} = - PublicInbox::Hval->new_oneline($rv->{description})->as_html; - - foreach my $key (qw(publicinbox vcs)) { - $rv->{$key} = $self->{"repo.$repo_path.$key"}; - } - - # of course git is the default VCS - $rv->{vcs} ||= 'git'; - $self->{-cache}->{$repo_path} = $rv; -} - -1; diff --git a/lib/PublicInbox/Repobrowse.pm b/lib/PublicInbox/Repobrowse.pm new file mode 100644 index 00000000..75dee72f --- /dev/null +++ b/lib/PublicInbox/Repobrowse.pm @@ -0,0 +1,106 @@ +# Copyright (C) 2015 all contributors +# License: AGPL-3.0+ + +# Version control system (VCS) repository viewer like cgit or gitweb, +# but with optional public-inbox archive integration. +# This uses cgit-compatible PATH_INFO URLs. +# This may be expanded to support other Free Software VCSes such as +# Subversion and Mercurial, so not just git +# +# Same web design principles as PublicInbox::WWW for supporting the +# lowest common denominators (see bottom of Documentation/design_www.txt) +# +# This allows an M:N relationship between "normal" repos for project +# and public-inbox (ssoma) git repositories where N may be zero. +# In other words, repobrowse must work for repositories without +# any public-inbox at all; or with multiple public-inboxes. +# And the rest of public-inbox will always work without a "normal" +# code repo for the project. + +package PublicInbox::Repobrowse; +use strict; +use warnings; +use URI::Escape qw(uri_escape_utf8 uri_unescape); +use PublicInbox::RepobrowseConfig; + +my %CMD = map { lc($_) => $_ } qw(Log Commit Tree Patch Blob Plain Tag); +my %VCS = (git => 'Git'); +my %LOADED; + +sub new { + my ($class, $file) = @_; + bless { rconfig => PublicInbox::RepobrowseConfig->new($file) }, $class; +} + +# simple response for errors +sub r { [ $_[0], ['Content-Type' => 'text/plain'], [ join(' ', @_, "\n") ] ] } + +sub run { + my ($self, $cgi, $method) = @_; + return r(405, 'Method Not Allowed') if ($method !~ /\AGET|HEAD\z/); + + # URL syntax: / repo [ / cmd [ / path ] ] + # cmd: log | commit | diff | tree | view | blob | snapshot + # repo and path (@extra) may both contain '/' + my $rconfig = $self->{rconfig}; + my $path_info = uri_unescape($cgi->path_info); + my (undef, $repo_path, @extra) = split(m{/+}, $path_info, -1); + + return r404() unless $repo_path; + my $repo_info; + until ($repo_info = $rconfig->lookup($repo_path)) { + my $p = shift @extra or last; + $repo_path .= "/$p"; + } + return r404() unless $repo_info; + + my $req = { + repo_info => $repo_info, + extra => \@extra, # path + cgi => $cgi, + rconfig => $rconfig, + tslash => 0, + }; + + my $cmd = shift @extra; + if (defined $cmd && length $cmd) { + my $vcs_lc = $repo_info->{vcs}; + my $vcs = $VCS{$vcs_lc} or return r404(); + my $mod = $CMD{$cmd}; + unless ($mod) { + unshift @extra, $cmd; + $mod = 'Fallback'; + } + $mod = load_once("PublicInbox::Repobrowse$vcs$mod"); + $vcs = load_once("PublicInbox::$vcs"); + $repo_info->{$vcs_lc} ||= $vcs->new($repo_info->{path}); + $req->{relcmd} = '../' x scalar(@extra); + while (@extra && $extra[-1] eq '') { + pop @extra; + ++$req->{tslash}; + } + $req->{expath} = join('/', @extra); + my $rv = eval { $mod->new->call($cmd, $req) }; + $rv || r404(); + } else { + $req->{relcmd} = defined $cmd ? '' : './'; + summary($req); + } +} + +sub summary { + r404(); +} + +sub r404 { r(404, 'Not Found') } + +sub load_once { + my ($mod) = @_; + + return $mod if $LOADED{$mod}; + eval "require $mod"; + $LOADED{$mod} = 1 unless $@; + $mod; +} + +1; diff --git a/lib/PublicInbox/RepobrowseBase.pm b/lib/PublicInbox/RepobrowseBase.pm new file mode 100644 index 00000000..06381b2a --- /dev/null +++ b/lib/PublicInbox/RepobrowseBase.pm @@ -0,0 +1,66 @@ +# Copyright (C) 2015 all contributors +# License: AGPL-3.0+ +package PublicInbox::RepobrowseBase; +use strict; +use warnings; +require PublicInbox::RepobrowseQuery; +use PublicInbox::Hval; + +sub new { bless {}, shift } + +sub call { + my ($self, $cmd, $req) = @_; + my $vcs = $req->{repo_info}->{vcs}; + my $rv = eval { + no strict 'refs'; + my $sub = "call_${vcs}_$cmd"; + $self->$sub($req); + }; + $@ ? [ 500, ['Content-Type'=>'text/plain'], [] ] : $rv; +} + +sub mime_load { + my ($self, $file) = @_; + my %rv; + open my $fh, '<', $file or return \%rv; + foreach (<$fh>) { + next if /^#/; # no comments + my ($type, @ext) = split(/\s+/); + + if (defined $type) { + $rv{$_} = $type foreach @ext; + } + } + \%rv; +} + +# returns undef if missing, so users can scan the blob if needed +sub mime_type_unsafe { + my ($self, $fn) = @_; + $fn =~ /\.([^\.]+)\z/ or return; + my $ext = $1; + my $m = $self->{mime_types} ||= $self->mime_load('/etc/mime.types'); + $m->{$ext}; +} + +sub mime_type { + my ($self, $fn) = @_; + my $ct = $self->mime_type_unsafe($fn); + + # XSS protection. Assume the browser knows what to do + # with images/audio/video; but don't allow random HTML from + # a repository to be served + (defined($ct) && $ct =~ m!\A(?:image|audio|video)/!) ? $ct : undef; +} + +# starts an HTML page for Repobrowse in a consistent way +sub html_start { + my ($self, $req, $title_html) = @_; + my $desc = $req->{repo_info}->{desc_html}; + + "$title_html" . + PublicInbox::Hval::STYLE . + "
$desc";
+}
+
+1;
diff --git a/lib/PublicInbox/RepobrowseConfig.pm b/lib/PublicInbox/RepobrowseConfig.pm
new file mode 100644
index 00000000..2f780b65
--- /dev/null
+++ b/lib/PublicInbox/RepobrowseConfig.pm
@@ -0,0 +1,57 @@
+# Copyright (C) 2015 all contributors 
+# License: AGPL-3.0+ 
+package PublicInbox::RepobrowseConfig;
+use strict;
+use warnings;
+use PublicInbox::Config qw/try_cat/;
+require PublicInbox::Hval;
+
+sub new {
+	my ($class, $file) = @_;
+	$file = default_file() unless defined($file);
+	my $self = bless PublicInbox::Config::git_config_dump($file), $class;
+	$self->{-cache} = {};
+	$self;
+}
+
+sub default_file {
+	my $f = $ENV{PI_REPO_CONFIG};
+	return $f if defined $f;
+	PublicInbox::Config::config_dir() . '/repobrowse_config';
+}
+
+# Returns something like:
+# {
+#	path => '/home/git/foo.git',
+#	description => 'foo repo',
+#	cloneurl => "git://example.com/foo.git\nhttp://example.com/foo.git",
+#	publicinbox => '/home/pub/foo-public.git',
+# }
+sub lookup {
+	my ($self, $repo_path) = @_; # "git.git"
+	my $rv;
+
+	$rv = $self->{-cache}->{$repo_path} and return $rv;
+
+	my $path = $self->{"repo.$repo_path.path"};
+	(defined $path && -d $path) or return;
+	$rv->{path} = $path;
+	$rv->{path_info} = $repo_path;
+
+	foreach my $key (qw(description cloneurl)) {
+		$rv->{$key} = try_cat("$path/$key");
+	}
+
+	$rv->{desc_html} =
+		PublicInbox::Hval->new_oneline($rv->{description})->as_html;
+
+	foreach my $key (qw(publicinbox vcs)) {
+		$rv->{$key} = $self->{"repo.$repo_path.$key"};
+	}
+
+	# of course git is the default VCS
+	$rv->{vcs} ||= 'git';
+	$self->{-cache}->{$repo_path} = $rv;
+}
+
+1;
diff --git a/lib/PublicInbox/RepobrowseGit.pm b/lib/PublicInbox/RepobrowseGit.pm
new file mode 100644
index 00000000..eb79e563
--- /dev/null
+++ b/lib/PublicInbox/RepobrowseGit.pm
@@ -0,0 +1,68 @@
+# Copyright (C) 2015 all contributors 
+# License: AGPL-3.0+ (https://www.gnu.org/licenses/agpl-3.0.txt)
+
+# common functions used by other RepobrowseGit* modules
+package PublicInbox::RepobrowseGit;
+use strict;
+use warnings;
+use base qw(Exporter);
+our @EXPORT_OK = qw(git_unquote git_commit_title git_dec_links);
+use PublicInbox::Hval qw(utf8_html);
+
+my %GIT_ESC = (
+	a => "\a",
+	b => "\b",
+	f => "\f",
+	n => "\n",
+	r => "\r",
+	t => "\t",
+	v => "\013",
+);
+
+sub git_unquote {
+	my ($s) = @_;
+	return $s unless ($s =~ /\A"(.*)"\z/);
+	$s = $1;
+	$s =~ s/\\([abfnrtv])/$GIT_ESC{$1}/g;
+	$s =~ s/\\([0-7]{1,3})/chr(oct($1))/ge;
+	$s;
+}
+
+sub git_commit_title {
+	my ($git, $obj) = @_; # PublicInbox::Git, $sha1hex
+	my $rv;
+	eval {
+		my $buf = $git->cat_file($obj);
+		($rv) = ($$buf =~ /\r?\n\r?\n([^\r\n]+)\r?\n?/);
+	};
+	$rv;
+}
+
+# example inputs: "HEAD -> master", "tag: v1.0.0",
+sub git_dec_links {
+	my ($rel, $D) = @_;
+	my @l;
+	foreach (split /, /, $D) {
+		if (/\A(\S+) -> (\S+)/) { # 'HEAD -> master'
+			my ($s, $h) = ($1, $2);
+			$s = utf8_html($s);
+			$h = PublicInbox::Hval->utf8($h);
+			my $r = $h->as_href;
+			$h = $h->as_html;
+			push @l, qq($s -> $h);
+		} elsif (s/\Atag: //) {
+			my $h = PublicInbox::Hval->utf8($_);
+			my $r = $h->as_href;
+			$h = $h->as_html;
+			push @l, qq($h);
+		} else {
+			my $h = PublicInbox::Hval->utf8($_);
+			my $r = $h->as_href;
+			$h = $h->as_html;
+			push @l, qq($h);
+		}
+	}
+	@l;
+}
+
+1;
diff --git a/lib/PublicInbox/RepobrowseGitBlob.pm b/lib/PublicInbox/RepobrowseGitBlob.pm
new file mode 100644
index 00000000..273a1018
--- /dev/null
+++ b/lib/PublicInbox/RepobrowseGitBlob.pm
@@ -0,0 +1,78 @@
+# Copyright (C) 2015-2016 all contributors 
+# License: AGPL-3.0+ 
+
+# Show a blob as-is
+package PublicInbox::RepobrowseGitBlob;
+use strict;
+use warnings;
+use base qw(PublicInbox::RepobrowseBase);
+use base qw(Exporter);
+our @EXPORT = qw(git_blob_mime_type git_blob_stream_response);
+
+sub call_git_blob {
+	my ($self, $req) = @_;
+	my $git = $req->{repo_info}->{git};
+	my $q = PublicInbox::RepobrowseQuery->new($req->{cgi});
+	my $id = $q->{id};
+	$id eq '' and $id = 'HEAD';
+
+	if (length(my $expath = $req->{expath})) {
+		$id .= ":$expath";
+	}
+	my ($cat, $hex, $type, $size) = $git->cat_file_begin($id);
+	return unless defined $cat;
+
+	my ($r, $buf);
+	my $left = $size;
+	if ($type eq 'blob') {
+		$type = git_blob_mime_type($self, $req, $cat, \$buf, \$left);
+	} elsif ($type eq 'commit' || $type eq 'tag') {
+		$type = 'text/plain';
+	} else {
+		$type = 'application/octet-stream';
+	}
+	git_blob_stream_response($git, $cat, $size, $type, $buf, $left);
+}
+
+sub git_blob_mime_type {
+	my ($self, $req, $cat, $buf, $left) = @_;
+	my $base = $req->{extra}->[-1];
+	my $type = $self->mime_type($base) if defined $base;
+	return $type if $type;
+
+	my $to_read = 8000; # git uses this size to detect binary files
+	$to_read = $$left if $to_read > $$left;
+	my $r = read($cat, $$buf, $to_read);
+	if (!defined $r || $r <= 0) {
+		my $git = $req->{repo_info}->{git};
+		$git->cat_file_finish($$left);
+		return;
+	}
+	$$left -= $r;
+	(index($buf, "\0") < 0) ?  'text/plain' : 'application/octet-stream';
+}
+
+sub git_blob_stream_response {
+	my ($git, $cat, $size, $type, $buf, $left) = @_;
+
+	sub {
+		my ($res) = @_;
+		my $to_read = 8192;
+		eval {
+			my $fh = $res->([ 200, ['Content-Length' => $size,
+						'Content-Type' => $type]]);
+			$fh->write($buf) if defined $buf;
+			while ($left > 0) {
+				$to_read = $left if $to_read > $left;
+				my $r = read($cat, $buf, $to_read);
+				last if (!defined $r || $r <= 0);
+				$left -= $r;
+				$fh->write($buf);
+			}
+			$fh->close;
+		};
+		$git->cat_file_finish($left);
+	}
+}
+
+1;
diff --git a/lib/PublicInbox/RepobrowseGitCommit.pm b/lib/PublicInbox/RepobrowseGitCommit.pm
new file mode 100644
index 00000000..b3973392
--- /dev/null
+++ b/lib/PublicInbox/RepobrowseGitCommit.pm
@@ -0,0 +1,363 @@
+# Copyright (C) 2015 all contributors 
+# License: AGPL-3.0+ 
+
+# shows the /commit/ endpoint for git repositories
+package PublicInbox::RepobrowseGitCommit;
+use strict;
+use warnings;
+use base qw(PublicInbox::RepobrowseBase);
+use PublicInbox::Hval qw(utf8_html);
+use PublicInbox::RepobrowseGit qw(git_unquote git_commit_title);
+
+use constant GIT_FMT => '--pretty=format:'.join('%n',
+	'%H', '%h', '%s', '%an <%ae>', '%ai', '%cn <%ce>', '%ci',
+	'%t', '%p', '%D', '%b%x00');
+
+sub git_commit_stream {
+	my ($req, $q, $H, $log, $fh) = @_;
+	chomp(my $h = <$log>); # abbreviated commit
+	my $l;
+	chomp(my $s = utf8_html($l = <$log>)); # subject
+	chomp(my $au = utf8_html($l = <$log>)); # author
+	chomp(my $ad = <$log>);
+	chomp(my $cu = utf8_html($l = <$log>));
+	chomp(my $cd = <$log>);
+	chomp(my $t = <$log>); # tree
+	chomp(my $p = <$log>); # parents
+	my @p = split(' ', $p);
+	chomp(my $D = <$log>); # TODO: decorate
+	my $git = $req->{repo_info}->{git};
+
+	my $rel = $req->{relcmd};
+	my $qs = $q->qs(id => $h);
+	chomp $H;
+	my $x = "$s" .
+		PublicInbox::Hval::STYLE . '
' .
+		"   commit $H (patch)\n" .
+		"     tree $t";
+
+	# extra show path information, if any
+	my $extra = $req->{extra};
+	my $path = '';
+	if (@$extra) {
+		my @t;
+		my $ep;
+		$x .= ' -- ';
+		$x .= join('/', map {
+			push @t, $_;
+			my $e = PublicInbox::Hval->utf8($_, join('/', @t));
+			$ep = $e->as_path;
+			my $eh = $e->as_html;
+			"$eh";
+		} @$extra);
+		$path = "/$ep";
+	}
+
+	$x .= "\n   author $au\t$ad\ncommitter $cu\t$cd\n";
+	my $np = scalar @p;
+	if ($np == 1) {
+		my $p = $p[0];
+		$x .= git_parent_line('   parent', $p, $q, $git, $rel, $path);
+	} elsif ($np > 1) {
+		my @common = ($q, $git, $rel, $path);
+		my @t = @p;
+		my $p = shift @t;
+		$x .= git_parent_line('  parents', $p, @common);
+		foreach $p (@t) {
+			$x .= git_parent_line('         ', $p, @common);
+		}
+	}
+	$fh->write($x .= "\n$s\n\n");
+
+	# body:
+	local $/ = "\0";
+	$l = <$log>;
+	chomp $l;
+	$fh->write(utf8_html($l)."---\n");
+	git_show_diffstat($req, $h, $fh, $log);
+	my $help;
+	$help = " This is a merge, showing combined diff:\n\n" if ($np > 1);
+
+	# diff
+	local $/ = "\n";
+	my $cmt = '[a-f0-9]+';
+	my $diff = { h => $h, p => \@p, rel => $rel };
+	my $cc_add;
+	while (defined($l = <$log>)) {
+		if ($help) {
+			$fh->write($help);
+			$help = undef;
+		}
+		if ($l =~ m{^diff --git ("?a/.+) ("?b/.+)$}) { # regular
+			$l = git_diff_ab_hdr($diff, $1, $2) . "\n";
+		} elsif ($l =~ m{^diff --(cc|combined) (.+)$}) {
+			$l = git_diff_cc_hdr($diff, $1, $2) . "\n";
+		} elsif ($l =~ /^index ($cmt)\.\.($cmt)(.*)$/o) { # regular
+			$l = git_diff_ab_index($diff, $1, $2, $3) . "\n";
+		} elsif ($l =~ /^@@ (\S+) (\S+) @@(.*)$/) { # regular
+			$l = git_diff_ab_hunk($diff, $1, $2, $3) . "\n";
+		} elsif ($l =~ /^\+/ || ($cc_add && $l =~ $cc_add)) {
+			$l = git_diff_add($l) . "\n";
+		} elsif ($l =~ /^index ($cmt,[^\.]+)\.\.($cmt)(.*)$/o) { # --cc
+			$l = git_diff_cc_index($diff, $1, $2, $3) . "\n";
+			$cc_add ||= $diff->{cc_add};
+		} elsif ($l =~ /^(@@@+) (\S+.*\S+) @@@+(.*)$/) { # --cc
+			$l = git_diff_cc_hunk($diff, $1, $2, $3) . "\n";
+		} else {
+			$l = utf8_html($l);
+		}
+		$fh->write($l);
+	}
+
+	if ($help) {
+		$fh->write(" This is a merge, combined diff is empty.\n");
+	}
+	$fh->write('
'); +} + +sub call_git_commit { + my ($self, $req) = @_; + + my $q = PublicInbox::RepobrowseQuery->new($req->{cgi}); + my $id = $q->{id}; + $id eq '' and $id = 'HEAD'; + my $git = $req->{repo_info}->{git}; + my @cmd = qw(show -z --numstat -p --encoding=UTF-8 + --no-notes --no-color --abbrev=10 -c); + my @path; + + # kill trailing slash + my $extra = $req->{extra}; + if (@$extra) { + pop @$extra if $extra->[-1] eq ''; + @path = (join('/', @$extra)); + push @cmd, '--follow'; + } + + my $log = $git->popen(@cmd, GIT_FMT, $id, '--', @path); + my $H = <$log>; + + # maybe the path didn't exist, yet, zip them back up + return git_commit_404($req, $q, $path[0]) unless defined $H; + sub { + my ($res) = @_; # Plack callback + my $fh = $res->([200, ['Content-Type'=>'text/html']]); + git_commit_stream($req, $q, $H, $log, $fh); + $fh->close; + } +} + +sub git_commit_404 { + my ($req, $q, $path) = @_; + my $x = 'Missing commit or path'; + my $pfx = "$req->{relcmd}commit"; + + # print STDERR "path: $path\n"; + my $try = 'try'; + $x = "$x
$x\n\n";
+	if (defined $path) {
+		my $qs = $q->qs;
+		$x .= "" .
+			"try without the path $path\n";
+		$try = 'or';
+	}
+	my $qs = $q->qs(id => '');
+	$x .= "$try the latest commit in HEAD\n";
+	$x .= '
'; + + [ 404, ['Content-Type'=>'text/html'], [ $x ] ]; +} + +sub git_show_diffstat { + my ($req, $h, $fh, $log) = @_; + local $/ = "\0\0"; + my $l = <$log>; + chomp $l; + my @stat = split("\0", $l); + my $nr = 0; + my ($nadd, $ndel) = (0, 0); + my $rel = $req->{relcmd}; + while (defined($l = shift @stat)) { + $l =~ s/\n?(\S+)\t+(\S+)\t+// or next; + my ($add, $del) = ($1, $2); + if ($add =~ /\A\d+\z/) { + $nadd += $add; + $ndel += $del; + $add = "+$add"; + $del = "-$del"; + } + my $num = sprintf('% 6s/%-6s', $del, $add); + if (length $l) { + $l = PublicInbox::Hval->utf8($l); + my $lp = $l->as_path; + my $lh = $l->as_html; + $l = "$lh"; + + } else { + my $from = shift @stat; + my $to = shift @stat; + $l = git_diffstat_rename($rel, $h, $from, $to); + } + ++$nr; + $fh->write(' '.$num."\t".$l."\n"); + } + $l = "\n $nr "; + $l .= $nr == 1 ? 'file changed, ' : 'files changed, '; + $l .= $nadd; + $l .= $nadd == 1 ? ' insertion(+), ' : ' insertions(+), '; + $l .= $ndel; + $l .= $ndel == 1 ? " deletion(-)\n\n" : " deletions(-)\n\n"; + $fh->write($l); +} + +# index abcdef89..01234567 +sub git_diff_ab_index { + my ($diff, $xa, $xb, $end) = @_; + # not wasting bandwidth on links here, yet + # links in hunk headers are far more useful with line offsets + $end = utf8_html($end); + "index $xa..$xb$end"; +} + +# diff --git a/foo.c b/bar.c +sub git_diff_ab_hdr { + my ($diff, $fa, $fb) = @_; + my $html_a = utf8_html($fa); + my $html_b = utf8_html($fb); + $fa = git_unquote($fa); + $fb = git_unquote($fb); + $fa =~ s!\Aa/!!; + $fb =~ s!\Ab/!!; + $fa = $diff->{fa} = PublicInbox::Hval->utf8($fa); + $fb = $diff->{fb} = PublicInbox::Hval->utf8($fb); + $diff->{path_a} = $fa->as_path; + $diff->{path_b} = $fb->as_path; + + # not wasting bandwidth on links here, yet + # links in hunk headers are far more useful with line offsets + "diff --git $html_a $html_b"; +} + +# @@ -1,2 +3,4 @@ (regular diff) +sub git_diff_ab_hunk { + my ($diff, $ca, $cb, $ctx) = @_; + my ($na) = ($ca =~ /\A-(\d+)/); + my ($nb) = ($cb =~ /\A\+(\d+)/); + + my $rel = $diff->{rel}; + my $rv = '@@ '; + if ($na == 0) { # new file + $rv .= $ca; + } else { + my $p = $diff->{p}->[0]; + $rv .= "{path_a}?id=$p#n$na\">"; + $rv .= "$ca"; + } + $rv .= ' '; + if ($nb == 0) { # deleted file + $rv .= $cb; + } else { + my $h = $diff->{h}; + $rv .= "{path_b}?id=$h#n$nb\">"; + $rv .= "$cb"; + } + $rv . ' @@' . utf8_html($ctx); +} + +sub git_diff_cc_hdr { + my ($diff, $combined, $path) = @_; + my $html_path = utf8_html($path); + my $cc = $diff->{cc} = PublicInbox::Hval->utf8(git_unquote($path)); + $diff->{path_cc} = $cc->as_path; + "diff --$combined $html_path"; +} + +# index abcdef09,01234567..76543210 +sub git_diff_cc_index { + my ($diff, $before, $last, $end) = @_; + $end = utf8_html($end); + my @before = split(',', $before); + $diff->{pobj_cc} = \@before; + $diff->{cc_add} ||= eval { + my $n = scalar(@before) - 1; + qr/^ {0,$n}[\+]/; + }; + + # not wasting bandwidth on links here, yet + # links in hunk headers are far more useful with line offsets + "index $before..$last$end"; +} + +# @@@ -1,2 -3,4 +5,6 @@@ (combined diff) +sub git_diff_cc_hunk { + my ($diff, $at, $offs, $ctx) = @_; + my @offs = split(' ', $offs); + my $last = pop @offs; + my @p = @{$diff->{p}}; + my @pobj = @{$diff->{pobj_cc}}; + my $path = $diff->{path_cc}; + my $rel = $diff->{rel}; + my $rv = $at; + + # special 'cc' action as we don't have reliable paths from parents + my $ppath = "${rel}cc/$path"; + foreach my $off (@offs) { + my $p = shift @p; + my $obj = shift @pobj; # blob SHA-1 + my ($n) = ($off =~ /\A-(\d+)/); # line number + + if ($n == 0) { # new file (does this happen with --cc?) + $rv .= " $off"; + } else { + $rv .= " "; + $rv .= "$off"; + } + } + + # we can use the normal 'tree' endpoint for the result + my ($n) = ($last =~ /\A\+(\d+)/); # line number + if ($n == 0) { # deleted file (does this happen with --cc?) + $rv .= " $last"; + } else { + my $h = $diff->{h}; + $rv .= " "; + $rv .= "$last"; + } + $rv .= " $at" . utf8_html($ctx); +} + +sub git_diffstat_rename { + my ($rel, $h, $from, $to) = @_; + my @from = split('/', $from); + my @to = split('/', $to); + my $orig_to = $to; + my ($base, @base); + while (@to && @from && $to[0] eq $from[0]) { + push @base, shift(@to); + shift @from; + } + + $base = utf8_html(join('/', @base)) if @base; + $from = utf8_html(join('/', @from)); + $to = PublicInbox::Hval->utf8(join('/', @to), $orig_to); + my $tp = $to->as_path; + my $th = $to->as_html; + $to = "$th"; + @base ? "$base/{$from => $to}" : "$from => $to"; +} + +sub git_diff_add { + my ($l) = @_; + chomp $l; + ''.utf8_html($l).''; +} + +sub git_parent_line { + my ($pfx, $p, $q, $git, $rel, $path) = @_; + my $qs = $q->qs(id => $p); + my $t = git_commit_title($git, $p); + $t = defined $t ? utf8_html($t) : ''; + $pfx . " $p ". $t . "\n"; +} + +1; diff --git a/lib/PublicInbox/RepobrowseGitFallback.pm b/lib/PublicInbox/RepobrowseGitFallback.pm new file mode 100644 index 00000000..79cc2fe4 --- /dev/null +++ b/lib/PublicInbox/RepobrowseGitFallback.pm @@ -0,0 +1,87 @@ +# Copyright (C) 2015 all contributors +# License: AGPL-3.0+ (https://www.gnu.org/licenses/agpl-3.0.txt) + +# when no endpoints match, fallback to this and serve a static file +# This can serve Smart HTTP in the future. +package PublicInbox::RepobrowseGitFallback; +use strict; +use warnings; +use base qw(PublicInbox::RepobrowseBase); +use Fcntl qw(:seek); + +# overrides PublicInbox::RepobrowseBase::call +sub call { + my ($self, undef, $req) = @_; + my $expath = $req->{expath}; + return if index($expath, '..') >= 0; # prevent path traversal + + my $git = $req->{repo_info}->{git}; + my $f = "$git->{git_dir}/$expath"; + return unless -f $f && -r _; + my @st = stat(_); + my ($size, $mtime) = ($st[7], $st[9]); + # TODO: if-modified-since and last-modified... + open my $in, '<', $f or return; + my $code = 200; + my $len = $size; + my @h; + + # FIXME: this is Plack-only + my $range = eval { $req->{cgi}->{env}->{HTTP_RANGE} }; + if (defined $range && $range =~ /\bbytes=(\d*)-(\d*)\z/) { + ($code, $len) = prepare_range($req, $in, \@h, $1, $2, $size); + } + + # we use the unsafe variant since we assume the server admin + # would not place untrusted HTML/JS/CSS in the git directory + my $type = $self->mime_type_unsafe($expath) || 'text/plain'; + push @h, 'Content-Type', $type, 'Content-Length', $len; + sub { + my ($res) = @_; # Plack callback + my $fh = $res->([ $code, \@h ]); + my $buf; + my $n = 8192; + while ($size > 0) { + $n = $size if $size < $n; + my $r = read($in, $buf, $n); + last if (!defined($r) || $r <= 0); + $fh->write($buf); + } + $fh->close; + } +} + +sub bad_range { [ 416, [], [] ] } + +sub prepare_range { + my ($req, $in, $h, $beg, $end, $size) = @_; + my $code = 200; + my $len = $size; + if ($beg eq '') { + if ($end ne '') { # last N bytes + $beg = $size - $end; + $beg = 0 if $beg < 0; + $end = $size - 1; + $code = 206; + } + } else { + if ($end eq '' || $end >= $size) { + $end = $size - 1; + $code = 206; + } elsif ($end < $size) { + $code = 206; + } + } + if ($code == 206) { + $len = $end - $beg + 1; + seek($in, $beg, SEEK_SET) or return [ 500, [], [] ]; + push @$h, qw(Accept-Ranges bytes), + 'Content-Range', "bytes $beg-$end/$size"; + + # FIXME: Plack::Middleware::Deflater bug? + $req->{cgi}->{env}->{'psgix.no-compress'} = 1; + } + ($code, $len); +} + +1; diff --git a/lib/PublicInbox/RepobrowseGitLog.pm b/lib/PublicInbox/RepobrowseGitLog.pm new file mode 100644 index 00000000..5d2c855c --- /dev/null +++ b/lib/PublicInbox/RepobrowseGitLog.pm @@ -0,0 +1,118 @@ +# Copyright (C) 2015 all contributors +# License: AGPL-3.0+ + +package PublicInbox::RepobrowseGitLog; +use strict; +use warnings; +use PublicInbox::Hval qw(utf8_html); +use base qw(PublicInbox::RepobrowseBase); +use PublicInbox::RepobrowseGit qw(git_dec_links git_commit_title); +# cannot rely on --date=format-local:... yet, it is too new (September 2015) +my $LOG_FMT = '--pretty=tformat:'. + join('%x00', qw(%h %p %s D%D)); +my $MSG_FMT = join('%x00', '', qw(%ai a%an b%b)); + +sub call_git_log { + my ($self, $req) = @_; + my $repo_info = $req->{repo_info}; + my $max = $repo_info->{max_commit_count} || 50; + $max = int($max); + $max = 50 if $max == 0; + + my $q = PublicInbox::RepobrowseQuery->new($req->{cgi}); + my $h = $q->{h}; + $h eq '' and $h = 'HEAD'; + + my $fmt = $LOG_FMT; + $fmt .= $MSG_FMT if $q->{showmsg}; + $fmt .= '%x00%x00'; + + my $git = $repo_info->{git}; + my $log = $git->popen(qw(log --no-notes --no-color + --abbrev-commit --abbrev=12), + $fmt, "-$max", $h); + sub { + my ($res) = @_; # Plack callback + my $fh = $res->([200, ['Content-Type'=>'text/html']]); + git_log_stream($req, $q, $log, $fh, $git); + $fh->close; + } +} + +sub git_log_stream { + my ($req, $q, $log, $fh, $git) = @_; + my $desc = $req->{repo_info}->{desc_html}; + my $showmsg = $q->{showmsg}; + + my $x = 'commit log '; + if ($showmsg) { + $showmsg = "&showmsg=1"; + my $qs = $q->qs(showmsg => ''); + $qs = $req->{cgi}->path_info if ($qs eq ''); + $x .= qq{[oneline|expand]}; + } else { + my $qs = $q->qs(showmsg => 1); + $x .= qq{[oneline|expand]}; + } + + my $rel = $req->{relcmd}; + $fh->write('' . PublicInbox::Hval::STYLE . + "$desc
$desc\n\n".
+		qq!commit\t\t$x\n!);
+	$fh->write($showmsg ? '
' : "\n"); + my %acache; + local $/ = "\0\0\n"; + my $nr = 0; + my (@parents, %seen); + while (defined(my $line = <$log>)) { + my ($id, $p, $s, $D, $ai, $an, $b) = split("\0", $line); + $seen{$id} = 1; + my @p = split(' ', $p); + push @parents, @p; + + $s = utf8_html($s); + $s = qq($s); + if ($D =~ /\AD(.+)/) { + $s .= ' ('. join(', ', git_dec_links($rel, $1)) . ')'; + } + + if (defined $b) { + $an =~ s/\Aa//; + $b =~ s/\Ab//; + $b =~ s/\s*\z//s; + + my $ah = $acache{$an} ||= utf8_html($an); + my $x = "
$id";
+			my $nl = $b eq '' ? '' : "\n"; # empty bodies :<
+			$b = $x . '  
' .
+				"$s\n- $ah @ $ai\n$nl" .
+				utf8_html($b) . '
'; + } else { + $b = qq($id\t$s\n); + } + $fh->write($b); + ++$nr; + } + + my $m = ''; + my $np = 0; + foreach my $p (@parents) { + next if $seen{$p}; + $seen{$p} = ++$np; + my $s = git_commit_title($git, $p); + $m .= qq(\n$p\t); + $s = defined($s) ? utf8_html($s) : ''; + $m .= qq($s); + } + my $foot = $showmsg ? "
\t\t$x\n\n" : "\n\t\t$x\n\n";
+	if ($np == 0) {
+		$foot .= "No commits follow";
+	} elsif ($np > 1) {
+		$foot .= "Unseen parent commits to follow (multiple choice):\n";
+	} else {
+		$foot .= "Next parent to follow:\n";
+	}
+	$fh->write($foot .= $m . '
'); +} + +1; diff --git a/lib/PublicInbox/RepobrowseGitPatch.pm b/lib/PublicInbox/RepobrowseGitPatch.pm new file mode 100644 index 00000000..7f03864e --- /dev/null +++ b/lib/PublicInbox/RepobrowseGitPatch.pm @@ -0,0 +1,47 @@ +# Copyright (C) 2015 all contributors +# License: AGPL-3.0+ + +# shows the /patch/ endpoint for git repositories +# usage: /repo.git/patch?id=COMMIT_ID +package PublicInbox::RepobrowseGitPatch; +use strict; +use warnings; +use base qw(PublicInbox::RepobrowseBase); + +# try to be educational and show the command-line used in the signature +my @CMD = qw(format-patch -M --stdout); +my $sig = '--signature=git '.join(' ', @CMD); + +sub call_git_patch { + my ($self, $req) = @_; + my $git = $req->{repo_info}->{git}; + my $q = PublicInbox::RepobrowseQuery->new($req->{cgi}); + my $id = $q->{id}; + $id =~ /\A[\w-]+([~\^][~\^\d])*\z/ or $id = 'HEAD'; + + # limit scope, don't take extra args to avoid wasting server + # resources buffering: + my $range = "$id~1..$id^0"; + my @cmd = (@CMD, $sig." $range", $range, '--'); + if (defined(my $expath = $req->{expath})) { + push @cmd, $expath; + } + my $fp = $git->popen(@cmd); + my ($buf, $n); + + $n = read($fp, $buf, 8192); + return unless (defined $n && $n > 0); + sub { + my ($res) = @_; # Plack callback + my $fh = $res->([200, ['Content-Type' => 'text/plain']]); + $fh->write($buf); + while (1) { + $n = read($fp, $buf, 8192); + last unless (defined $n && $n > 0); + $fh->write($buf); + } + $fh->close; + } +} + +1; diff --git a/lib/PublicInbox/RepobrowseGitPlain.pm b/lib/PublicInbox/RepobrowseGitPlain.pm new file mode 100644 index 00000000..ebb4f397 --- /dev/null +++ b/lib/PublicInbox/RepobrowseGitPlain.pm @@ -0,0 +1,81 @@ +# Copyright (C) 2015-2016 all contributors +# License: AGPL-3.0+ +package PublicInbox::RepobrowseGitPlain; +use strict; +use warnings; +use base qw(PublicInbox::RepobrowseBase); +use PublicInbox::RepobrowseGitBlob; +use PublicInbox::Hval qw(utf8_html); + +sub call_git_plain { + my ($self, $req) = @_; + my $git = $req->{repo_info}->{git}; + my $q = PublicInbox::RepobrowseQuery->new($req->{cgi}); + my $id = $q->{id}; + $id eq '' and $id = 'HEAD'; + + if (length(my $expath = $req->{expath})) { + $id .= ":$expath"; + } else { + $id .= ':'; + } + my ($cat, $hex, $type, $size) = $git->cat_file_begin($id); + return unless defined $cat; + + my ($r, $buf); + my $left = $size; + if ($type eq 'blob') { + $type = git_blob_mime_type($self, $req, $cat, \$buf, \$left); + } elsif ($type eq 'commit' || $type eq 'tag') { + $type = 'text/plain'; + } elsif ($type eq 'tree') { + $git->cat_file_finish($left); + return git_tree_plain($req, $git, $hex); + } else { + $type = 'application/octet-stream'; + } + git_blob_stream_response($git, $cat, $size, $type, $buf, $left); +} + +# This should follow the cgit DOM structure in case anybody depends on it, +# not using
 here as we don't expect people to actually view it much
+sub git_tree_plain {
+	my ($req, $git, $hex) = @_;
+
+	my @ex = @{$req->{extra}};
+	my $rel = $req->{relcmd};
+	my $title = utf8_html(join('/', '', @ex, ''));
+	my $tslash = $req->{tslash};
+	my $pfx = $tslash ? './' : 'plain/';
+	my $t = "

$title

    "; + if (@ex) { + if ($tslash) { + $t .= qq(
  • ../
  • ); + } else { + $t .= qq(
  • ../
  • ); + my $last = PublicInbox::Hval->utf8($ex[-1])->as_href; + $pfx = "$last/"; + } + } + my $ls = $git->popen(qw(ls-tree --name-only -z --abbrev=12), $hex); + sub { + my ($res) = @_; + my $fh = $res->([ 200, ['Content-Type' => 'text/html']]); + $fh->write("$title". + $t); + + local $/ = "\0"; + while (defined(my $n = <$ls>)) { + chomp $n; + $n = PublicInbox::Hval->utf8($n); + my $ref = $n->as_path; + $n = $n->as_html; + + $fh->write(qq(
  • $n
  • )) + } + $fh->write('
'); + $fh->close; + } +} + +1; diff --git a/lib/PublicInbox/RepobrowseGitTag.pm b/lib/PublicInbox/RepobrowseGitTag.pm new file mode 100644 index 00000000..13af0a34 --- /dev/null +++ b/lib/PublicInbox/RepobrowseGitTag.pm @@ -0,0 +1,187 @@ +# Copyright (C) 2016 all contributors +# License: AGPL-3.0+ + +# shows the /tag/ endpoint for git repositories +package PublicInbox::RepobrowseGitTag; +use strict; +use warnings; +use base qw(PublicInbox::RepobrowseBase); +use POSIX qw(strftime); +use PublicInbox::Hval qw(utf8_html); + +my %cmd_map = ( # type => action + commit => 'commit', + tag => 'tag', + # tree/blob fall back to 'show' +); + +sub call_git_tag { + my ($self, $req) = @_; + + my $q = PublicInbox::RepobrowseQuery->new($req->{cgi}); + my $h = $q->{h}; + $h eq '' and return sub { + my ($res) = @_; + git_tag_list($self, $req, $res); + }; + sub { + my ($res) = @_; + git_tag_show($self, $req, $h, $res); + } +} + +sub read_err { + my ($fh, $type, $hex) = @_; + + $fh->write("

error reading $type $hex");
+}
+
+sub git_show_tag_as_tag {
+	my ($self, $fh, $req, $h, $cat, $left, $type, $hex) = @_;
+	my $buf = '';
+	my $offset = 0;
+	while ($$left > 0) {
+		my $r = read($cat, $buf, $$left, $offset);
+		unless (defined $r) {
+			read_err($fh, $type, $hex);
+			last;
+		}
+		$offset += $r;
+		$$left -= $r;
+	}
+	my $head;
+	($head, $buf) = split(/\r?\n\r?\n/, $buf, 2);
+
+	my %h = map { split(/[ \t]/, $_, 2) } split(/\r?\n/, $head);
+	my $tag = utf8_html($h{tag});
+	$type = $h{type} || '(unknown)';
+	my $obj = $h{object};
+	$h = $self->html_start($req, 'tag: ' . $tag);
+	my $label = "$type $obj";
+	my $cmd = $cmd_map{$type} || 'show';
+	my $rel = $req->{relcmd};
+	my $obj_link = qq($label);
+	$head = $h . "\n\n   tag $tag\nobject $obj_link\n";
+	if (my $tagger = $h{tagger}) {
+		$head .= 'tagger ' . join("\t", creator_split($tagger)) . "\n";
+	}
+	$fh->write($head . "\n");
+
+	# n.b. tag subjects may not have a blank line after them,
+	# but we bold the first line anyways
+	my @buf = split(/\r?\n/s, $buf);
+	if (defined(my $subj = shift @buf)) {
+		$fh->write('' . utf8_html($subj) . "\n");
+
+		$fh->write(utf8_html($_) . "\n") foreach @buf;
+	}
+}
+
+sub git_tag_show {
+	my ($self, $req, $h, $res) = @_;
+	my $git = $req->{repo_info}->{git};
+	my $fh;
+	my $hdr = ['Content-Type', 'text/html; charset=UTF-8'];
+
+	# yes, this could still theoretically show anything,
+	# but a tag could also point to anything:
+	$git->cat_file("refs/tags/$h", sub {
+		my ($cat, $left, $type, $hex) = @_;
+		$fh = $res->([200, $hdr]);
+		$h = PublicInbox::Hval->utf8($h);
+		my $m = "git_show_${type}_as_tag";
+
+		# git_show_tag_as_tag, git_show_commit_as_tag,
+		# git_show_tree_as_tag, git_show_blob_as_tag
+		if ($self->can($m)) {
+			$self->$m($fh, $req, $h, $cat, $left, $type, $hex);
+		} else {
+			$self->unknown_tag_type($fh, $req, $h, $type, $hex);
+		}
+	});
+	unless ($fh) {
+		$fh = $res->([404, $hdr]);
+		$fh->write(invalid_tag_start($req, $h));
+	}
+	$fh->write('
'); + $fh->close; +} + +sub invalid_tag_start { + my ($self, $req, $h) = @_; + my $rel = $req->{relcmd}; + $h = 'missing tag: ' . utf8_html($h); + $self->html_start($req, $h) . "\n\n\t$h\n\n" . + qq(see tag list for valid tags.); +} + +sub git_tag_list { + my ($self, $req, $res) = @_; + my $repo_info = $req->{repo_info}; + my $git = $repo_info->{git}; + my $desc = $repo_info->{desc_html}; + + # TODO: use Xapian so we can more easily handle offsets/limits + # for pagination instead of limiting + my $nr = 0; + my $count = 50; + my @cmd = (qw(for-each-ref --sort=-creatordate), + '--format=%(refname) %(creatordate:short) %(subject)', + "--count=$count", 'refs/tags/'); + my $refs = $git->popen(@cmd); + my $fh = $res->([200, ['Content-Type', 'text/html; charset=UTF-8']]); + + # tag names are unpredictable in length and requires tables :< + $fh->write($self->html_start($req, + "$repo_info->{path_info}: tag list") . + '
' . + join('', map { "" } qw(tag subject date)). + ''); + + foreach (<$refs>) { + my ($ref, $date, $s) = split(' ', $_, 3); + ++$nr; + $ref =~ s!\Arefs/tags/!!; + $ref = PublicInbox::Hval->utf8($ref); + my $h = $ref->as_html; + $ref = $ref->as_href; + $fh->write(qq("); + } + my $end = ''; + if ($nr == $count) { + $end = "
Showing the latest $nr tags
"; + } + $fh->write("
$_
$h) . + utf8_html($s) . "$date
$end"); + $fh->close; +} + +sub unknown_tag_type { + my ($self, $fh, $req, $h, $type, $hex) = @_; + my $repo_info = $req->{repo_info}; + $h = $h->as_html; + my $rel = $req->{relcmd}; + my $label = "$type $hex"; + my $cmd = $cmd_map{$type} || 'show'; + my $obj_link = qq($label\n); + + $fh->write($self->html_start($req, + "$repo_info->{path_info}: ref: $h") . + "\n\n $h (lightweight tag)\nobject $obj_link\n"); +} + +sub creator_split { + my ($tagger) = @_; + $tagger =~ s/\s*(\d+)(?:\s+([\+\-])?([ \d]{1,2})(\d\d))\z// or + return ($tagger, 0); + my ($tz_sign, $tz_H, $tz_M) = ($2, $3, $4); + my $sec = $1; + my $off = $tz_H * 3600 + $tz_M * 60; + $off *= -1 if $tz_sign eq '-'; + my @time = gmtime($sec + $off); + my $time = strftime('%Y-%m-%d %H:%M:%S', @time)." $tz_sign$tz_H$tz_M"; + + (utf8_html($tagger), $time); +} + +1; diff --git a/lib/PublicInbox/RepobrowseGitTree.pm b/lib/PublicInbox/RepobrowseGitTree.pm new file mode 100644 index 00000000..13ded634 --- /dev/null +++ b/lib/PublicInbox/RepobrowseGitTree.pm @@ -0,0 +1,187 @@ +# Copyright (C) 2015 all contributors +# License: AGPL-3.0+ +package PublicInbox::RepobrowseGitTree; +use strict; +use warnings; +use base qw(PublicInbox::RepobrowseBase); +use PublicInbox::Hval qw(utf8_html); + +my %GIT_MODE = ( + '100644' => ' ', # blob + '100755' => 'x', # executable blob + '040000' => 'd', # tree + '120000' => 'l', # symlink + '160000' => 'g', # commit (gitlink) +); + +my $BINARY_MSG = "Binary file, save using the 'plain' link above"; + +sub git_tree_stream { + my ($self, $req, $res) = @_; # res: Plack callback + my @extra = @{$req->{extra}}; + my $git = $req->{repo_info}->{git}; + my $q = PublicInbox::RepobrowseQuery->new($req->{cgi}); + my $id = $q->{id}; + if ($id eq '') { + chomp($id = $git->qx(qw(rev-parse --short=10 HEAD))); + $q->{id} = $id; + } + + my $obj = "$id:$req->{expath}"; + my ($hex, $type, $size) = $git->check($obj); + + if (!defined($type) || ($type ne 'blob' && $type ne 'tree')) { + return $res->([404, ['Content-Type'=>'text/html'], + ['Not Found']]); + } + + my $fh = $res->([200, ['Content-Type'=>'text/html; charset=UTF-8']]); + $fh->write(''. PublicInbox::Hval::STYLE . + ''); + + if ($type eq 'tree') { + git_tree_show($req, $fh, $git, $hex, $q); + } elsif ($type eq 'blob') { + git_blob_show($req, $fh, $git, $hex, $q); + } else { + # TODO + } + $fh->write(''); + $fh->close; +} + +sub call_git_tree { + my ($self, $req) = @_; + sub { git_tree_stream($self, $req, @_) }; +} + +sub cur_path { + my ($req, $q) = @_; + my $qs = $q->qs; + my @ex = @{$req->{extra}} or return 'root'; + my $s; + + my $rel = $req->{relcmd}; + # avoid relative paths, here, we don't want to propagate + # trailing-slash URLs although we tolerate them + $s = "root/"; + my $cur = pop @ex; + my @t; + $s .= join('/', (map { + push @t, $_; + my $e = PublicInbox::Hval->utf8($_, join('/', @t)); + my $ep = $e->as_path; + my $eh = $e->as_html; + "$eh"; + } @ex), ''.utf8_html($cur).''); +} + +sub git_blob_show { + my ($req, $fh, $git, $hex, $q) = @_; + # ref: buffer_is_binary in git.git + my $to_read = 8000; # git uses this size to detect binary files + my $text_p; + my $n = 0; + + my $rel = $req->{relcmd}; + my $plain = join('/', "${rel}plain", @{$req->{extra}}); + $plain = PublicInbox::Hval->utf8($plain)->as_path . $q->qs; + my $t = cur_path($req, $q); + my $h = qq{
path: $t\n\nblob: $hex (plain)};
+	my $end = '';
+
+	$git->cat_file($hex, sub {
+		my ($cat, $left) = @_; # $$left == $size
+		$to_read = $$left if $to_read > $$left;
+		my $r = read($cat, my $buf, $to_read);
+		return unless defined($r) && $r > 0;
+		$$left -= $r;
+
+		if (index($buf, "\0") >= 0) {
+			$fh->write("$h\n$BINARY_MSG
"); + return; + } + $fh->write($h . '
');
+		$text_p = 1;
+
+		while (1) {
+			my @buf = split(/\r?\n/, $buf, -1);
+			$buf = pop @buf; # last line, careful...
+			foreach my $l (@buf) {
+				++$n;
+				$fh->write("". utf8_html($l).
+						"\n");
+			}
+			# no trailing newline:
+			if ($$left == 0 && $buf ne '') {
+				++$n;
+				$buf = utf8_html($buf);
+				$fh->write("". $buf ."");
+				$end = '
\ No newline at end of file
'; + last; + } + + last unless defined($buf); + + $to_read = $$left if $to_read > $$left; + my $off = length $buf; # last line from previous read + $r = read($cat, $buf, $to_read, $off); + return unless defined($r) && $r > 0; + $$left -= $r; + } + 0; + }); + + # line numbers go in a second column: + $fh->write('
');
+	$fh->write(qq($_\n)) foreach (1..$n);
+	$fh->write("

$end"); +} + +sub git_tree_show { + my ($req, $fh, $git, $hex, $q) = @_; + $fh->write('
');
+	my $ls = $git->popen(qw(ls-tree --abbrev=16 -l -z), $hex);
+	my $t = cur_path($req, $q);
+	my $pfx;
+	$fh->write("path: $t\n\n");
+	my $qs = $q->qs;
+
+	if ($req->{tslash}) {
+		$pfx = './';
+	} elsif (defined(my $last = $req->{extra}->[-1])) {
+		$pfx = PublicInbox::Hval->utf8($last)->as_path . '/';
+	} else {
+		$pfx = 'tree/';
+	}
+
+	local $/ = "\0";
+	$fh->write("mode\tsize\tname\n");
+	while (defined(my $l = <$ls>)) {
+		chomp $l;
+		my ($m, $t, $x, $s, $path) =
+			($l =~ /\A(\S+) (\S+) (\S+)( *\S+)\t(.+)\z/s);
+		$m = $GIT_MODE{$m} or next;
+		$path = PublicInbox::Hval->utf8($path);
+		my $ref = $path->as_path;
+		$path = $path->as_html;
+
+		if ($m eq 'g') {
+			# TODO: support cross-repository gitlinks
+			$fh->write('g' . (' ' x 18) . "$path @ $x\n");
+			next;
+		}
+		elsif ($m eq 'd') { $path = "$path/" }
+		elsif ($m eq 'x') { $path = "$path" }
+		elsif ($m eq 'l') { $path = "$path" }
+		$s =~ s/\s+//g;
+
+		# 'plain' and 'log' links intentionally omitted for brevity
+		# and speed
+		$fh->write(qq($m\t).
+			qq($s\t$path\n));
+	}
+	$fh->write('
'); +} + +1; diff --git a/lib/PublicInbox/RepobrowseQuery.pm b/lib/PublicInbox/RepobrowseQuery.pm new file mode 100644 index 00000000..e9c5153f --- /dev/null +++ b/lib/PublicInbox/RepobrowseQuery.pm @@ -0,0 +1,42 @@ +# Copyright (C) 2015 all contributors +# License: AGPL-3.0+ + +# query parameter management for repobrowse +package PublicInbox::RepobrowseQuery; +use strict; +use warnings; +use PublicInbox::Hval; +my @KNOWN_PARAMS = qw(id id2 h showmsg ofs); + +sub new { + my ($class, $cgi) = @_; + my $self = bless {}, $class; + + foreach my $k (@KNOWN_PARAMS) { + my $v = $cgi->param($k); + $self->{$k} = defined $v ? $v : ''; + } + $self; +} + +sub qs { + my ($self, %over) = @_; + + if (keys %over) { + my $tmp = bless { %$self }, ref($self); + foreach my $k (keys %over) { $tmp->{$k} = $over{$k}; } + $self = $tmp; + } + + my @qs; + foreach my $k (@KNOWN_PARAMS) { + my $v = $self->{$k}; + + next if ($v eq ''); + $v = PublicInbox::Hval->new($v)->as_href; + push @qs, "$k=$v"; + } + scalar(@qs) ? ('?' . join('&', @qs)) : ''; +} + +1; -- cgit v1.2.3-24-ge0c7