From d9563ea5516e8e786debf223e10ec11695aee9d7 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 9 Feb 2017 01:37:03 +0000 Subject: repobrowse: shorten internal names We'll still be keeping "repobrowse" for the public API for use with .psgi files, but shortening the name means less typing and we may have command-line tools, too. --- MANIFEST | 36 ++-- lib/PublicInbox/RepoBase.pm | 115 +++++++++++ lib/PublicInbox/RepoConfig.pm | 88 +++++++++ lib/PublicInbox/RepoGit.pm | 68 +++++++ lib/PublicInbox/RepoGitAtom.pm | 169 ++++++++++++++++ lib/PublicInbox/RepoGitBlob.pm | 77 ++++++++ lib/PublicInbox/RepoGitCommit.pm | 201 +++++++++++++++++++ lib/PublicInbox/RepoGitDiff.pm | 73 +++++++ lib/PublicInbox/RepoGitDiffCommon.pm | 297 +++++++++++++++++++++++++++++ lib/PublicInbox/RepoGitFallback.pm | 21 ++ lib/PublicInbox/RepoGitLog.pm | 152 +++++++++++++++ lib/PublicInbox/RepoGitPatch.pm | 39 ++++ lib/PublicInbox/RepoGitPlain.pm | 100 ++++++++++ lib/PublicInbox/RepoGitQuery.pm | 50 +++++ lib/PublicInbox/RepoGitSnapshot.pm | 110 +++++++++++ lib/PublicInbox/RepoGitSummary.pm | 109 +++++++++++ lib/PublicInbox/RepoGitTag.pm | 213 +++++++++++++++++++++ lib/PublicInbox/RepoGitTree.pm | 220 +++++++++++++++++++++ lib/PublicInbox/RepoRoot.pm | 71 +++++++ lib/PublicInbox/Repobrowse.pm | 14 +- lib/PublicInbox/RepobrowseBase.pm | 115 ----------- lib/PublicInbox/RepobrowseConfig.pm | 88 --------- lib/PublicInbox/RepobrowseGit.pm | 68 ------- lib/PublicInbox/RepobrowseGitAtom.pm | 169 ---------------- lib/PublicInbox/RepobrowseGitBlob.pm | 77 -------- lib/PublicInbox/RepobrowseGitCommit.pm | 201 ------------------- lib/PublicInbox/RepobrowseGitDiff.pm | 73 ------- lib/PublicInbox/RepobrowseGitDiffCommon.pm | 297 ----------------------------- lib/PublicInbox/RepobrowseGitFallback.pm | 21 -- lib/PublicInbox/RepobrowseGitLog.pm | 152 --------------- lib/PublicInbox/RepobrowseGitPatch.pm | 39 ---- lib/PublicInbox/RepobrowseGitPlain.pm | 100 ---------- lib/PublicInbox/RepobrowseGitQuery.pm | 50 ----- lib/PublicInbox/RepobrowseGitSnapshot.pm | 110 ----------- lib/PublicInbox/RepobrowseGitSummary.pm | 109 ----------- lib/PublicInbox/RepobrowseGitTag.pm | 213 --------------------- lib/PublicInbox/RepobrowseGitTree.pm | 220 --------------------- lib/PublicInbox/RepobrowseRoot.pm | 71 ------- t/repobrowse_git.t | 2 +- 39 files changed, 2199 insertions(+), 2199 deletions(-) create mode 100644 lib/PublicInbox/RepoBase.pm create mode 100644 lib/PublicInbox/RepoConfig.pm create mode 100644 lib/PublicInbox/RepoGit.pm create mode 100644 lib/PublicInbox/RepoGitAtom.pm create mode 100644 lib/PublicInbox/RepoGitBlob.pm create mode 100644 lib/PublicInbox/RepoGitCommit.pm create mode 100644 lib/PublicInbox/RepoGitDiff.pm create mode 100644 lib/PublicInbox/RepoGitDiffCommon.pm create mode 100644 lib/PublicInbox/RepoGitFallback.pm create mode 100644 lib/PublicInbox/RepoGitLog.pm create mode 100644 lib/PublicInbox/RepoGitPatch.pm create mode 100644 lib/PublicInbox/RepoGitPlain.pm create mode 100644 lib/PublicInbox/RepoGitQuery.pm create mode 100644 lib/PublicInbox/RepoGitSnapshot.pm create mode 100644 lib/PublicInbox/RepoGitSummary.pm create mode 100644 lib/PublicInbox/RepoGitTag.pm create mode 100644 lib/PublicInbox/RepoGitTree.pm create mode 100644 lib/PublicInbox/RepoRoot.pm delete mode 100644 lib/PublicInbox/RepobrowseBase.pm delete mode 100644 lib/PublicInbox/RepobrowseConfig.pm delete mode 100644 lib/PublicInbox/RepobrowseGit.pm delete mode 100644 lib/PublicInbox/RepobrowseGitAtom.pm delete mode 100644 lib/PublicInbox/RepobrowseGitBlob.pm delete mode 100644 lib/PublicInbox/RepobrowseGitCommit.pm delete mode 100644 lib/PublicInbox/RepobrowseGitDiff.pm delete mode 100644 lib/PublicInbox/RepobrowseGitDiffCommon.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/RepobrowseGitQuery.pm delete mode 100644 lib/PublicInbox/RepobrowseGitSnapshot.pm delete mode 100644 lib/PublicInbox/RepobrowseGitSummary.pm delete mode 100644 lib/PublicInbox/RepobrowseGitTag.pm delete mode 100644 lib/PublicInbox/RepobrowseGitTree.pm delete mode 100644 lib/PublicInbox/RepobrowseRoot.pm diff --git a/MANIFEST b/MANIFEST index d0223c65..a59d9dcc 100644 --- a/MANIFEST +++ b/MANIFEST @@ -80,27 +80,27 @@ lib/PublicInbox/NewsWWW.pm lib/PublicInbox/ParentPipe.pm lib/PublicInbox/ProcessPipe.pm lib/PublicInbox/Qspawn.pm +lib/PublicInbox/RepoBase.pm +lib/PublicInbox/RepoConfig.pm +lib/PublicInbox/RepoGit.pm +lib/PublicInbox/RepoGitAtom.pm +lib/PublicInbox/RepoGitBlob.pm +lib/PublicInbox/RepoGitCommit.pm +lib/PublicInbox/RepoGitDiff.pm +lib/PublicInbox/RepoGitDiffCommon.pm +lib/PublicInbox/RepoGitFallback.pm +lib/PublicInbox/RepoGitLog.pm +lib/PublicInbox/RepoGitPatch.pm +lib/PublicInbox/RepoGitPlain.pm +lib/PublicInbox/RepoGitQuery.pm lib/PublicInbox/RepoGitSearch.pm lib/PublicInbox/RepoGitSearchIdx.pm +lib/PublicInbox/RepoGitSnapshot.pm +lib/PublicInbox/RepoGitSummary.pm +lib/PublicInbox/RepoGitTag.pm +lib/PublicInbox/RepoGitTree.pm +lib/PublicInbox/RepoRoot.pm lib/PublicInbox/Repobrowse.pm -lib/PublicInbox/RepobrowseBase.pm -lib/PublicInbox/RepobrowseConfig.pm -lib/PublicInbox/RepobrowseGit.pm -lib/PublicInbox/RepobrowseGitAtom.pm -lib/PublicInbox/RepobrowseGitBlob.pm -lib/PublicInbox/RepobrowseGitCommit.pm -lib/PublicInbox/RepobrowseGitDiff.pm -lib/PublicInbox/RepobrowseGitDiffCommon.pm -lib/PublicInbox/RepobrowseGitFallback.pm -lib/PublicInbox/RepobrowseGitLog.pm -lib/PublicInbox/RepobrowseGitPatch.pm -lib/PublicInbox/RepobrowseGitPlain.pm -lib/PublicInbox/RepobrowseGitQuery.pm -lib/PublicInbox/RepobrowseGitSnapshot.pm -lib/PublicInbox/RepobrowseGitSummary.pm -lib/PublicInbox/RepobrowseGitTag.pm -lib/PublicInbox/RepobrowseGitTree.pm -lib/PublicInbox/RepobrowseRoot.pm lib/PublicInbox/SaPlugin/ListMirror.pm lib/PublicInbox/Search.pm lib/PublicInbox/SearchIdx.pm diff --git a/lib/PublicInbox/RepoBase.pm b/lib/PublicInbox/RepoBase.pm new file mode 100644 index 00000000..e60677d6 --- /dev/null +++ b/lib/PublicInbox/RepoBase.pm @@ -0,0 +1,115 @@ +# Copyright (C) 2015 all contributors +# License: AGPL-3.0+ +package PublicInbox::RepoBase; +use strict; +use warnings; +require PublicInbox::RepoGitQuery; +use PublicInbox::Hval; +our %MIME_TYPE_WHITELIST = ('application/pdf' => 1); + +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; + while (<$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); + return unless defined $ct; + + # 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 + ($ct =~ m!\A(?:image|audio|video)/! || $MIME_TYPE_WHITELIST{$ct}) ? + $ct : undef; +} + +# starts an HTML page for Repobrowse in a consistent way +sub html_start { + my ($self, $req, $title_html, $opts) = @_; + my $desc = $req->{repo_info}->{desc_html}; + my $meta = ''; + + if ($opts) { + my @robots; + foreach (qw(nofollow noindex)) { + push @robots, $_ if $opts->{$_}; + } + $meta = qq('; + } + + "$title_html" . + PublicInbox::Hval::STYLE . $meta . + "
$desc";
+}
+
+sub r {
+	my ($self, $status, $req, @extra) = @_;
+	my @h;
+
+	my $body = '';
+	if ($status == 301 || $status == 302) {
+		# The goal is to be able to make redirects like we make
+		#  tags with '../'
+		my $env = $req->{env};
+		my $base = PublicInbox::Repobrowse::base_url($env);
+		my ($redir) = @extra;
+		if ($redir =~ m!\A\.\./!) { # relative redirect
+			my @orig = split(m!/+!, $env->{PATH_INFO});
+			my @dest = split(m!/+!, $redir);
+
+			while ($dest[0] eq '..') {
+				pop @orig;
+				shift @dest;
+			}
+			my $end = '';
+			$end = pop @dest if $dest[-1] =~ /\A[#\?]/;
+			$redir = $base . join('/', @orig, @dest) . $end;
+		} else {
+			$redir = $base . '/' . $redir;
+		}
+		push @h, qw(Content-Type text/plain Location), $redir;
+
+		# mainly for curl (no-'-L') users:
+		$body = "Redirecting to $redir\n";
+	} else {
+		push @h, qw(Content-Type text/plain);
+	}
+
+	[ $status, \@h, [ $body ] ]
+}
+
+1;
diff --git a/lib/PublicInbox/RepoConfig.pm b/lib/PublicInbox/RepoConfig.pm
new file mode 100644
index 00000000..32346852
--- /dev/null
+++ b/lib/PublicInbox/RepoConfig.pm
@@ -0,0 +1,88 @@
+# Copyright (C) 2015 all contributors 
+# License: AGPL-3.0+ 
+package PublicInbox::RepoConfig;
+use strict;
+use warnings;
+use PublicInbox::Inbox;
+use PublicInbox::Config;
+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} = {};
+
+	# hard disable these with '-' prefix by default:
+	$self->{'repobrowse.snapshots'} ||= '-tar.bz2 -tar.xz';
+
+	# for root
+	$self->{-groups} = { -hidden => [], -none => [] };
+	$self;
+}
+
+sub default_file {
+	my $f = $ENV{REPOBROWSE_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->{repo} = $repo_path;
+
+	# snapshots:
+	my $snap = (split('/', $repo_path))[-1];
+	$snap =~ s/\.git\z//; # seems common for git URLs to end in ".git"
+	$rv->{snapshot_re} = qr/\A\Q$snap\E[-_]/;
+	$rv->{snapshot_pfx} = $snap;
+
+	# gitweb compatibility
+	foreach my $key (qw(description cloneurl)) {
+		$rv->{$key} = PublicInbox::Inbox::try_cat("$path/$key");
+	}
+
+	$rv->{desc_html} =
+		PublicInbox::Hval->new_oneline($rv->{description})->as_html;
+
+	foreach my $key (qw(publicinbox vcs readme group snapshots)) {
+		$rv->{$key} = $self->{"repo.$repo_path.$key"};
+	}
+	unless (defined $rv->{snapshots}) {
+		$rv->{snapshots} = $self->{'repobrowse.snapshots'} || '';
+	}
+
+	my %disabled;
+	foreach (split(/\s+/, $rv->{snapshots})) {
+		s/\A-// and $disabled{$_} = 1;
+	}
+	$rv->{snapshots_disabled} = \%disabled;
+
+	my $g = $rv->{group};
+	defined $g or $g = '-none';
+	if (ref($g) eq 'ARRAY') {
+		push @{$self->{-groups}->{$_} ||= []}, $repo_path foreach @$g;
+	} else {
+		push @{$self->{-groups}->{$g} ||= []}, $repo_path;
+	}
+
+	# of course git is the default VCS
+	$rv->{vcs} ||= 'git';
+	$self->{-cache}->{$repo_path} = $rv;
+}
+
+1;
diff --git a/lib/PublicInbox/RepoGit.pm b/lib/PublicInbox/RepoGit.pm
new file mode 100644
index 00000000..f7bf0b9d
--- /dev/null
+++ b/lib/PublicInbox/RepoGit.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 RepoGit* modules
+package PublicInbox::RepoGit;
+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/RepoGitAtom.pm b/lib/PublicInbox/RepoGitAtom.pm
new file mode 100644
index 00000000..a9f40126
--- /dev/null
+++ b/lib/PublicInbox/RepoGitAtom.pm
@@ -0,0 +1,169 @@
+# Copyright (C) 2016 all contributors 
+# License: AGPL-3.0+ 
+
+# show log as an Atom feed
+package PublicInbox::RepoGitAtom;
+use strict;
+use warnings;
+use PublicInbox::Hval qw(utf8_html);
+use base qw(PublicInbox::RepoBase);
+use PublicInbox::Qspawn;
+
+use constant DATEFMT => '%Y-%m-%dT%H:%M:%SZ';
+use constant STATES => qw(H ct an ae at s b);
+use constant STATE_BODY => (scalar(STATES) - 1);
+my $ATOM_FMT = '--pretty=tformat:'.
+		join('%n', map { "%$_" } STATES).'%x00';
+use POSIX qw(strftime);
+
+sub repo_root_url {
+	my ($self, $req) = @_;
+	my $env = $req->{env};
+	my $uri = $env->{REQUEST_URI};
+	$uri =~ s/\?.+\z//; # no query string
+	my @uri = split(m!/+!, $uri);
+	my @extra = @{$req->{extra}};
+	while (@uri && @extra && $uri[-1] eq $extra[-1]) {
+		pop @uri;
+		pop @extra;
+	}
+	pop @uri if $uri[-1] eq 'atom'; # warn if not equal?
+	PublicInbox::Repobrowse::base_url($env) . join('/', @uri);
+}
+
+sub flush_hdr ($$$) {
+	my ($dst, $hdr, $url) = @_;
+	$$dst .= '';
+	$$dst .= utf8_html($hdr->{'s'}); # commit subject
+	$$dst .= '';
+	$$dst .= strftime(DATEFMT, gmtime($hdr->{ct}));
+	$$dst .= '';
+	$$dst .= utf8_html($hdr->{an});
+	$$dst .= '';
+	$$dst .= utf8_html($hdr->{ae});
+	$$dst .= '';
+	$$dst .= strftime(DATEFMT, gmtime($hdr->{at}));
+	$$dst .= '';
+	$$dst .= qq();
+	$$dst .= $H;
+	$$dst .= qq();
+
+	$$dst .= qq();
+	$$dst .= qq();
+	undef
+}
+
+sub git_atom_sed ($$) {
+	my ($self, $req) = @_;
+	my $buf = '';
+	my $state = 0;
+	my $rel = $req->{relcmd};
+	my $repo_info = $req->{repo_info};
+	my $title = join('/', $repo_info->{repo}, @{$req->{extra}});
+	$title = utf8_html("$title, branch $req->{q}->{h}");
+	my $url = repo_root_url($self, $req);
+	my $hdr = {};
+	$req->{axml} = qq(\n) .
+		qq() .
+		qq($title) .
+		qq($repo_info->{desc_html}) .
+		qq();
+	my ($plinks, $id, $ai);
+	my $end = '';
+	my $blines;
+	sub {
+		my $dst;
+		# $_[0] == scalar buffer, undef means EOF from "git log"
+		$dst = delete $req->{axml} || '';
+		my @tmp;
+		if (defined $_[0]) {
+			$buf .= $_[0];
+			@tmp = split(/\n/, $buf, -1);
+			$buf = @tmp ? pop(@tmp) : '';
+		} else {
+			@tmp = split(/\n/, $buf, -1);
+			$buf = '';
+			$end = '';
+		}
+
+		foreach my $l (@tmp) {
+			if ($state != STATE_BODY) {
+				$hdr->{((STATES)[$state])} = $l;
+				if (++$state == STATE_BODY) {
+					flush_hdr(\$dst, $hdr, $url);
+					$hdr = {};
+					$blines = 0;
+				}
+				next;
+			}
+			if ($l eq "\0") {
+				$dst .= qq(
); + $state = 0; + } else { + $dst .= "\n" if $blines++; + $dst .= utf8_html($l); + } + } + $dst .= $end; + } +} + +sub git_atom_cb { + my ($self, $req) = @_; + sub { + my ($r) = @_; + my $env = $req->{env}; + if (!defined $r) { + my $git = $req->{repo_info}->{git}; + return [ 400, [ 'Content-Type', 'text/plain' ], + [ $git->err ] ]; + } + $env->{'qspawn.filter'} = git_atom_sed($self, $req); + [ 200, [ 'Content-Type', 'application/atom+xml' ] ]; + } +} + +sub call_git_atom { + my ($self, $req) = @_; + my $repo_info = $req->{repo_info}; + my $max = $repo_info->{max_commit_count} || 10; + $max = int($max); + $max = 50 if $max == 0; + + my $git = $repo_info->{git}; + my $env = $req->{env}; + my $q =$req->{'q'} = PublicInbox::RepoGitQuery->new($env); + my $h = $q->{h}; + my $read_log = sub { + my $cmd = $git->cmd(qw(log --no-notes --no-color + --abbrev-commit), $git->abbrev, + $ATOM_FMT, "-$max", $h, '--'); + my $expath = $req->{expath}; + push @$cmd, $expath if $expath ne ''; + my $rdr = { 2 => $git->err_begin }; + my $qsp = PublicInbox::Qspawn->new($cmd, undef, undef, $rdr); + $qsp->psgi_return($env, undef, git_atom_cb($self, $req)); + }; + + sub { + $env->{'qspawn.response'} = $_[0]; + return $read_log->() if $h ne ''; + + my $cmd = $git->cmd(qw(symbolic-ref --short HEAD)); + my $rdr = { 2 => $git->err_begin }; + my $qsp = PublicInbox::Qspawn->new($cmd, undef, undef, $rdr); + $qsp->psgi_qx($env, undef, sub { + chomp($h = ${$_[0]}); + $read_log->(); + }) + } +} + +1; diff --git a/lib/PublicInbox/RepoGitBlob.pm b/lib/PublicInbox/RepoGitBlob.pm new file mode 100644 index 00000000..586b4acc --- /dev/null +++ b/lib/PublicInbox/RepoGitBlob.pm @@ -0,0 +1,77 @@ +# Copyright (C) 2015-2016 all contributors +# License: AGPL-3.0+ + +# Show a blob as-is +package PublicInbox::RepoGitBlob; +use strict; +use warnings; +use base qw(PublicInbox::RepoBase); +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::RepoGitQuery->new($req->{env}); + my $id = $q->{id}; + $id eq '' and $id = 'HEAD'; + $id .= ":$req->{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; charset=UTF-8'; + } 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; charset=UTF-8' + : '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/RepoGitCommit.pm b/lib/PublicInbox/RepoGitCommit.pm new file mode 100644 index 00000000..e98c3c18 --- /dev/null +++ b/lib/PublicInbox/RepoGitCommit.pm @@ -0,0 +1,201 @@ +# Copyright (C) 2015 all contributors +# License: AGPL-3.0+ + +# shows the /commit/ endpoint for git repositories +# +# anchors used: +# D - diffstat +# P - parents +# ...and various filenames from to_attr +# The 'D' and 'P' anchors may conflict with odd filenames, but we won't +# punish the common case with extra bytes if somebody uses 'D' or 'P' +# in filenames. + +package PublicInbox::RepoGitCommit; +use strict; +use warnings; +use base qw(PublicInbox::RepoBase); +use PublicInbox::Hval qw(utf8_html to_attr); +use PublicInbox::RepoGit qw(git_unquote git_commit_title); +use PublicInbox::RepoGitDiffCommon; +use PublicInbox::Qspawn; + +use constant GIT_FMT => '--pretty=format:'.join('%n', + '%H', '%h', '%s', '%an <%ae>', '%ai', '%cn <%ce>', '%ci', + '%t', '%p', '%D', '%b%x00'); + +use constant CC_EMPTY => " This is a merge, and the combined diff is empty.\n"; +use constant CC_MERGE => " This is a merge, showing combined diff:\n\n"; + +sub commit_header { + my ($self, $req) = @_; + my ($H, $h, $s, $au, $ad, $cu, $cd, $t, $p, $D, $rest) = + split("\n", $req->{dbuf}, 11); + $s = utf8_html($s); + $au = utf8_html($au); + $cu = utf8_html($cu); + my @p = split(' ', $p); + + my $rel = $req->{relcmd}; + my $q = $req->{'q'}; + my $qs = $req->{qs} = $q->qs(id => $h); + my $x = $self->html_start($req, $s) . "\n" . + qq( commit $H (patch)\n) . + qq( tree $t); + + my $git = $req->{repo_info}->{git}; + # 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; + $ep = "${rel}tree/$ep?id=$h"; + qq($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) { + $req->{mhelp} = CC_MERGE; + 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); + } + } + $x .= "\n"; + $x .= $s; + $x .= "\n\n"; + my $bx00; + + # FIXME: deal with excessively long commit message bodies + ($bx00, $req->{dbuf}) = split("\0", $rest, 2); + $req->{anchors} = {}; + $req->{h} = $h; + $req->{p} = \@p; + $x .= utf8_html($bx00) . "---\n"; +} + +sub git_commit_sed ($$) { + my ($self, $req) = @_; + git_diff_sed_init($req); + my $dbuf = \($req->{dbuf}); + + # this filters for $fh->write or $body->getline (see Qspawn) + sub { + my $dst = ''; + if (defined $_[0]) { # $_[0] == scalar buffer + $$dbuf .= $_[0]; + if ($req->{dstate} == DSTATE_INIT) { + return $dst if index($$dbuf, "\0") < 0; + $req->{dstate} = DSTATE_STAT; + $dst .= commit_header($self, $req); + } + git_diff_sed_run(\$dst, $req); + } else { # undef means EOF from "git show", flush the last bit + git_diff_sed_close(\$dst, $req); + $dst .= CC_EMPTY if delete $req->{mhelp}; + show_unchanged(\$dst, $req); + $dst .= ''; + } + $dst; + } +} + +sub call_git_commit { # RepoBase calls this + my ($self, $req) = @_; + my $env = $req->{env}; + my $q = PublicInbox::RepoGitQuery->new($env); + my $id = $q->{id}; + $id eq '' and $id = 'HEAD'; + + my $expath = $req->{expath}; + if ($expath ne '') { + my $relup = join('', map { '../' } @{$req->{extra}}); + my $qs = $q->qs; + return $self->r(301, $req, "$relup$qs#".to_attr($expath)); + } + + my $git = $req->{repo_info}->{git}; + my $cmd = $git->cmd(qw(show -z --numstat -p --encoding=UTF-8 + --no-notes --no-color -c), + $git->abbrev, GIT_FMT, $id, '--'); + my $rdr = { 2 => $git->err_begin }; + my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); + $req->{'q'} = $q; + $env->{'qspawn.quiet'} = 1; + $qsp->psgi_return($env, undef, sub { # parse header + my ($r, $bref) = @_; + if (!defined $r) { + my $errmsg = $git->err; + [ 500, [ 'Content-Type', 'text/html' ], [ $errmsg ] ]; + } elsif ($r == 0) { + git_commit_404($req); + } else { + $env->{'qspawn.filter'} = git_commit_sed($self, $req); + [ 200, [ 'Content-Type', 'text/html' ] ]; + } + }); +} + +sub git_commit_404 { + my ($req) = @_; + my $x = 'Missing commit or path'; + my $pfx = "$req->{relcmd}commit"; + + my $try = 'try'; + $x = "$x
$x\n\n";
+	my $qs = $req->{'q'}->qs(id => '');
+	$x .= "$try the latest commit in HEAD\n";
+	$x .= '
'; + + [404, ['Content-Type'=>'text/html'], [ $x ]]; +} + +# FIXME: horrifically expensive... +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 . qq( $p $t\n); +} + +# do not break anchor links if the combined diff doesn't show changes: +sub show_unchanged { + my ($dst, $req) = @_; + + my @unchanged = sort keys %{$req->{anchors}}; + return unless @unchanged; + my $anchors = $req->{anchors}; + $$dst .= "\n There are uninteresting changes from this merge.\n" . + qq( See the parents, ) . + "or view final state(s) below:\n\n"; + my $rel = $req->{relcmd}; + my $qs = $req->{qs}; + foreach my $anchor (@unchanged) { + my $fn = $anchors->{$anchor}; + my $p = PublicInbox::Hval->utf8(git_unquote($fn)); + $p = $p->as_path; + $fn = utf8_html($fn); + $$dst .= qq(\t); + $$dst .= "$fn\n"; + } +} + +1; diff --git a/lib/PublicInbox/RepoGitDiff.pm b/lib/PublicInbox/RepoGitDiff.pm new file mode 100644 index 00000000..bb71e738 --- /dev/null +++ b/lib/PublicInbox/RepoGitDiff.pm @@ -0,0 +1,73 @@ +# Copyright (C) 2016 all contributors +# License: AGPL-3.0+ + +# shows the /diff endpoint for git repositories for cgit compatibility +# usage: /repo.git/diff?id=COMMIT_ID&id2=COMMIT_ID2 +# +# We probably will not link to this outright because it's expensive, +# but exists to preserve URL compatibility with cgit. +package PublicInbox::RepoGitDiff; +use strict; +use warnings; +use base qw(PublicInbox::RepoBase); +use PublicInbox::Hval qw(utf8_html); +use PublicInbox::RepoGitDiffCommon; +use PublicInbox::Qspawn; + +sub git_diff_sed ($$) { + my ($self, $req) = @_; + git_diff_sed_init($req); + $req->{dstate} = DSTATE_STAT; + # this filters for $fh->write or $body->getline (see Qspawn) + sub { + my $dst = delete $req->{dhtml} || ''; + if (defined $_[0]) { # $_[0] == scalar buffer + $req->{dbuf} .= $_[0]; + git_diff_sed_run(\$dst, $req); + } else { # undef means EOF from "git show", flush the last bit + git_diff_sed_close(\$dst, $req); + $dst .= ''; + } + $dst; + } +} + +sub call_git_diff { + my ($self, $req) = @_; + my $env = $req->{env}; + my $q = PublicInbox::RepoGitQuery->new($env); + my $id = $q->{id}; + my $id2 = $q->{id2}; + + my $git = $req->{repo_info}->{git}; + my $cmd = $git->cmd(qw(diff-tree -z --numstat -p --encoding=UTF-8 + --no-color -M -B -D -r), $id2, $id, '--'); + my $expath = $req->{expath}; + push @$cmd, $expath if $expath ne ''; + my $o = { nofollow => 1, noindex => 1 }; + my $ex = $expath eq '' ? '' : " $expath"; + $req->{dhtml} = $self->html_start($req, 'diff', $o). "\n\n". + utf8_html("git diff-tree -r -M -B -D ". + "$id2 $id --$ex"). "\n\n"; + $req->{p} = [ $id2 ]; + $req->{h} = $id; + my $rdr = { 2 => $git->err_begin }; + my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); + # $env->{'qspawn.quiet'} = 1; + $qsp->psgi_return($env, undef, sub { # parse header + my ($r) = @_; + if (!defined $r) { + [ 500, [ 'Content-Type', 'text/html' ], [ $git->err ]]; + } elsif ($r == 0) { + [ 200, [ 'Content-Type', 'text/html' ], [ + delete($req->{dhtml}). + 'No differences' ] + ] + } else { + $env->{'qspawn.filter'} = git_diff_sed($self, $req); + [ 200, [ 'Content-Type', 'text/html' ] ]; + } + }); +} + +1; diff --git a/lib/PublicInbox/RepoGitDiffCommon.pm b/lib/PublicInbox/RepoGitDiffCommon.pm new file mode 100644 index 00000000..0604f9dd --- /dev/null +++ b/lib/PublicInbox/RepoGitDiffCommon.pm @@ -0,0 +1,297 @@ +# Copyright (C) 2016 all contributors +# License: AGPL-3.0+ + +# common git diff-related code +package PublicInbox::RepoGitDiffCommon; +use strict; +use warnings; +use PublicInbox::RepoGit qw/git_unquote git_commit_title/; +use PublicInbox::Hval qw/utf8_html to_attr/; +use base qw/Exporter/; +our @EXPORT = qw/git_diff_sed_init git_diff_sed_close git_diff_sed_run + DSTATE_INIT DSTATE_STAT DSTATE_LINES/; + +# index abcdef89..01234567 +sub git_diff_ab_index ($$$) { + my ($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 ($req, $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/!!; + my $anchor = to_attr($fb); + delete $req->{anchors}->{$anchor}; + $fa = $req->{fa} = PublicInbox::Hval->utf8($fa); + $fb = $req->{fb} = PublicInbox::Hval->utf8($fb); + $req->{path_a} = $fa->as_path; + $req->{path_b} = $fb->as_path; + + # not wasting bandwidth on links here + # links in hunk headers are far more useful with line offsets + qq(diff --git $html_a $html_b); +} + +# diff (--cc|--combined) +sub git_diff_cc_hdr { + my ($req, $combined, $path) = @_; + my $html_path = utf8_html($path); + $path = git_unquote($path); + my $anchor = to_attr($path); + delete $req->{anchors}->{$anchor}; + my $cc = $req->{cc} = PublicInbox::Hval->utf8($path); + $req->{path_cc} = $cc->as_path; + qq(diff --$combined $html_path); +} + +# @@ -1,2 +3,4 @@ (regular diff) +sub git_diff_ab_hunk ($$$$) { + my ($req, $ca, $cb, $ctx) = @_; + my ($na) = ($ca =~ /\A-(\d+)/); + my ($nb) = ($cb =~ /\A\+(\d+)/); + + # we add "rel=nofollow" here to reduce load on search engines, here + my $rel = $req->{relcmd}; + my $rv = '@@ '; + if (defined($na) && $na == 0) { # new file + $rv .= $ca; + } else { + $na = defined $na ? "#n$na" : ''; + my $p = $req->{p}->[0]; + $rv .= qq({path_a}?id=$p$na">); + $rv .= "$ca"; + } + $rv .= ' '; + if (defined($nb) && $nb == 0) { # deleted file + $rv .= $cb; + } else { + my $h = $req->{h}; + $nb = defined $nb ? "#n$nb" : ''; + $rv .= qq({path_b}?id=$h$nb">); + $rv .= "$cb"; + } + $rv . ' @@' . utf8_html($ctx); +} + +# index abcdef09,01234567..76543210 +sub git_diff_cc_index { + my ($req, $before, $last, $end) = @_; + $end = utf8_html($end); + my @before = split(',', $before); + $req->{pobj_cc} = \@before; + + # 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 ($req, $at, $offs, $ctx) = @_; + my @offs = split(' ', $offs); + my $last = pop @offs; + my @p = @{$req->{p}}; + my @pobj = @{$req->{pobj_cc}}; + my $path = $req->{path_cc}; + my $rel = $req->{relcmd}; + 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 = $req->{h}; + $rv .= qq( $last); + } + $rv .= " $at" . utf8_html($ctx); +} + +sub git_diffstat_rename ($$$) { + my ($req, $from, $to) = @_; + my $anchor = to_attr(git_unquote($to)); + $req->{anchors}->{$anchor} = $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 = qq($th); + @base ? "$base/{$from => $to}" : "$from => $to"; +} + +sub DSTATE_INIT () { 0 } +sub DSTATE_STAT () { 1 } +sub DSTATE_LINES () { 2 } + +sub git_diff_sed_init ($) { + my ($req) = @_; + $req->{dbuf} = ''; + $req->{ndiff} = $req->{nchg} = $req->{nadd} = $req->{ndel} = 0; + $req->{dstate} = DSTATE_INIT; +} + +sub git_diff_sed_stat ($$) { + my ($dst, $req) = @_; + my @stat = split(/\0/, $req->{dbuf}, -1); + my $eos; + my $nchg = \($req->{nchg}); + my $nadd = \($req->{nadd}); + my $ndel = \($req->{ndel}); + if (!$req->{dstat_started}) { + $req->{dstat_started} = 1; + + # merges start with an extra '\0' before the diffstat + # non-merge commits start with an extra '\n', instead + if ($req->{mhelp}) { + if ($stat[0] eq '') { + shift @stat; + } else { + warn +'initial merge diffstat line was not empty'; + } + } else { + # for commits, only (not diff-tree) + $stat[0] =~ s/\A\n//s; + } + } + while (defined(my $l = shift @stat)) { + if ($l eq '') { + $eos = 1 if $stat[0] && $stat[0] =~ /\Ad/; # "diff --" + last; + } elsif ($l =~ /\Adiff /) { + unshift @stat, $l; + $eos = 1; + last; + } + $l =~ /\A(\S+)\t+(\S+)\t+(.*)/ or next; + my ($add, $del, $fn) = ($1, $2, $3); + if ($fn ne '') { # normal modification + # TODO: discard diffs if they are too big + # gigantic changes with many files may still OOM us + my $anchor = to_attr(git_unquote($fn)); + $req->{anchors}->{$anchor} = $fn; + $l = utf8_html($fn); + $l = qq($l); + } else { # rename + # incomplete... + if (scalar(@stat) < 2) { + unshift @stat, $l; + last; + } + my $from = shift @stat; + my $to = shift @stat; + $l = git_diffstat_rename($req, $from, $to); + } + + # text changes show numerically, Binary does not + if ($add =~ /\A\d+\z/) { + $$nadd += $add; + $$ndel += $del; + $add = "+$add"; + $del = "-$del"; + } + ++$$nchg; + my $num = sprintf('% 6s/%-6s', $del, $add); + $$dst .= " $num\t$l\n"; + } + + $req->{dbuf} = join("\0", @stat); + return unless $eos; + + $req->{dstate} = DSTATE_LINES; + $$dst .= "\n $$nchg "; + $$dst .= $$nchg == 1 ? 'file changed, ' : 'files changed, '; + $$dst .= $$nadd; + $$dst .= $$nadd == 1 ? ' insertion(+), ' : ' insertions(+), '; + $$dst .= $$ndel; + $$dst .= $$ndel == 1 ? " deletion(-)\n\n" : " deletions(-)\n\n"; +} + +sub git_diff_sed_lines ($$) { + my ($dst, $req) = @_; + + # TODO: discard diffs if they are too big + + my @dlines = split(/\n/, $req->{dbuf}, -1); + $req->{dbuf} = ''; + + if (my $help = delete $req->{mhelp}) { + $$dst .= $help; # CC_MERGE + } + + # don't touch the last line, it may not be terminated + $req->{dbuf} .= pop @dlines; + + my $ndiff = \($req->{ndiff}); + my $cmt = '[a-f0-9]+'; + while (defined(my $l = shift @dlines)) { + if ($l =~ m{\Adiff --git ("?a/.+) ("?b/.+)\z}) { # regular + $$dst .= git_diff_ab_hdr($req, $1, $2) . "\n"; + } elsif ($l =~ m{\Adiff --(cc|combined) (.+)\z}) { + $$dst .= git_diff_cc_hdr($req, $1, $2) . "\n"; + } elsif ($l =~ /\Aindex ($cmt)\.\.($cmt)(.*)\z/o) { # regular + $$dst .= git_diff_ab_index($1, $2, $3) . "\n"; + } elsif ($l =~ /\A@@ (\S+) (\S+) @@(.*)\z/) { # regular + $$dst .= git_diff_ab_hunk($req, $1, $2, $3) . "\n"; + } elsif ($l =~ /\Aindex ($cmt,[^\.]+)\.\.($cmt)(.*)$/o) { #--cc + $$dst .= git_diff_cc_index($req, $1, $2, $3) . "\n"; + } elsif ($l =~ /\A(@@@+) (\S+.*\S+) @@@+(.*)\z/) { # --cc + $$dst .= git_diff_cc_hunk($req, $1, $2, $3) . "\n"; + } else { + $$dst .= utf8_html($l) . "\n"; + } + ++$$ndiff; + } +} + +sub git_diff_sed_run ($$) { + my ($dst, $req) = @_; + $req->{dstate} == DSTATE_STAT and git_diff_sed_stat($dst, $req); + $req->{dstate} == DSTATE_LINES and git_diff_sed_lines($dst, $req); + undef; +} + +sub git_diff_sed_close ($$) { + my ($dst, $req) = @_; + $$dst .= utf8_html(delete $req->{dbuf}); + undef; +} + +1; diff --git a/lib/PublicInbox/RepoGitFallback.pm b/lib/PublicInbox/RepoGitFallback.pm new file mode 100644 index 00000000..5ce469be --- /dev/null +++ b/lib/PublicInbox/RepoGitFallback.pm @@ -0,0 +1,21 @@ +# 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::RepoGitFallback; +use strict; +use warnings; +use base qw(PublicInbox::RepoBase); +use PublicInbox::GitHTTPBackend; + +# overrides PublicInbox::RepoBase::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}; + PublicInbox::GitHTTPBackend::serve($req->{env}, $git, $expath); +} + +1; diff --git a/lib/PublicInbox/RepoGitLog.pm b/lib/PublicInbox/RepoGitLog.pm new file mode 100644 index 00000000..9cfa526e --- /dev/null +++ b/lib/PublicInbox/RepoGitLog.pm @@ -0,0 +1,152 @@ +# Copyright (C) 2015 all contributors +# License: AGPL-3.0+ + +# show the log view +package PublicInbox::RepoGitLog; +use strict; +use warnings; +use PublicInbox::Hval qw(utf8_html); +use base qw(PublicInbox::RepoBase); +use PublicInbox::RepoGit qw(git_dec_links git_commit_title); +use PublicInbox::Qspawn; +# cannot rely on --date=format-local:... yet, it is too new (September 2015) +use constant STATES => qw(h p D ai an s b); +use constant STATE_BODY => (scalar(STATES) - 1); +my $LOG_FMT = '--pretty=tformat:'. join('%n', map { "%$_" } STATES).'%x00'; + +sub parent_links { + if (@_ == 1) { # typical, single-parent commit + qq( / parent $_[0]); + } elsif (@_ > 0) { # merge commit + ' / parents ' . + join(' ', map { qq($_) } @_); + } else { + ''; # root commit + } +} + +sub flush_log_hdr ($$$) { + my ($req, $dst, $hdr) = @_; + my $rel = $req->{relcmd}; + my $seen = $req->{seen}; + $$dst .= '
' if scalar keys %$seen;
+	my $id = $hdr->{h};
+	$seen->{$id} = 1;
+	$$dst .= qq();
+	$$dst .= utf8_html($hdr->{'s'}); # FIXME may still OOM
+	$$dst .= '';
+	my $D = $hdr->{D}; # FIXME: thousands of decorations may OOM us
+	if ($D ne '') {
+		$$dst .= ' (' . join(', ', git_dec_links($rel, $D)) . ')';
+	}
+	my @p = split(/ /, $hdr->{p});
+	push @{$req->{parents}}, @p;
+	my $plinks = parent_links(@p);
+	$$dst .= "\n- ";
+	$$dst .= utf8_html($hdr->{an});
+	$$dst .= " @ $hdr->{ai}\n  commit $id$plinks\n";
+	undef
+}
+
+sub git_log_sed_end ($$) {
+	my ($req, $dst) = @_;
+	$$dst .= '
';
+	my $m = '';
+	my $np = 0;
+	my $seen = $req->{seen};
+	my $git = $req->{repo_info}->{git};
+	my $rel = $req->{relcmd};
+	foreach my $p (@{$req->{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);
+	}
+	if ($np == 0) {
+		$$dst .= "No commits follow";
+	} elsif ($np > 1) {
+		$$dst .= "Unseen parent commits to follow (multiple choice):\n";
+	} else {
+		$$dst .= "Next parent to follow:\n";
+	}
+	$$dst .= $m;
+	$$dst .= '
'; +} + +sub git_log_sed ($$) { + my ($self, $req) = @_; + my $buf = ''; + my $state = 0; + $req->{seen} = {}; + $req->{parents} = []; + my $hdr = {}; + sub { + my $dst; + # $_[0] == scalar buffer, undef means EOF from "git log" + $dst = delete $req->{lhtml} || ''; + my @tmp; + if (defined $_[0]) { + $buf .= $_[0]; + @tmp = split(/\n/, $buf, -1); + $buf = @tmp ? pop(@tmp) : ''; + } else { + @tmp = split(/\n/, $buf, -1); + $buf = undef; + } + + foreach my $l (@tmp) { + if ($state != STATE_BODY) { + $hdr->{((STATES)[$state])} = $l; + if (++$state == STATE_BODY) { + flush_log_hdr($req, \$dst, $hdr); + $hdr = {}; + } + next; + } + if ($l eq "\0") { + $dst .= qq(
); + $state = 0; + } else { + $dst .= "\n"; + $dst .= utf8_html($l); + } + } + git_log_sed_end($req, \$dst) unless defined $buf; + $dst; + }; +} + +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 $env = $req->{env}; + my $q = $req->{'q'} = PublicInbox::RepoGitQuery->new($env); + my $h = $q->{h}; + $h eq '' and $h = 'HEAD'; + my $git = $repo_info->{git}; + my $cmd = $git->cmd(qw(log --no-notes --no-color --abbrev-commit), + $git->abbrev, $LOG_FMT, "-$max", $h, '--'); + my $rdr = { 2 => $git->err_begin }; + my $title = "log: $repo_info->{repo} (" . utf8_html($h). ')'; + $req->{lhtml} = $self->html_start($req, $title) . "\n\n"; + my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); + $qsp->psgi_return($env, undef, sub { + my ($r) = @_; + if (!defined $r) { + [ 500, [ 'Content-Type', 'text/html' ], [ $git->err ] ]; + } elsif ($r == 0) { + [ 404, [ 'Content-Type', 'text/html' ], [ $git->err ] ]; + } else { + $env->{'qspawn.filter'} = git_log_sed($self, $req); + [ 200, [ 'Content-Type', 'text/html' ] ]; + } + }); +} + +1; diff --git a/lib/PublicInbox/RepoGitPatch.pm b/lib/PublicInbox/RepoGitPatch.pm new file mode 100644 index 00000000..e9227b6f --- /dev/null +++ b/lib/PublicInbox/RepoGitPatch.pm @@ -0,0 +1,39 @@ +# 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::RepoGitPatch; +use strict; +use warnings; +use base qw(PublicInbox::RepoBase); +use PublicInbox::Qspawn; + +# 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 $env = $req->{env}; + my $q = PublicInbox::RepoGitQuery->new($env); + 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 = $git->cmd(@CMD, $sig." $range", $range, '--'); + my $expath = $req->{expath}; + push @$cmd, $expath if $expath ne ''; + + my $qsp = PublicInbox::Qspawn->new($cmd); + $qsp->psgi_return($env, undef, sub { + my ($r) = @_; + my $h = ['Content-Type', 'text/plain; charset=UTF-8']; + $r ? [ 200, $h ] : [ 500, $h, [ "format-patch error\n" ] ]; + }); +} + +1; diff --git a/lib/PublicInbox/RepoGitPlain.pm b/lib/PublicInbox/RepoGitPlain.pm new file mode 100644 index 00000000..2ba24e08 --- /dev/null +++ b/lib/PublicInbox/RepoGitPlain.pm @@ -0,0 +1,100 @@ +# Copyright (C) 2015-2016 all contributors +# License: AGPL-3.0+ +package PublicInbox::RepoGitPlain; +use strict; +use warnings; +use base qw(PublicInbox::RepoBase); +use PublicInbox::RepoGitBlob; +use PublicInbox::Hval qw(utf8_html); +use PublicInbox::Qspawn; + +sub call_git_plain { + my ($self, $req) = @_; + my $git = $req->{repo_info}->{git}; + my $q = PublicInbox::RepoGitQuery->new($req->{env}); + my $id = $q->{id}; + $id eq '' and $id = 'HEAD'; + $id .= ":$req->{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'; + } 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); +} + +sub git_tree_sed ($) { + my ($req) = @_; + my $buf = ''; + my $end; + my $pfx = $req->{tpfx}; + sub { # $_[0] = buffer or undef + my $dst = delete $req->{tstart} || ''; + my @files; + if (defined $_[0]) { + @files = split(/\0/, $buf .= $_[0]); + $buf = pop @files if scalar @files; + } else { + @files = split(/\0/, $buf); + $end = ''; + } + foreach my $n (@files) { + $n = PublicInbox::Hval->utf8($n); + my $ref = $n->as_path; + $dst .= qq(
  • ); + $dst .= $n->as_html; + $dst .= '
  • '; + } + $end ? $dst .= $end : $dst; + } +} + +# 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/"; + } + } + + $req->{tpfx} = $pfx; + $req->{tstart} = "$title".$t; + my $cmd = $git->cmd(qw(ls-tree --name-only -z), $git->abbrev, $hex); + my $rdr = { 2 => $git->err_begin }; + my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); + my $env = $req->{env}; + $qsp->psgi_return($env, undef, sub { + my ($r) = @_; + if (!defined $r) { + [ 500, [ 'Content-Type', 'text/plain' ], [ $git->err ]]; + } else { + $env->{'qspawn.filter'} = git_tree_sed($req); + [ 200, [ 'Content-Type', 'text/html' ] ]; + } + }); +} + +1; diff --git a/lib/PublicInbox/RepoGitQuery.pm b/lib/PublicInbox/RepoGitQuery.pm new file mode 100644 index 00000000..638a1316 --- /dev/null +++ b/lib/PublicInbox/RepoGitQuery.pm @@ -0,0 +1,50 @@ +# Copyright (C) 2015 all contributors +# License: AGPL-3.0+ + +# query parameter management for repobrowse +package PublicInbox::RepoGitQuery; +use strict; +use warnings; +use PublicInbox::Hval; +use URI::Escape qw(uri_unescape); +my @KNOWN_PARAMS = qw(id id2 h ofs); + +sub new { + my ($class, $env) = @_; + # we don't care about multi-value + my %tmp = map { + my ($k, $v) = split('=', uri_unescape($_), 2); + $v = '' unless defined $v; + $v =~ tr/+/ /; + ($k, $v) + } split(/[&;]/, $env->{QUERY_STRING}); + + my $self = {}; + foreach (@KNOWN_PARAMS) { + my $v = $tmp{$_}; + $self->{$_} = defined $v ? $v : ''; + } + bless $self, $class; +} + +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/RepoGitSnapshot.pm b/lib/PublicInbox/RepoGitSnapshot.pm new file mode 100644 index 00000000..e05ad80c --- /dev/null +++ b/lib/PublicInbox/RepoGitSnapshot.pm @@ -0,0 +1,110 @@ +# Copyright (C) 2016 all contributors +# License: AGPL-3.0+ + +# shows the /snapshot/ endpoint for git repositories +# Mainly for compatibility reasons with cgit, I'm unsure if +# showing this in a repository viewer is a good idea. + +package PublicInbox::RepoGitSnapshot; +use strict; +use warnings; +use base qw(PublicInbox::RepoBase); +use PublicInbox::Git; +use PublicInbox::Qspawn; +our $SUFFIX; +BEGIN { + # as described in git-archive(1), users may add support for + # other compression schemes such as xz or bz2 via git-config(1): + # git config tar.tar.xz.command "xz -c" + # git config tar.tar.bz2.command "bzip2 -c" + chomp(my @l = `git archive --list`); + $SUFFIX = join('|', map { quotemeta $_ } @l); +} + +# Not using standard mime types since the compressed tarballs are +# special or do not match my /etc/mime.types. Choose what gitweb +# and cgit agree on for compatibility. +our %FMT_TYPES = ( + 'tar' => 'application/x-tar', + 'tar.bz2' => 'application/x-bzip2', + 'tar.gz' => 'application/x-gzip', + 'tar.xz' => 'application/x-xz', + 'tgz' => 'application/x-gzip', + 'zip' => 'application/x-zip', +); + +sub call_git_snapshot ($$) { # invoked by PublicInbox::RepoBase::call + my ($self, $req) = @_; + + my @extra = @{$req->{extra}}; + my $ref = shift @extra; + return $self->r(404) if (!defined $ref) || scalar(@extra); + my $orig_fn = $ref; + + # just in case git changes refname rules, don't allow wonky filenames + # to break the Content-Disposition header, either. + return $self->r(404) if $orig_fn =~ /["\s]/s; + return $self->r(404) unless ($ref =~ s/\.($SUFFIX)\z//o); + my $fmt = $1; + my $env = $req->{env}; + my $repo_info = $req->{repo_info}; + + # support disabling certain snapshots types entirely to twart + # URL guessing since it could burn server resources. + return $self->r(404) if $repo_info->{snapshots_disabled}->{$fmt}; + + # strip optional basename (may not exist) + $ref =~ s/$repo_info->{snapshot_re}//; + + # don't allow option/command injection, git refs do not start with '-' + return $self->r(404) if $ref =~ /\A-/; + + my $git = $repo_info->{git}; + my $tree = ''; + my $last_cb = sub { + delete $env->{'repobrowse.tree_cb'}; + delete $env->{'qspawn.quiet'}; + my $pfx = "$repo_info->{snapshot_pfx}-$ref/"; + my $cmd = $git->cmd('archive', + "--prefix=$pfx", "--format=$fmt", $tree); + my $rdr = { 2 => $git->err_begin }; + my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); + $qsp->psgi_return($env, undef, sub { + my $r = $_[0]; + return $self->r(500) unless $r; + [ 200, [ 'Content-Type', + $FMT_TYPES{$fmt} || 'application/octet-stream', + 'Content-Disposition', + qq(inline; filename="$orig_fn"), + 'ETag', qq("$tree") ] ]; + }); + }; + + my $cmd = $git->cmd(qw(rev-parse --verify --revs-only)); + # try prefixing "v" or "V" for tag names to get the tree + my @refs = ("V$ref", "v$ref", $ref); + $env->{'qspawn.quiet'} = 1; + my $tree_cb = $env->{'repobrowse.tree_cb'} = sub { + my ($ref) = @_; + if (defined $ref) { + $tree = $$ref; + chomp $tree; + } + return $last_cb->() if $tree ne ''; + unless (scalar(@refs)) { + my $res = delete $env->{'qspawn.response'}; + return $res->($self->r(404)); + } + my $rdr = { 2 => $git->err_begin }; + my $r = pop @refs; + my $qsp = PublicInbox::Qspawn->new([@$cmd, $r], undef, $rdr); + $qsp->psgi_qx($env, undef, $env->{'repobrowse.tree_cb'}); + }; + sub { + $env->{'qspawn.response'} = $_[0]; + # kick off the "loop" foreach @refs + $tree_cb->(undef); + } +} + +1; diff --git a/lib/PublicInbox/RepoGitSummary.pm b/lib/PublicInbox/RepoGitSummary.pm new file mode 100644 index 00000000..e9e1458b --- /dev/null +++ b/lib/PublicInbox/RepoGitSummary.pm @@ -0,0 +1,109 @@ +# Copyright (C) 2016 all contributors +# License: AGPL-3.0+ + +# The main summary/landing page of a git repository viewer +package PublicInbox::RepoGitSummary; +use strict; +use warnings; +use PublicInbox::Hval qw(utf8_html); +use base qw(PublicInbox::RepoBase); +use PublicInbox::Qspawn; + +sub call_git_summary { + my ($self, $req) = @_; + my $git = $req->{repo_info}->{git}; + my $env = $req->{env}; + + # n.b. we would use %(HEAD) in for-each-ref --format if we could + # rely on git 1.9.0+, but it's too soon for that in early 2017... + my $cmd = $git->cmd(qw(symbolic-ref HEAD)); + my $rdr = { 2 => $git->err_begin }; + my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); + sub { + my ($res) = @_; # Plack streaming callback + $qsp->psgi_qx($env, undef, sub { + chomp(my $head_ref = ${$_[0]}); + for_each_ref($self, $req, $res, $head_ref); + }); + } +} + +use constant EACH_REF_FMT => '--format=' . + join(' ', map { "%($_)" } + qw(refname objecttype objectname creatordate:short subject)); + +sub for_each_ref { + my ($self, $req, $res, $head_ref) = @_; + my $count = 10; # TODO: configurable + my $fh; + my $repo_info = $req->{repo_info}; + my $git = $repo_info->{git}; + my $refs = $git->popen(qw(for-each-ref --sort=-creatordate), + EACH_REF_FMT, "--count=$count", + qw(refs/heads/ refs/tags/)); + $fh = $res->([200, ['Content-Type'=>'text/html; charset=UTF-8']]); + # ref names are unpredictable in length and requires tables :< + $fh->write($self->html_start($req, + "$repo_info->{repo}: overview") . + '
    '); + + my $rel = $req->{relcmd}; + while (<$refs>) { + my ($ref, $type, $hex, $date, $s) = split(' ', $_, 5); + my $x = $ref eq $head_ref ? ' (HEAD)' : ''; + $ref =~ s!\Arefs/(?:heads|tags)/!!; + $ref = PublicInbox::Hval->utf8($ref); + my $h = $ref->as_html; + $ref = $ref->as_href; + my $sref; + if ($type eq 'tag') { + $h = "$h"; + $sref = $ref = $rel . 'tag?h=' . $ref; + } elsif ($type eq 'commit') { + $sref = $rel . 'commit?h=' . $ref; + $ref = $rel . 'log?h=' . $ref; + } else { + # no point in wasting code to support tagged + # trees/blobs... + next; + } + chomp $s; + $fh->write(qq() . + qq('); + + } + $fh->write('
    $h$x$date ) . utf8_html($s) . + '
    '); + + # some people will use README.md or even README.sh here... + my $readme = $repo_info->{readme}; + defined $readme or $readme = [ 'README', 'README.md' ]; + $readme = [ $readme ] if (ref($readme) ne 'ARRAY'); + foreach my $r (@$readme) { + my $doc = $git->cat_file('HEAD:'.$r); + defined $doc or next; + $fh->write('
    ' . readme_path_links($rel, $r) .
    +			" (HEAD)\n\n" . utf8_html($$doc) . '
    '); + } + $fh->write(''); + $fh->close; +} + +sub readme_path_links { + my ($rel, $readme) = @_; + my @path = split(m!/+!, $readme); + + my $s = "tree root/"; + my @t; + $s .= join('/', (map { + push @t, $_; + my $e = PublicInbox::Hval->utf8($_, join('/', @t)); + my $ep = $e->as_path; + my $eh = $e->as_html; + $e = "$eh"; + # bold the last one + scalar(@t) == scalar(@path) ? "$e" : $e; + } @path)); +} + +1; diff --git a/lib/PublicInbox/RepoGitTag.pm b/lib/PublicInbox/RepoGitTag.pm new file mode 100644 index 00000000..96835b2c --- /dev/null +++ b/lib/PublicInbox/RepoGitTag.pm @@ -0,0 +1,213 @@ +# Copyright (C) 2016 all contributors +# License: AGPL-3.0+ + +# shows the /tag/ endpoint for git repositories +package PublicInbox::RepoGitTag; +use strict; +use warnings; +use base qw(PublicInbox::RepoBase); +use POSIX qw(strftime); +use PublicInbox::Hval qw(utf8_html); +use PublicInbox::Qspawn; + +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::RepoGitQuery->new($req->{env}); + my $h = $q->{h}; + $h eq '' and return git_tag_list($self, $req); + 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_each_tag_sed ($$) { + my ($self, $req) = @_; + my $repo_info = $req->{repo_info}; + my $buf = ''; + my $nr = 0; + $req->{thtml} = $self->html_start($req, "$repo_info->{repo}: tag list") . + '' . + join('', map { "" } qw(tag date subject)). + ''; + sub { + my $dst = delete $req->{thtml} || ''; + my $end = ''; + my @lines; + if (defined $_[0]) { + @lines = split(/\n/, $buf .= $_[0]); + $buf = pop @lines if @lines; + } else { # for-each-ref EOF + @lines = split(/\n/, $buf); + $buf = undef; + if ($nr == $req->{-tag_count}) { + $end = "
    Showing the latest $nr tags
    "; + } elsif ($nr == 0) { + $end = '
    no tags to show
    '; + } + $end = "
    $_
    $end"; + } + for (@lines) { + 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; + $dst .= qq() . + qq($h) . + qq($date) . + utf8_html($s) . ''; + } + $dst .= $end; + } +} + +sub git_tag_list { + my ($self, $req) = @_; + my $git = $req->{repo_info}->{git}; + + # TODO: use Xapian so we can more easily handle offsets/limits + # for pagination instead of limiting + my $count = $req->{-tag_count} = 50; + my $cmd = $git->cmd(qw(for-each-ref --sort=-creatordate), + '--format=%(refname) %(creatordate:short) %(subject)', + "--count=$count", 'refs/tags/'); + my $rdr = { 2 => $git->err_begin }; + my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); + my $env = $req->{env}; + $env->{'qspawn.quiet'} = 1; + $qsp->psgi_return($env, undef, sub { # parse output + my ($r) = @_; + if (!defined $r) { + my $errmsg = $git->err; + [ 500, [ 'Content-Type', 'text/html; charset=UTF-8'], + [ $errmsg ] ]; + } else { + $env->{'qspawn.filter'} = git_each_tag_sed($self, $req); + [ 200, [ 'Content-Type', 'text/html; charset=UTF-8' ]]; + } + }); +} + +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->{repo}: 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/RepoGitTree.pm b/lib/PublicInbox/RepoGitTree.pm new file mode 100644 index 00000000..4a68cf69 --- /dev/null +++ b/lib/PublicInbox/RepoGitTree.pm @@ -0,0 +1,220 @@ +# Copyright (C) 2015 all contributors +# License: AGPL-3.0+ +package PublicInbox::RepoGitTree; +use strict; +use warnings; +use base qw(PublicInbox::RepoBase); +use PublicInbox::Hval qw(utf8_html); +use PublicInbox::Qspawn; + +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 'raw' link above"; + +sub call_git_tree { + my ($self, $req) = @_; + my @extra = @{$req->{extra}}; + my $git = $req->{repo_info}->{git}; + my $q = PublicInbox::RepoGitQuery->new($req->{env}); + 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); + + unless (defined($type)) { + return [404, ['Content-Type'=>'text/plain'], ['Not Found']]; + } + + my $opts = { nofollow => 1 }; + my $title = $req->{expath}; + $title = $title eq '' ? 'tree' : utf8_html($title); + if ($type eq 'tree') { + $opts->{noindex} = 1; + $req->{thtml} = $self->html_start($req, $title, $opts) . "\n"; + git_tree_show($req, $hex, $q); + } elsif ($type eq 'blob') { + sub { + my $res = $_[0]; + my $fh = $res->([200, + ['Content-Type','text/html; charset=UTF-8']]); + $fh->write($self->html_start($req, $title, $opts) . + "\n"); + git_blob_show($req, $fh, $git, $hex, $q); + $fh->write(''); + $fh->close; + } + } else { + [404, ['Content-Type', 'text/plain; charset=UTF-8'], + ["Unrecognized type ($type) for $obj\n"]]; + } +} + +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{\npath: $t\n\nblob $hex}; + my $end = ''; + + $git->cat_file($hex, sub { + my ($cat, $left) = @_; # $$left == $size + $h .= qq{\t$$left bytes (raw)}; + $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_sed ($) { + my ($req) = @_; + my @lines; + my $buf = ''; + my $qs = $req->{qs}; + my $pfx = $req->{tpfx}; + my $end; + sub { + my $dst = delete $req->{thtml} || ''; + if (defined $_[0]) { + @lines = split(/\0/, $buf .= $_[0]); + $buf = pop @lines if @lines; + } else { + @lines = split(/\0/, $buf); + $end = ''; + } + for (@lines) { + my ($m, $x, $s, $path) = + (/\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 + $dst .= 'g' . (' ' x 15) . "$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 + $dst .= qq($m\t). + qq($s\t$path\n); + } + $dst; + } +} + +sub git_tree_show { + my ($req, $hex, $q) = @_; + my $git = $req->{repo_info}->{git}; + my $cmd = $git->cmd(qw(ls-tree -l -z), $git->abbrev, $hex); + my $rdr = { 2 => $git->err_begin }; + my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); + my $t = cur_path($req, $q); + my $pfx; + + $req->{thtml} .= "\npath: $t\n\nmode\tsize\tname\n"; + $req->{qs} = $q->qs; + if ($req->{tslash}) { + $pfx = './'; + } elsif (defined(my $last = $req->{extra}->[-1])) { + $pfx = PublicInbox::Hval->utf8($last)->as_path . '/'; + } else { + $pfx = 'tree/'; + } + $req->{tpfx} = $pfx; + my $env = $req->{env}; + $qsp->psgi_return($env, undef, sub { + my ($r) = @_; + if (defined $r) { + $env->{'qspawn.filter'} = git_tree_sed($req); + [ 200, [ 'Content-Type', 'text/html' ] ]; + } else { + [ 500, [ 'Content-Type', 'text/plain' ], [ $git->err ]]; + } + }); +} + +1; diff --git a/lib/PublicInbox/RepoRoot.pm b/lib/PublicInbox/RepoRoot.pm new file mode 100644 index 00000000..9ab25b97 --- /dev/null +++ b/lib/PublicInbox/RepoRoot.pm @@ -0,0 +1,71 @@ +# Copyright (C) 2016 all contributors +# License: AGPL-3.0+ + +# displays the root '/' where all the projects lie +package PublicInbox::RepoRoot; +use strict; +use warnings; +use base qw(PublicInbox::RepoBase); +use PublicInbox::Hval qw(utf8_html); + +sub call { + my ($self, $rconfig) = @_; + sub { + my ($res) = @_; # PSGI callback + my @h = ('Content-Type', 'text/html; charset=UTF-8'); + my $fh = $res->([200, \@h]); + repobrowse_index($fh, $rconfig); + $fh->close; + } +} + +sub repobrowse_index { + my ($fh, $rconfig) = @_; + my $title = 'repobrowse index'; + $fh->write("$title" . + PublicInbox::Hval::STYLE . + "
    $title");
    +
    +	# preload all groups
    +	foreach my $k (sort keys %$rconfig) {
    +		$k =~ /\Arepo\.(.+)\.path\z/ or next;
    +		my $repo_path = $1;
    +		$rconfig->lookup($repo_path); # insert into groups
    +	}
    +
    +	my $groups = $rconfig->{-groups};
    +	if (scalar(keys %$groups) > 2) { # default has '-none' + '-hidden'
    +		$fh->write("\n\nuncategorized
    ". + ""); + } else { + $fh->write(""); + } + foreach my $repo_path (sort @{$groups->{-none}}) { + my $r = $rconfig->lookup($repo_path); + my $p = PublicInbox::Hval->utf8($r->{repo}); + my $l = $p->as_html; + $p = $p->as_path; + $fh->write(qq($l) . + " $r->{desc_html}"); + } + + foreach my $group (keys %$groups) { + next if $group =~ /\A-(?:none|hidden)\z/; + my $g = utf8_html($group); + $fh->write("
     
    ". + "
    $g
    "); + foreach my $repo_path (sort @{$groups->{$group}}) { + my $r = $rconfig->lookup($repo_path); + my $p = PublicInbox::Hval->utf8($r->{repo}); + my $l = $p->as_html; + $p = $p->as_path; + $fh->write(' ' . + qq($l) . + " $r->{desc_html}"); + } + } + + $fh->write(''); +} + +1; diff --git a/lib/PublicInbox/Repobrowse.pm b/lib/PublicInbox/Repobrowse.pm index 1050ed50..87e12278 100644 --- a/lib/PublicInbox/Repobrowse.pm +++ b/lib/PublicInbox/Repobrowse.pm @@ -21,7 +21,7 @@ package PublicInbox::Repobrowse; use strict; use warnings; use URI::Escape qw(uri_escape_utf8 uri_unescape); -use PublicInbox::RepobrowseConfig; +use PublicInbox::RepoConfig; my %CMD = map { lc($_) => $_ } qw(Log Commit Tree Patch Blob Plain Tag Atom Diff Snapshot); @@ -30,7 +30,7 @@ my %LOADED; sub new { my ($class, $rconfig) = @_; - $rconfig ||= PublicInbox::RepobrowseConfig->new; + $rconfig ||= PublicInbox::RepoConfig->new; bless { rconfig => $rconfig }, $class; } @@ -80,8 +80,8 @@ sub no_tslash { sub root_index { my ($self) = @_; - my $mod = load_once('PublicInbox::RepobrowseRoot'); - $mod->new->call($self->{rconfig}); # RepobrowseRoot::call + my $mod = load_once('PublicInbox::RepoRoot'); + $mod->new->call($self->{rconfig}); # RepoRoot::call } sub call { @@ -97,7 +97,7 @@ sub call { return $self->root_index($self) unless length($repo_path); - my $rconfig = $self->{rconfig}; # RepobrowseConfig + my $rconfig = $self->{rconfig}; # RepoConfig my $repo_info; until ($repo_info = $rconfig->lookup($repo_path)) { my $p = shift @extra or last; @@ -141,14 +141,14 @@ sub call { return no_tslash($env) if ($tslash && $NO_TSLASH{$mod}); $req->{tslash} = $tslash; - $mod = load_once("PublicInbox::Repobrowse$vcs$mod"); + $mod = load_once("PublicInbox::Repo$vcs$mod"); $vcs = load_once("PublicInbox::$vcs"); # $repo_info->{git} ||= PublicInbox::Git->new(...) $repo_info->{$vcs_lc} ||= $vcs->new($repo_info->{path}); $req->{expath} = join('/', @extra); - my $rv = eval { $mod->new->call($cmd, $req) }; # RepobrowseBase::call + my $rv = eval { $mod->new->call($cmd, $req) }; # RepoBase::call $rv || r404(); } diff --git a/lib/PublicInbox/RepobrowseBase.pm b/lib/PublicInbox/RepobrowseBase.pm deleted file mode 100644 index 44f422df..00000000 --- a/lib/PublicInbox/RepobrowseBase.pm +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (C) 2015 all contributors -# License: AGPL-3.0+ -package PublicInbox::RepobrowseBase; -use strict; -use warnings; -require PublicInbox::RepobrowseGitQuery; -use PublicInbox::Hval; -our %MIME_TYPE_WHITELIST = ('application/pdf' => 1); - -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; - while (<$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); - return unless defined $ct; - - # 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 - ($ct =~ m!\A(?:image|audio|video)/! || $MIME_TYPE_WHITELIST{$ct}) ? - $ct : undef; -} - -# starts an HTML page for Repobrowse in a consistent way -sub html_start { - my ($self, $req, $title_html, $opts) = @_; - my $desc = $req->{repo_info}->{desc_html}; - my $meta = ''; - - if ($opts) { - my @robots; - foreach (qw(nofollow noindex)) { - push @robots, $_ if $opts->{$_}; - } - $meta = qq('; - } - - "$title_html" . - PublicInbox::Hval::STYLE . $meta . - "
    $desc";
    -}
    -
    -sub r {
    -	my ($self, $status, $req, @extra) = @_;
    -	my @h;
    -
    -	my $body = '';
    -	if ($status == 301 || $status == 302) {
    -		# The goal is to be able to make redirects like we make
    -		#  tags with '../'
    -		my $env = $req->{env};
    -		my $base = PublicInbox::Repobrowse::base_url($env);
    -		my ($redir) = @extra;
    -		if ($redir =~ m!\A\.\./!) { # relative redirect
    -			my @orig = split(m!/+!, $env->{PATH_INFO});
    -			my @dest = split(m!/+!, $redir);
    -
    -			while ($dest[0] eq '..') {
    -				pop @orig;
    -				shift @dest;
    -			}
    -			my $end = '';
    -			$end = pop @dest if $dest[-1] =~ /\A[#\?]/;
    -			$redir = $base . join('/', @orig, @dest) . $end;
    -		} else {
    -			$redir = $base . '/' . $redir;
    -		}
    -		push @h, qw(Content-Type text/plain Location), $redir;
    -
    -		# mainly for curl (no-'-L') users:
    -		$body = "Redirecting to $redir\n";
    -	} else {
    -		push @h, qw(Content-Type text/plain);
    -	}
    -
    -	[ $status, \@h, [ $body ] ]
    -}
    -
    -1;
    diff --git a/lib/PublicInbox/RepobrowseConfig.pm b/lib/PublicInbox/RepobrowseConfig.pm
    deleted file mode 100644
    index a08c6cec..00000000
    --- a/lib/PublicInbox/RepobrowseConfig.pm
    +++ /dev/null
    @@ -1,88 +0,0 @@
    -# Copyright (C) 2015 all contributors 
    -# License: AGPL-3.0+ 
    -package PublicInbox::RepobrowseConfig;
    -use strict;
    -use warnings;
    -use PublicInbox::Inbox;
    -use PublicInbox::Config;
    -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} = {};
    -
    -	# hard disable these with '-' prefix by default:
    -	$self->{'repobrowse.snapshots'} ||= '-tar.bz2 -tar.xz';
    -
    -	# for root
    -	$self->{-groups} = { -hidden => [], -none => [] };
    -	$self;
    -}
    -
    -sub default_file {
    -	my $f = $ENV{REPOBROWSE_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->{repo} = $repo_path;
    -
    -	# snapshots:
    -	my $snap = (split('/', $repo_path))[-1];
    -	$snap =~ s/\.git\z//; # seems common for git URLs to end in ".git"
    -	$rv->{snapshot_re} = qr/\A\Q$snap\E[-_]/;
    -	$rv->{snapshot_pfx} = $snap;
    -
    -	# gitweb compatibility
    -	foreach my $key (qw(description cloneurl)) {
    -		$rv->{$key} = PublicInbox::Inbox::try_cat("$path/$key");
    -	}
    -
    -	$rv->{desc_html} =
    -		PublicInbox::Hval->new_oneline($rv->{description})->as_html;
    -
    -	foreach my $key (qw(publicinbox vcs readme group snapshots)) {
    -		$rv->{$key} = $self->{"repo.$repo_path.$key"};
    -	}
    -	unless (defined $rv->{snapshots}) {
    -		$rv->{snapshots} = $self->{'repobrowse.snapshots'} || '';
    -	}
    -
    -	my %disabled;
    -	foreach (split(/\s+/, $rv->{snapshots})) {
    -		s/\A-// and $disabled{$_} = 1;
    -	}
    -	$rv->{snapshots_disabled} = \%disabled;
    -
    -	my $g = $rv->{group};
    -	defined $g or $g = '-none';
    -	if (ref($g) eq 'ARRAY') {
    -		push @{$self->{-groups}->{$_} ||= []}, $repo_path foreach @$g;
    -	} else {
    -		push @{$self->{-groups}->{$g} ||= []}, $repo_path;
    -	}
    -
    -	# 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
    deleted file mode 100644
    index eb79e563..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/RepobrowseGitAtom.pm b/lib/PublicInbox/RepobrowseGitAtom.pm
    deleted file mode 100644
    index 65b723c4..00000000
    --- a/lib/PublicInbox/RepobrowseGitAtom.pm
    +++ /dev/null
    @@ -1,169 +0,0 @@
    -# Copyright (C) 2016 all contributors 
    -# License: AGPL-3.0+ 
    -
    -# show log as an Atom feed
    -package PublicInbox::RepobrowseGitAtom;
    -use strict;
    -use warnings;
    -use PublicInbox::Hval qw(utf8_html);
    -use base qw(PublicInbox::RepobrowseBase);
    -use PublicInbox::Qspawn;
    -
    -use constant DATEFMT => '%Y-%m-%dT%H:%M:%SZ';
    -use constant STATES => qw(H ct an ae at s b);
    -use constant STATE_BODY => (scalar(STATES) - 1);
    -my $ATOM_FMT = '--pretty=tformat:'.
    -		join('%n', map { "%$_" } STATES).'%x00';
    -use POSIX qw(strftime);
    -
    -sub repo_root_url {
    -	my ($self, $req) = @_;
    -	my $env = $req->{env};
    -	my $uri = $env->{REQUEST_URI};
    -	$uri =~ s/\?.+\z//; # no query string
    -	my @uri = split(m!/+!, $uri);
    -	my @extra = @{$req->{extra}};
    -	while (@uri && @extra && $uri[-1] eq $extra[-1]) {
    -		pop @uri;
    -		pop @extra;
    -	}
    -	pop @uri if $uri[-1] eq 'atom'; # warn if not equal?
    -	PublicInbox::Repobrowse::base_url($env) . join('/', @uri);
    -}
    -
    -sub flush_hdr ($$$) {
    -	my ($dst, $hdr, $url) = @_;
    -	$$dst .= '';
    -	$$dst .= utf8_html($hdr->{'s'}); # commit subject
    -	$$dst .= '';
    -	$$dst .= strftime(DATEFMT, gmtime($hdr->{ct}));
    -	$$dst .= '';
    -	$$dst .= utf8_html($hdr->{an});
    -	$$dst .= '';
    -	$$dst .= utf8_html($hdr->{ae});
    -	$$dst .= '';
    -	$$dst .= strftime(DATEFMT, gmtime($hdr->{at}));
    -	$$dst .= '';
    -	$$dst .= qq();
    -	$$dst .= $H;
    -	$$dst .= qq();
    -
    -	$$dst .= qq();
    -	$$dst .= qq();
    -	undef
    -}
    -
    -sub git_atom_sed ($$) {
    -	my ($self, $req) = @_;
    -	my $buf = '';
    -	my $state = 0;
    -	my $rel = $req->{relcmd};
    -	my $repo_info = $req->{repo_info};
    -	my $title = join('/', $repo_info->{repo}, @{$req->{extra}});
    -	$title = utf8_html("$title, branch $req->{q}->{h}");
    -	my $url = repo_root_url($self, $req);
    -	my $hdr = {};
    -	$req->{axml} = qq(\n) .
    -		qq() .
    -		qq($title) .
    -		qq($repo_info->{desc_html}) .
    -		qq();
    -	my ($plinks, $id, $ai);
    -	my $end = '';
    -	my $blines;
    -	sub {
    -		my $dst;
    -		# $_[0] == scalar buffer, undef means EOF from "git log"
    -		$dst = delete $req->{axml} || '';
    -		my @tmp;
    -		if (defined $_[0]) {
    -			$buf .= $_[0];
    -			@tmp = split(/\n/, $buf, -1);
    -			$buf = @tmp ? pop(@tmp) : '';
    -		} else {
    -			@tmp = split(/\n/, $buf, -1);
    -			$buf = '';
    -			$end = '';
    -		}
    -
    -		foreach my $l (@tmp) {
    -			if ($state != STATE_BODY) {
    -				$hdr->{((STATES)[$state])} = $l;
    -				if (++$state == STATE_BODY) {
    -					flush_hdr(\$dst, $hdr, $url);
    -					$hdr = {};
    -					$blines = 0;
    -				}
    -				next;
    -			}
    -			if ($l eq "\0") {
    -				$dst .= qq(
    ); - $state = 0; - } else { - $dst .= "\n" if $blines++; - $dst .= utf8_html($l); - } - } - $dst .= $end; - } -} - -sub git_atom_cb { - my ($self, $req) = @_; - sub { - my ($r) = @_; - my $env = $req->{env}; - if (!defined $r) { - my $git = $req->{repo_info}->{git}; - return [ 400, [ 'Content-Type', 'text/plain' ], - [ $git->err ] ]; - } - $env->{'qspawn.filter'} = git_atom_sed($self, $req); - [ 200, [ 'Content-Type', 'application/atom+xml' ] ]; - } -} - -sub call_git_atom { - my ($self, $req) = @_; - my $repo_info = $req->{repo_info}; - my $max = $repo_info->{max_commit_count} || 10; - $max = int($max); - $max = 50 if $max == 0; - - my $git = $repo_info->{git}; - my $env = $req->{env}; - my $q =$req->{'q'} = PublicInbox::RepobrowseGitQuery->new($env); - my $h = $q->{h}; - my $read_log = sub { - my $cmd = $git->cmd(qw(log --no-notes --no-color - --abbrev-commit), $git->abbrev, - $ATOM_FMT, "-$max", $h, '--'); - my $expath = $req->{expath}; - push @$cmd, $expath if $expath ne ''; - my $rdr = { 2 => $git->err_begin }; - my $qsp = PublicInbox::Qspawn->new($cmd, undef, undef, $rdr); - $qsp->psgi_return($env, undef, git_atom_cb($self, $req)); - }; - - sub { - $env->{'qspawn.response'} = $_[0]; - return $read_log->() if $h ne ''; - - my $cmd = $git->cmd(qw(symbolic-ref --short HEAD)); - my $rdr = { 2 => $git->err_begin }; - my $qsp = PublicInbox::Qspawn->new($cmd, undef, undef, $rdr); - $qsp->psgi_qx($env, undef, sub { - chomp($h = ${$_[0]}); - $read_log->(); - }) - } -} - -1; diff --git a/lib/PublicInbox/RepobrowseGitBlob.pm b/lib/PublicInbox/RepobrowseGitBlob.pm deleted file mode 100644 index a11c457c..00000000 --- a/lib/PublicInbox/RepobrowseGitBlob.pm +++ /dev/null @@ -1,77 +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::RepobrowseGitQuery->new($req->{env}); - my $id = $q->{id}; - $id eq '' and $id = 'HEAD'; - $id .= ":$req->{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; charset=UTF-8'; - } 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; charset=UTF-8' - : '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 6eadd2e5..00000000 --- a/lib/PublicInbox/RepobrowseGitCommit.pm +++ /dev/null @@ -1,201 +0,0 @@ -# Copyright (C) 2015 all contributors -# License: AGPL-3.0+ - -# shows the /commit/ endpoint for git repositories -# -# anchors used: -# D - diffstat -# P - parents -# ...and various filenames from to_attr -# The 'D' and 'P' anchors may conflict with odd filenames, but we won't -# punish the common case with extra bytes if somebody uses 'D' or 'P' -# in filenames. - -package PublicInbox::RepobrowseGitCommit; -use strict; -use warnings; -use base qw(PublicInbox::RepobrowseBase); -use PublicInbox::Hval qw(utf8_html to_attr); -use PublicInbox::RepobrowseGit qw(git_unquote git_commit_title); -use PublicInbox::RepobrowseGitDiffCommon; -use PublicInbox::Qspawn; - -use constant GIT_FMT => '--pretty=format:'.join('%n', - '%H', '%h', '%s', '%an <%ae>', '%ai', '%cn <%ce>', '%ci', - '%t', '%p', '%D', '%b%x00'); - -use constant CC_EMPTY => " This is a merge, and the combined diff is empty.\n"; -use constant CC_MERGE => " This is a merge, showing combined diff:\n\n"; - -sub commit_header { - my ($self, $req) = @_; - my ($H, $h, $s, $au, $ad, $cu, $cd, $t, $p, $D, $rest) = - split("\n", $req->{dbuf}, 11); - $s = utf8_html($s); - $au = utf8_html($au); - $cu = utf8_html($cu); - my @p = split(' ', $p); - - my $rel = $req->{relcmd}; - my $q = $req->{'q'}; - my $qs = $req->{qs} = $q->qs(id => $h); - my $x = $self->html_start($req, $s) . "\n" . - qq( commit $H (patch)\n) . - qq( tree $t); - - my $git = $req->{repo_info}->{git}; - # 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; - $ep = "${rel}tree/$ep?id=$h"; - qq($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) { - $req->{mhelp} = CC_MERGE; - 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); - } - } - $x .= "\n"; - $x .= $s; - $x .= "\n\n"; - my $bx00; - - # FIXME: deal with excessively long commit message bodies - ($bx00, $req->{dbuf}) = split("\0", $rest, 2); - $req->{anchors} = {}; - $req->{h} = $h; - $req->{p} = \@p; - $x .= utf8_html($bx00) . "---\n"; -} - -sub git_commit_sed ($$) { - my ($self, $req) = @_; - git_diff_sed_init($req); - my $dbuf = \($req->{dbuf}); - - # this filters for $fh->write or $body->getline (see Qspawn) - sub { - my $dst = ''; - if (defined $_[0]) { # $_[0] == scalar buffer - $$dbuf .= $_[0]; - if ($req->{dstate} == DSTATE_INIT) { - return $dst if index($$dbuf, "\0") < 0; - $req->{dstate} = DSTATE_STAT; - $dst .= commit_header($self, $req); - } - git_diff_sed_run(\$dst, $req); - } else { # undef means EOF from "git show", flush the last bit - git_diff_sed_close(\$dst, $req); - $dst .= CC_EMPTY if delete $req->{mhelp}; - show_unchanged(\$dst, $req); - $dst .= ''; - } - $dst; - } -} - -sub call_git_commit { # RepobrowseBase calls this - my ($self, $req) = @_; - my $env = $req->{env}; - my $q = PublicInbox::RepobrowseGitQuery->new($env); - my $id = $q->{id}; - $id eq '' and $id = 'HEAD'; - - my $expath = $req->{expath}; - if ($expath ne '') { - my $relup = join('', map { '../' } @{$req->{extra}}); - my $qs = $q->qs; - return $self->r(301, $req, "$relup$qs#".to_attr($expath)); - } - - my $git = $req->{repo_info}->{git}; - my $cmd = $git->cmd(qw(show -z --numstat -p --encoding=UTF-8 - --no-notes --no-color -c), - $git->abbrev, GIT_FMT, $id, '--'); - my $rdr = { 2 => $git->err_begin }; - my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); - $req->{'q'} = $q; - $env->{'qspawn.quiet'} = 1; - $qsp->psgi_return($env, undef, sub { # parse header - my ($r, $bref) = @_; - if (!defined $r) { - my $errmsg = $git->err; - [ 500, [ 'Content-Type', 'text/html' ], [ $errmsg ] ]; - } elsif ($r == 0) { - git_commit_404($req); - } else { - $env->{'qspawn.filter'} = git_commit_sed($self, $req); - [ 200, [ 'Content-Type', 'text/html' ] ]; - } - }); -} - -sub git_commit_404 { - my ($req) = @_; - my $x = 'Missing commit or path'; - my $pfx = "$req->{relcmd}commit"; - - my $try = 'try'; - $x = "$x
    $x\n\n";
    -	my $qs = $req->{'q'}->qs(id => '');
    -	$x .= "$try the latest commit in HEAD\n";
    -	$x .= '
    '; - - [404, ['Content-Type'=>'text/html'], [ $x ]]; -} - -# FIXME: horrifically expensive... -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 . qq( $p $t\n); -} - -# do not break anchor links if the combined diff doesn't show changes: -sub show_unchanged { - my ($dst, $req) = @_; - - my @unchanged = sort keys %{$req->{anchors}}; - return unless @unchanged; - my $anchors = $req->{anchors}; - $$dst .= "\n There are uninteresting changes from this merge.\n" . - qq( See the parents, ) . - "or view final state(s) below:\n\n"; - my $rel = $req->{relcmd}; - my $qs = $req->{qs}; - foreach my $anchor (@unchanged) { - my $fn = $anchors->{$anchor}; - my $p = PublicInbox::Hval->utf8(git_unquote($fn)); - $p = $p->as_path; - $fn = utf8_html($fn); - $$dst .= qq(\t); - $$dst .= "$fn\n"; - } -} - -1; diff --git a/lib/PublicInbox/RepobrowseGitDiff.pm b/lib/PublicInbox/RepobrowseGitDiff.pm deleted file mode 100644 index e2b7179f..00000000 --- a/lib/PublicInbox/RepobrowseGitDiff.pm +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (C) 2016 all contributors -# License: AGPL-3.0+ - -# shows the /diff endpoint for git repositories for cgit compatibility -# usage: /repo.git/diff?id=COMMIT_ID&id2=COMMIT_ID2 -# -# We probably will not link to this outright because it's expensive, -# but exists to preserve URL compatibility with cgit. -package PublicInbox::RepobrowseGitDiff; -use strict; -use warnings; -use base qw(PublicInbox::RepobrowseBase); -use PublicInbox::Hval qw(utf8_html); -use PublicInbox::RepobrowseGitDiffCommon; -use PublicInbox::Qspawn; - -sub git_diff_sed ($$) { - my ($self, $req) = @_; - git_diff_sed_init($req); - $req->{dstate} = DSTATE_STAT; - # this filters for $fh->write or $body->getline (see Qspawn) - sub { - my $dst = delete $req->{dhtml} || ''; - if (defined $_[0]) { # $_[0] == scalar buffer - $req->{dbuf} .= $_[0]; - git_diff_sed_run(\$dst, $req); - } else { # undef means EOF from "git show", flush the last bit - git_diff_sed_close(\$dst, $req); - $dst .= ''; - } - $dst; - } -} - -sub call_git_diff { - my ($self, $req) = @_; - my $env = $req->{env}; - my $q = PublicInbox::RepobrowseGitQuery->new($env); - my $id = $q->{id}; - my $id2 = $q->{id2}; - - my $git = $req->{repo_info}->{git}; - my $cmd = $git->cmd(qw(diff-tree -z --numstat -p --encoding=UTF-8 - --no-color -M -B -D -r), $id2, $id, '--'); - my $expath = $req->{expath}; - push @$cmd, $expath if $expath ne ''; - my $o = { nofollow => 1, noindex => 1 }; - my $ex = $expath eq '' ? '' : " $expath"; - $req->{dhtml} = $self->html_start($req, 'diff', $o). "\n\n". - utf8_html("git diff-tree -r -M -B -D ". - "$id2 $id --$ex"). "\n\n"; - $req->{p} = [ $id2 ]; - $req->{h} = $id; - my $rdr = { 2 => $git->err_begin }; - my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); - # $env->{'qspawn.quiet'} = 1; - $qsp->psgi_return($env, undef, sub { # parse header - my ($r) = @_; - if (!defined $r) { - [ 500, [ 'Content-Type', 'text/html' ], [ $git->err ]]; - } elsif ($r == 0) { - [ 200, [ 'Content-Type', 'text/html' ], [ - delete($req->{dhtml}). - 'No differences' ] - ] - } else { - $env->{'qspawn.filter'} = git_diff_sed($self, $req); - [ 200, [ 'Content-Type', 'text/html' ] ]; - } - }); -} - -1; diff --git a/lib/PublicInbox/RepobrowseGitDiffCommon.pm b/lib/PublicInbox/RepobrowseGitDiffCommon.pm deleted file mode 100644 index 968ca7a7..00000000 --- a/lib/PublicInbox/RepobrowseGitDiffCommon.pm +++ /dev/null @@ -1,297 +0,0 @@ -# Copyright (C) 2016 all contributors -# License: AGPL-3.0+ - -# common git diff-related code -package PublicInbox::RepobrowseGitDiffCommon; -use strict; -use warnings; -use PublicInbox::RepobrowseGit qw/git_unquote git_commit_title/; -use PublicInbox::Hval qw/utf8_html to_attr/; -use base qw/Exporter/; -our @EXPORT = qw/git_diff_sed_init git_diff_sed_close git_diff_sed_run - DSTATE_INIT DSTATE_STAT DSTATE_LINES/; - -# index abcdef89..01234567 -sub git_diff_ab_index ($$$) { - my ($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 ($req, $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/!!; - my $anchor = to_attr($fb); - delete $req->{anchors}->{$anchor}; - $fa = $req->{fa} = PublicInbox::Hval->utf8($fa); - $fb = $req->{fb} = PublicInbox::Hval->utf8($fb); - $req->{path_a} = $fa->as_path; - $req->{path_b} = $fb->as_path; - - # not wasting bandwidth on links here - # links in hunk headers are far more useful with line offsets - qq(diff --git $html_a $html_b); -} - -# diff (--cc|--combined) -sub git_diff_cc_hdr { - my ($req, $combined, $path) = @_; - my $html_path = utf8_html($path); - $path = git_unquote($path); - my $anchor = to_attr($path); - delete $req->{anchors}->{$anchor}; - my $cc = $req->{cc} = PublicInbox::Hval->utf8($path); - $req->{path_cc} = $cc->as_path; - qq(diff --$combined $html_path); -} - -# @@ -1,2 +3,4 @@ (regular diff) -sub git_diff_ab_hunk ($$$$) { - my ($req, $ca, $cb, $ctx) = @_; - my ($na) = ($ca =~ /\A-(\d+)/); - my ($nb) = ($cb =~ /\A\+(\d+)/); - - # we add "rel=nofollow" here to reduce load on search engines, here - my $rel = $req->{relcmd}; - my $rv = '@@ '; - if (defined($na) && $na == 0) { # new file - $rv .= $ca; - } else { - $na = defined $na ? "#n$na" : ''; - my $p = $req->{p}->[0]; - $rv .= qq({path_a}?id=$p$na">); - $rv .= "$ca"; - } - $rv .= ' '; - if (defined($nb) && $nb == 0) { # deleted file - $rv .= $cb; - } else { - my $h = $req->{h}; - $nb = defined $nb ? "#n$nb" : ''; - $rv .= qq({path_b}?id=$h$nb">); - $rv .= "$cb"; - } - $rv . ' @@' . utf8_html($ctx); -} - -# index abcdef09,01234567..76543210 -sub git_diff_cc_index { - my ($req, $before, $last, $end) = @_; - $end = utf8_html($end); - my @before = split(',', $before); - $req->{pobj_cc} = \@before; - - # 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 ($req, $at, $offs, $ctx) = @_; - my @offs = split(' ', $offs); - my $last = pop @offs; - my @p = @{$req->{p}}; - my @pobj = @{$req->{pobj_cc}}; - my $path = $req->{path_cc}; - my $rel = $req->{relcmd}; - 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 = $req->{h}; - $rv .= qq( $last); - } - $rv .= " $at" . utf8_html($ctx); -} - -sub git_diffstat_rename ($$$) { - my ($req, $from, $to) = @_; - my $anchor = to_attr(git_unquote($to)); - $req->{anchors}->{$anchor} = $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 = qq($th); - @base ? "$base/{$from => $to}" : "$from => $to"; -} - -sub DSTATE_INIT () { 0 } -sub DSTATE_STAT () { 1 } -sub DSTATE_LINES () { 2 } - -sub git_diff_sed_init ($) { - my ($req) = @_; - $req->{dbuf} = ''; - $req->{ndiff} = $req->{nchg} = $req->{nadd} = $req->{ndel} = 0; - $req->{dstate} = DSTATE_INIT; -} - -sub git_diff_sed_stat ($$) { - my ($dst, $req) = @_; - my @stat = split(/\0/, $req->{dbuf}, -1); - my $eos; - my $nchg = \($req->{nchg}); - my $nadd = \($req->{nadd}); - my $ndel = \($req->{ndel}); - if (!$req->{dstat_started}) { - $req->{dstat_started} = 1; - - # merges start with an extra '\0' before the diffstat - # non-merge commits start with an extra '\n', instead - if ($req->{mhelp}) { - if ($stat[0] eq '') { - shift @stat; - } else { - warn -'initial merge diffstat line was not empty'; - } - } else { - # for commits, only (not diff-tree) - $stat[0] =~ s/\A\n//s; - } - } - while (defined(my $l = shift @stat)) { - if ($l eq '') { - $eos = 1 if $stat[0] && $stat[0] =~ /\Ad/; # "diff --" - last; - } elsif ($l =~ /\Adiff /) { - unshift @stat, $l; - $eos = 1; - last; - } - $l =~ /\A(\S+)\t+(\S+)\t+(.*)/ or next; - my ($add, $del, $fn) = ($1, $2, $3); - if ($fn ne '') { # normal modification - # TODO: discard diffs if they are too big - # gigantic changes with many files may still OOM us - my $anchor = to_attr(git_unquote($fn)); - $req->{anchors}->{$anchor} = $fn; - $l = utf8_html($fn); - $l = qq($l); - } else { # rename - # incomplete... - if (scalar(@stat) < 2) { - unshift @stat, $l; - last; - } - my $from = shift @stat; - my $to = shift @stat; - $l = git_diffstat_rename($req, $from, $to); - } - - # text changes show numerically, Binary does not - if ($add =~ /\A\d+\z/) { - $$nadd += $add; - $$ndel += $del; - $add = "+$add"; - $del = "-$del"; - } - ++$$nchg; - my $num = sprintf('% 6s/%-6s', $del, $add); - $$dst .= " $num\t$l\n"; - } - - $req->{dbuf} = join("\0", @stat); - return unless $eos; - - $req->{dstate} = DSTATE_LINES; - $$dst .= "\n $$nchg "; - $$dst .= $$nchg == 1 ? 'file changed, ' : 'files changed, '; - $$dst .= $$nadd; - $$dst .= $$nadd == 1 ? ' insertion(+), ' : ' insertions(+), '; - $$dst .= $$ndel; - $$dst .= $$ndel == 1 ? " deletion(-)\n\n" : " deletions(-)\n\n"; -} - -sub git_diff_sed_lines ($$) { - my ($dst, $req) = @_; - - # TODO: discard diffs if they are too big - - my @dlines = split(/\n/, $req->{dbuf}, -1); - $req->{dbuf} = ''; - - if (my $help = delete $req->{mhelp}) { - $$dst .= $help; # CC_MERGE - } - - # don't touch the last line, it may not be terminated - $req->{dbuf} .= pop @dlines; - - my $ndiff = \($req->{ndiff}); - my $cmt = '[a-f0-9]+'; - while (defined(my $l = shift @dlines)) { - if ($l =~ m{\Adiff --git ("?a/.+) ("?b/.+)\z}) { # regular - $$dst .= git_diff_ab_hdr($req, $1, $2) . "\n"; - } elsif ($l =~ m{\Adiff --(cc|combined) (.+)\z}) { - $$dst .= git_diff_cc_hdr($req, $1, $2) . "\n"; - } elsif ($l =~ /\Aindex ($cmt)\.\.($cmt)(.*)\z/o) { # regular - $$dst .= git_diff_ab_index($1, $2, $3) . "\n"; - } elsif ($l =~ /\A@@ (\S+) (\S+) @@(.*)\z/) { # regular - $$dst .= git_diff_ab_hunk($req, $1, $2, $3) . "\n"; - } elsif ($l =~ /\Aindex ($cmt,[^\.]+)\.\.($cmt)(.*)$/o) { #--cc - $$dst .= git_diff_cc_index($req, $1, $2, $3) . "\n"; - } elsif ($l =~ /\A(@@@+) (\S+.*\S+) @@@+(.*)\z/) { # --cc - $$dst .= git_diff_cc_hunk($req, $1, $2, $3) . "\n"; - } else { - $$dst .= utf8_html($l) . "\n"; - } - ++$$ndiff; - } -} - -sub git_diff_sed_run ($$) { - my ($dst, $req) = @_; - $req->{dstate} == DSTATE_STAT and git_diff_sed_stat($dst, $req); - $req->{dstate} == DSTATE_LINES and git_diff_sed_lines($dst, $req); - undef; -} - -sub git_diff_sed_close ($$) { - my ($dst, $req) = @_; - $$dst .= utf8_html(delete $req->{dbuf}); - undef; -} - -1; diff --git a/lib/PublicInbox/RepobrowseGitFallback.pm b/lib/PublicInbox/RepobrowseGitFallback.pm deleted file mode 100644 index 38640139..00000000 --- a/lib/PublicInbox/RepobrowseGitFallback.pm +++ /dev/null @@ -1,21 +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 PublicInbox::GitHTTPBackend; - -# 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}; - PublicInbox::GitHTTPBackend::serve($req->{env}, $git, $expath); -} - -1; diff --git a/lib/PublicInbox/RepobrowseGitLog.pm b/lib/PublicInbox/RepobrowseGitLog.pm deleted file mode 100644 index 85593cb8..00000000 --- a/lib/PublicInbox/RepobrowseGitLog.pm +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright (C) 2015 all contributors -# License: AGPL-3.0+ - -# show the log view -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); -use PublicInbox::Qspawn; -# cannot rely on --date=format-local:... yet, it is too new (September 2015) -use constant STATES => qw(h p D ai an s b); -use constant STATE_BODY => (scalar(STATES) - 1); -my $LOG_FMT = '--pretty=tformat:'. join('%n', map { "%$_" } STATES).'%x00'; - -sub parent_links { - if (@_ == 1) { # typical, single-parent commit - qq( / parent $_[0]); - } elsif (@_ > 0) { # merge commit - ' / parents ' . - join(' ', map { qq($_) } @_); - } else { - ''; # root commit - } -} - -sub flush_log_hdr ($$$) { - my ($req, $dst, $hdr) = @_; - my $rel = $req->{relcmd}; - my $seen = $req->{seen}; - $$dst .= '
    ' if scalar keys %$seen;
    -	my $id = $hdr->{h};
    -	$seen->{$id} = 1;
    -	$$dst .= qq();
    -	$$dst .= utf8_html($hdr->{'s'}); # FIXME may still OOM
    -	$$dst .= '';
    -	my $D = $hdr->{D}; # FIXME: thousands of decorations may OOM us
    -	if ($D ne '') {
    -		$$dst .= ' (' . join(', ', git_dec_links($rel, $D)) . ')';
    -	}
    -	my @p = split(/ /, $hdr->{p});
    -	push @{$req->{parents}}, @p;
    -	my $plinks = parent_links(@p);
    -	$$dst .= "\n- ";
    -	$$dst .= utf8_html($hdr->{an});
    -	$$dst .= " @ $hdr->{ai}\n  commit $id$plinks\n";
    -	undef
    -}
    -
    -sub git_log_sed_end ($$) {
    -	my ($req, $dst) = @_;
    -	$$dst .= '
    ';
    -	my $m = '';
    -	my $np = 0;
    -	my $seen = $req->{seen};
    -	my $git = $req->{repo_info}->{git};
    -	my $rel = $req->{relcmd};
    -	foreach my $p (@{$req->{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);
    -	}
    -	if ($np == 0) {
    -		$$dst .= "No commits follow";
    -	} elsif ($np > 1) {
    -		$$dst .= "Unseen parent commits to follow (multiple choice):\n";
    -	} else {
    -		$$dst .= "Next parent to follow:\n";
    -	}
    -	$$dst .= $m;
    -	$$dst .= '
    '; -} - -sub git_log_sed ($$) { - my ($self, $req) = @_; - my $buf = ''; - my $state = 0; - $req->{seen} = {}; - $req->{parents} = []; - my $hdr = {}; - sub { - my $dst; - # $_[0] == scalar buffer, undef means EOF from "git log" - $dst = delete $req->{lhtml} || ''; - my @tmp; - if (defined $_[0]) { - $buf .= $_[0]; - @tmp = split(/\n/, $buf, -1); - $buf = @tmp ? pop(@tmp) : ''; - } else { - @tmp = split(/\n/, $buf, -1); - $buf = undef; - } - - foreach my $l (@tmp) { - if ($state != STATE_BODY) { - $hdr->{((STATES)[$state])} = $l; - if (++$state == STATE_BODY) { - flush_log_hdr($req, \$dst, $hdr); - $hdr = {}; - } - next; - } - if ($l eq "\0") { - $dst .= qq(
    ); - $state = 0; - } else { - $dst .= "\n"; - $dst .= utf8_html($l); - } - } - git_log_sed_end($req, \$dst) unless defined $buf; - $dst; - }; -} - -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 $env = $req->{env}; - my $q = $req->{'q'} = PublicInbox::RepobrowseGitQuery->new($env); - my $h = $q->{h}; - $h eq '' and $h = 'HEAD'; - my $git = $repo_info->{git}; - my $cmd = $git->cmd(qw(log --no-notes --no-color --abbrev-commit), - $git->abbrev, $LOG_FMT, "-$max", $h, '--'); - my $rdr = { 2 => $git->err_begin }; - my $title = "log: $repo_info->{repo} (" . utf8_html($h). ')'; - $req->{lhtml} = $self->html_start($req, $title) . "\n\n"; - my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); - $qsp->psgi_return($env, undef, sub { - my ($r) = @_; - if (!defined $r) { - [ 500, [ 'Content-Type', 'text/html' ], [ $git->err ] ]; - } elsif ($r == 0) { - [ 404, [ 'Content-Type', 'text/html' ], [ $git->err ] ]; - } else { - $env->{'qspawn.filter'} = git_log_sed($self, $req); - [ 200, [ 'Content-Type', 'text/html' ] ]; - } - }); -} - -1; diff --git a/lib/PublicInbox/RepobrowseGitPatch.pm b/lib/PublicInbox/RepobrowseGitPatch.pm deleted file mode 100644 index 5ae768e8..00000000 --- a/lib/PublicInbox/RepobrowseGitPatch.pm +++ /dev/null @@ -1,39 +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); -use PublicInbox::Qspawn; - -# 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 $env = $req->{env}; - my $q = PublicInbox::RepobrowseGitQuery->new($env); - 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 = $git->cmd(@CMD, $sig." $range", $range, '--'); - my $expath = $req->{expath}; - push @$cmd, $expath if $expath ne ''; - - my $qsp = PublicInbox::Qspawn->new($cmd); - $qsp->psgi_return($env, undef, sub { - my ($r) = @_; - my $h = ['Content-Type', 'text/plain; charset=UTF-8']; - $r ? [ 200, $h ] : [ 500, $h, [ "format-patch error\n" ] ]; - }); -} - -1; diff --git a/lib/PublicInbox/RepobrowseGitPlain.pm b/lib/PublicInbox/RepobrowseGitPlain.pm deleted file mode 100644 index 24cc70b0..00000000 --- a/lib/PublicInbox/RepobrowseGitPlain.pm +++ /dev/null @@ -1,100 +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); -use PublicInbox::Qspawn; - -sub call_git_plain { - my ($self, $req) = @_; - my $git = $req->{repo_info}->{git}; - my $q = PublicInbox::RepobrowseGitQuery->new($req->{env}); - my $id = $q->{id}; - $id eq '' and $id = 'HEAD'; - $id .= ":$req->{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'; - } 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); -} - -sub git_tree_sed ($) { - my ($req) = @_; - my $buf = ''; - my $end; - my $pfx = $req->{tpfx}; - sub { # $_[0] = buffer or undef - my $dst = delete $req->{tstart} || ''; - my @files; - if (defined $_[0]) { - @files = split(/\0/, $buf .= $_[0]); - $buf = pop @files if scalar @files; - } else { - @files = split(/\0/, $buf); - $end = ''; - } - foreach my $n (@files) { - $n = PublicInbox::Hval->utf8($n); - my $ref = $n->as_path; - $dst .= qq(
  • ); - $dst .= $n->as_html; - $dst .= '
  • '; - } - $end ? $dst .= $end : $dst; - } -} - -# 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/"; - } - } - - $req->{tpfx} = $pfx; - $req->{tstart} = "$title".$t; - my $cmd = $git->cmd(qw(ls-tree --name-only -z), $git->abbrev, $hex); - my $rdr = { 2 => $git->err_begin }; - my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); - my $env = $req->{env}; - $qsp->psgi_return($env, undef, sub { - my ($r) = @_; - if (!defined $r) { - [ 500, [ 'Content-Type', 'text/plain' ], [ $git->err ]]; - } else { - $env->{'qspawn.filter'} = git_tree_sed($req); - [ 200, [ 'Content-Type', 'text/html' ] ]; - } - }); -} - -1; diff --git a/lib/PublicInbox/RepobrowseGitQuery.pm b/lib/PublicInbox/RepobrowseGitQuery.pm deleted file mode 100644 index 1140f6fa..00000000 --- a/lib/PublicInbox/RepobrowseGitQuery.pm +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (C) 2015 all contributors -# License: AGPL-3.0+ - -# query parameter management for repobrowse -package PublicInbox::RepobrowseGitQuery; -use strict; -use warnings; -use PublicInbox::Hval; -use URI::Escape qw(uri_unescape); -my @KNOWN_PARAMS = qw(id id2 h ofs); - -sub new { - my ($class, $env) = @_; - # we don't care about multi-value - my %tmp = map { - my ($k, $v) = split('=', uri_unescape($_), 2); - $v = '' unless defined $v; - $v =~ tr/+/ /; - ($k, $v) - } split(/[&;]/, $env->{QUERY_STRING}); - - my $self = {}; - foreach (@KNOWN_PARAMS) { - my $v = $tmp{$_}; - $self->{$_} = defined $v ? $v : ''; - } - bless $self, $class; -} - -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/RepobrowseGitSnapshot.pm b/lib/PublicInbox/RepobrowseGitSnapshot.pm deleted file mode 100644 index 450fdad6..00000000 --- a/lib/PublicInbox/RepobrowseGitSnapshot.pm +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (C) 2016 all contributors -# License: AGPL-3.0+ - -# shows the /snapshot/ endpoint for git repositories -# Mainly for compatibility reasons with cgit, I'm unsure if -# showing this in a repository viewer is a good idea. - -package PublicInbox::RepobrowseGitSnapshot; -use strict; -use warnings; -use base qw(PublicInbox::RepobrowseBase); -use PublicInbox::Git; -use PublicInbox::Qspawn; -our $SUFFIX; -BEGIN { - # as described in git-archive(1), users may add support for - # other compression schemes such as xz or bz2 via git-config(1): - # git config tar.tar.xz.command "xz -c" - # git config tar.tar.bz2.command "bzip2 -c" - chomp(my @l = `git archive --list`); - $SUFFIX = join('|', map { quotemeta $_ } @l); -} - -# Not using standard mime types since the compressed tarballs are -# special or do not match my /etc/mime.types. Choose what gitweb -# and cgit agree on for compatibility. -our %FMT_TYPES = ( - 'tar' => 'application/x-tar', - 'tar.bz2' => 'application/x-bzip2', - 'tar.gz' => 'application/x-gzip', - 'tar.xz' => 'application/x-xz', - 'tgz' => 'application/x-gzip', - 'zip' => 'application/x-zip', -); - -sub call_git_snapshot ($$) { # invoked by PublicInbox::RepobrowseBase::call - my ($self, $req) = @_; - - my @extra = @{$req->{extra}}; - my $ref = shift @extra; - return $self->r(404) if (!defined $ref) || scalar(@extra); - my $orig_fn = $ref; - - # just in case git changes refname rules, don't allow wonky filenames - # to break the Content-Disposition header, either. - return $self->r(404) if $orig_fn =~ /["\s]/s; - return $self->r(404) unless ($ref =~ s/\.($SUFFIX)\z//o); - my $fmt = $1; - my $env = $req->{env}; - my $repo_info = $req->{repo_info}; - - # support disabling certain snapshots types entirely to twart - # URL guessing since it could burn server resources. - return $self->r(404) if $repo_info->{snapshots_disabled}->{$fmt}; - - # strip optional basename (may not exist) - $ref =~ s/$repo_info->{snapshot_re}//; - - # don't allow option/command injection, git refs do not start with '-' - return $self->r(404) if $ref =~ /\A-/; - - my $git = $repo_info->{git}; - my $tree = ''; - my $last_cb = sub { - delete $env->{'repobrowse.tree_cb'}; - delete $env->{'qspawn.quiet'}; - my $pfx = "$repo_info->{snapshot_pfx}-$ref/"; - my $cmd = $git->cmd('archive', - "--prefix=$pfx", "--format=$fmt", $tree); - my $rdr = { 2 => $git->err_begin }; - my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); - $qsp->psgi_return($env, undef, sub { - my $r = $_[0]; - return $self->r(500) unless $r; - [ 200, [ 'Content-Type', - $FMT_TYPES{$fmt} || 'application/octet-stream', - 'Content-Disposition', - qq(inline; filename="$orig_fn"), - 'ETag', qq("$tree") ] ]; - }); - }; - - my $cmd = $git->cmd(qw(rev-parse --verify --revs-only)); - # try prefixing "v" or "V" for tag names to get the tree - my @refs = ("V$ref", "v$ref", $ref); - $env->{'qspawn.quiet'} = 1; - my $tree_cb = $env->{'repobrowse.tree_cb'} = sub { - my ($ref) = @_; - if (defined $ref) { - $tree = $$ref; - chomp $tree; - } - return $last_cb->() if $tree ne ''; - unless (scalar(@refs)) { - my $res = delete $env->{'qspawn.response'}; - return $res->($self->r(404)); - } - my $rdr = { 2 => $git->err_begin }; - my $r = pop @refs; - my $qsp = PublicInbox::Qspawn->new([@$cmd, $r], undef, $rdr); - $qsp->psgi_qx($env, undef, $env->{'repobrowse.tree_cb'}); - }; - sub { - $env->{'qspawn.response'} = $_[0]; - # kick off the "loop" foreach @refs - $tree_cb->(undef); - } -} - -1; diff --git a/lib/PublicInbox/RepobrowseGitSummary.pm b/lib/PublicInbox/RepobrowseGitSummary.pm deleted file mode 100644 index e7739ecd..00000000 --- a/lib/PublicInbox/RepobrowseGitSummary.pm +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (C) 2016 all contributors -# License: AGPL-3.0+ - -# The main summary/landing page of a git repository viewer -package PublicInbox::RepobrowseGitSummary; -use strict; -use warnings; -use PublicInbox::Hval qw(utf8_html); -use base qw(PublicInbox::RepobrowseBase); -use PublicInbox::Qspawn; - -sub call_git_summary { - my ($self, $req) = @_; - my $git = $req->{repo_info}->{git}; - my $env = $req->{env}; - - # n.b. we would use %(HEAD) in for-each-ref --format if we could - # rely on git 1.9.0+, but it's too soon for that in early 2017... - my $cmd = $git->cmd(qw(symbolic-ref HEAD)); - my $rdr = { 2 => $git->err_begin }; - my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); - sub { - my ($res) = @_; # Plack streaming callback - $qsp->psgi_qx($env, undef, sub { - chomp(my $head_ref = ${$_[0]}); - for_each_ref($self, $req, $res, $head_ref); - }); - } -} - -use constant EACH_REF_FMT => '--format=' . - join(' ', map { "%($_)" } - qw(refname objecttype objectname creatordate:short subject)); - -sub for_each_ref { - my ($self, $req, $res, $head_ref) = @_; - my $count = 10; # TODO: configurable - my $fh; - my $repo_info = $req->{repo_info}; - my $git = $repo_info->{git}; - my $refs = $git->popen(qw(for-each-ref --sort=-creatordate), - EACH_REF_FMT, "--count=$count", - qw(refs/heads/ refs/tags/)); - $fh = $res->([200, ['Content-Type'=>'text/html; charset=UTF-8']]); - # ref names are unpredictable in length and requires tables :< - $fh->write($self->html_start($req, - "$repo_info->{repo}: overview") . - '
    '); - - my $rel = $req->{relcmd}; - while (<$refs>) { - my ($ref, $type, $hex, $date, $s) = split(' ', $_, 5); - my $x = $ref eq $head_ref ? ' (HEAD)' : ''; - $ref =~ s!\Arefs/(?:heads|tags)/!!; - $ref = PublicInbox::Hval->utf8($ref); - my $h = $ref->as_html; - $ref = $ref->as_href; - my $sref; - if ($type eq 'tag') { - $h = "$h"; - $sref = $ref = $rel . 'tag?h=' . $ref; - } elsif ($type eq 'commit') { - $sref = $rel . 'commit?h=' . $ref; - $ref = $rel . 'log?h=' . $ref; - } else { - # no point in wasting code to support tagged - # trees/blobs... - next; - } - chomp $s; - $fh->write(qq() . - qq('); - - } - $fh->write('
    $h$x$date ) . utf8_html($s) . - '
    '); - - # some people will use README.md or even README.sh here... - my $readme = $repo_info->{readme}; - defined $readme or $readme = [ 'README', 'README.md' ]; - $readme = [ $readme ] if (ref($readme) ne 'ARRAY'); - foreach my $r (@$readme) { - my $doc = $git->cat_file('HEAD:'.$r); - defined $doc or next; - $fh->write('
    ' . readme_path_links($rel, $r) .
    -			" (HEAD)\n\n" . utf8_html($$doc) . '
    '); - } - $fh->write(''); - $fh->close; -} - -sub readme_path_links { - my ($rel, $readme) = @_; - my @path = split(m!/+!, $readme); - - my $s = "tree root/"; - my @t; - $s .= join('/', (map { - push @t, $_; - my $e = PublicInbox::Hval->utf8($_, join('/', @t)); - my $ep = $e->as_path; - my $eh = $e->as_html; - $e = "$eh"; - # bold the last one - scalar(@t) == scalar(@path) ? "$e" : $e; - } @path)); -} - -1; diff --git a/lib/PublicInbox/RepobrowseGitTag.pm b/lib/PublicInbox/RepobrowseGitTag.pm deleted file mode 100644 index 1d26eab0..00000000 --- a/lib/PublicInbox/RepobrowseGitTag.pm +++ /dev/null @@ -1,213 +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); -use PublicInbox::Qspawn; - -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::RepobrowseGitQuery->new($req->{env}); - my $h = $q->{h}; - $h eq '' and return git_tag_list($self, $req); - 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_each_tag_sed ($$) { - my ($self, $req) = @_; - my $repo_info = $req->{repo_info}; - my $buf = ''; - my $nr = 0; - $req->{thtml} = $self->html_start($req, "$repo_info->{repo}: tag list") . - '' . - join('', map { "" } qw(tag date subject)). - ''; - sub { - my $dst = delete $req->{thtml} || ''; - my $end = ''; - my @lines; - if (defined $_[0]) { - @lines = split(/\n/, $buf .= $_[0]); - $buf = pop @lines if @lines; - } else { # for-each-ref EOF - @lines = split(/\n/, $buf); - $buf = undef; - if ($nr == $req->{-tag_count}) { - $end = "
    Showing the latest $nr tags
    "; - } elsif ($nr == 0) { - $end = '
    no tags to show
    '; - } - $end = "
    $_
    $end"; - } - for (@lines) { - 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; - $dst .= qq() . - qq($h) . - qq($date) . - utf8_html($s) . ''; - } - $dst .= $end; - } -} - -sub git_tag_list { - my ($self, $req) = @_; - my $git = $req->{repo_info}->{git}; - - # TODO: use Xapian so we can more easily handle offsets/limits - # for pagination instead of limiting - my $count = $req->{-tag_count} = 50; - my $cmd = $git->cmd(qw(for-each-ref --sort=-creatordate), - '--format=%(refname) %(creatordate:short) %(subject)', - "--count=$count", 'refs/tags/'); - my $rdr = { 2 => $git->err_begin }; - my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); - my $env = $req->{env}; - $env->{'qspawn.quiet'} = 1; - $qsp->psgi_return($env, undef, sub { # parse output - my ($r) = @_; - if (!defined $r) { - my $errmsg = $git->err; - [ 500, [ 'Content-Type', 'text/html; charset=UTF-8'], - [ $errmsg ] ]; - } else { - $env->{'qspawn.filter'} = git_each_tag_sed($self, $req); - [ 200, [ 'Content-Type', 'text/html; charset=UTF-8' ]]; - } - }); -} - -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->{repo}: 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 c242fd1a..00000000 --- a/lib/PublicInbox/RepobrowseGitTree.pm +++ /dev/null @@ -1,220 +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); -use PublicInbox::Qspawn; - -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 'raw' link above"; - -sub call_git_tree { - my ($self, $req) = @_; - my @extra = @{$req->{extra}}; - my $git = $req->{repo_info}->{git}; - my $q = PublicInbox::RepobrowseGitQuery->new($req->{env}); - 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); - - unless (defined($type)) { - return [404, ['Content-Type'=>'text/plain'], ['Not Found']]; - } - - my $opts = { nofollow => 1 }; - my $title = $req->{expath}; - $title = $title eq '' ? 'tree' : utf8_html($title); - if ($type eq 'tree') { - $opts->{noindex} = 1; - $req->{thtml} = $self->html_start($req, $title, $opts) . "\n"; - git_tree_show($req, $hex, $q); - } elsif ($type eq 'blob') { - sub { - my $res = $_[0]; - my $fh = $res->([200, - ['Content-Type','text/html; charset=UTF-8']]); - $fh->write($self->html_start($req, $title, $opts) . - "\n"); - git_blob_show($req, $fh, $git, $hex, $q); - $fh->write(''); - $fh->close; - } - } else { - [404, ['Content-Type', 'text/plain; charset=UTF-8'], - ["Unrecognized type ($type) for $obj\n"]]; - } -} - -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{\npath: $t\n\nblob $hex}; - my $end = ''; - - $git->cat_file($hex, sub { - my ($cat, $left) = @_; # $$left == $size - $h .= qq{\t$$left bytes (raw)}; - $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_sed ($) { - my ($req) = @_; - my @lines; - my $buf = ''; - my $qs = $req->{qs}; - my $pfx = $req->{tpfx}; - my $end; - sub { - my $dst = delete $req->{thtml} || ''; - if (defined $_[0]) { - @lines = split(/\0/, $buf .= $_[0]); - $buf = pop @lines if @lines; - } else { - @lines = split(/\0/, $buf); - $end = ''; - } - for (@lines) { - my ($m, $x, $s, $path) = - (/\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 - $dst .= 'g' . (' ' x 15) . "$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 - $dst .= qq($m\t). - qq($s\t$path\n); - } - $dst; - } -} - -sub git_tree_show { - my ($req, $hex, $q) = @_; - my $git = $req->{repo_info}->{git}; - my $cmd = $git->cmd(qw(ls-tree -l -z), $git->abbrev, $hex); - my $rdr = { 2 => $git->err_begin }; - my $qsp = PublicInbox::Qspawn->new($cmd, undef, $rdr); - my $t = cur_path($req, $q); - my $pfx; - - $req->{thtml} .= "\npath: $t\n\nmode\tsize\tname\n"; - $req->{qs} = $q->qs; - if ($req->{tslash}) { - $pfx = './'; - } elsif (defined(my $last = $req->{extra}->[-1])) { - $pfx = PublicInbox::Hval->utf8($last)->as_path . '/'; - } else { - $pfx = 'tree/'; - } - $req->{tpfx} = $pfx; - my $env = $req->{env}; - $qsp->psgi_return($env, undef, sub { - my ($r) = @_; - if (defined $r) { - $env->{'qspawn.filter'} = git_tree_sed($req); - [ 200, [ 'Content-Type', 'text/html' ] ]; - } else { - [ 500, [ 'Content-Type', 'text/plain' ], [ $git->err ]]; - } - }); -} - -1; diff --git a/lib/PublicInbox/RepobrowseRoot.pm b/lib/PublicInbox/RepobrowseRoot.pm deleted file mode 100644 index fda96437..00000000 --- a/lib/PublicInbox/RepobrowseRoot.pm +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (C) 2016 all contributors -# License: AGPL-3.0+ - -# displays the root '/' where all the projects lie -package PublicInbox::RepobrowseRoot; -use strict; -use warnings; -use base qw(PublicInbox::RepobrowseBase); -use PublicInbox::Hval qw(utf8_html); - -sub call { - my ($self, $rconfig) = @_; - sub { - my ($res) = @_; # PSGI callback - my @h = ('Content-Type', 'text/html; charset=UTF-8'); - my $fh = $res->([200, \@h]); - repobrowse_index($fh, $rconfig); - $fh->close; - } -} - -sub repobrowse_index { - my ($fh, $rconfig) = @_; - my $title = 'repobrowse index'; - $fh->write("$title" . - PublicInbox::Hval::STYLE . - "
    $title");
    -
    -	# preload all groups
    -	foreach my $k (sort keys %$rconfig) {
    -		$k =~ /\Arepo\.(.+)\.path\z/ or next;
    -		my $repo_path = $1;
    -		$rconfig->lookup($repo_path); # insert into groups
    -	}
    -
    -	my $groups = $rconfig->{-groups};
    -	if (scalar(keys %$groups) > 2) { # default has '-none' + '-hidden'
    -		$fh->write("\n\nuncategorized
    ". - ""); - } else { - $fh->write(""); - } - foreach my $repo_path (sort @{$groups->{-none}}) { - my $r = $rconfig->lookup($repo_path); - my $p = PublicInbox::Hval->utf8($r->{repo}); - my $l = $p->as_html; - $p = $p->as_path; - $fh->write(qq($l) . - " $r->{desc_html}"); - } - - foreach my $group (keys %$groups) { - next if $group =~ /\A-(?:none|hidden)\z/; - my $g = utf8_html($group); - $fh->write("
     
    ". - "
    $g
    "); - foreach my $repo_path (sort @{$groups->{$group}}) { - my $r = $rconfig->lookup($repo_path); - my $p = PublicInbox::Hval->utf8($r->{repo}); - my $l = $p->as_html; - $p = $p->as_path; - $fh->write(' ' . - qq($l) . - " $r->{desc_html}"); - } - } - - $fh->write(''); -} - -1; diff --git a/t/repobrowse_git.t b/t/repobrowse_git.t index 6ae7475b..0ac977f3 100644 --- a/t/repobrowse_git.t +++ b/t/repobrowse_git.t @@ -3,7 +3,7 @@ use strict; use warnings; use Test::More; -use PublicInbox::RepobrowseGit qw(git_unquote); +use PublicInbox::RepoGit qw(git_unquote); is("foo\nbar", git_unquote('"foo\\nbar"'), 'unquoted newline'); is("Eléanor", git_unquote('"El\\303\\251anor"'), 'unquoted octal'); -- cgit v1.2.3-24-ge0c7