# 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 $repo = $req->{-repo}; my $git = $repo->{git}; my $tip = $req->{tip} || $repo->tip; my $obj = "$tip:$req->{expath}"; sub { my ($res) = @_; $git->check_async($req->{env}, $obj, sub { my ($info) = @_; my ($hex, $type, $size) = @$info; unless (defined $type) { return $res->([404, ['Content-Type','text/plain'], ['Not Found']]); } show_tree($self, $req, $res, $hex, $type, $size); }); } } sub show_tree { my ($self, $req, $res, $hex, $type, $size) = @_; my $opts = { nofollow => 1 }; my $title = "tree: ".utf8_html($req->{expath}); if ($type eq 'tree') { $opts->{noindex} = 1; $req->{thtml} = $self->html_start($req, $title, $opts) . "\n"; git_tree_show($req, $res, $hex); } elsif ($type eq 'blob') { 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,$hex); $fh->write(''); $fh->close; } else { $res->([404, ['Content-Type', 'text/plain; charset=UTF-8'], ["Unrecognized type ($type) for $hex\n"]]); } } sub cur_path { my ($req) = @_; my @ex = @{$req->{extra}} or return 'root'; my $s; my $tip = $req->{-repo}->tip; 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, $hex) = @_; # 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 $git = $req->{-repo}->{git}; my $rel = $req->{relcmd}; my $raw = join('/', "${rel}raw", $req->{-repo}->tip, @{$req->{extra}}); $raw = PublicInbox::Hval->utf8($raw)->as_path; my $t = cur_path($req); my $s = qq{\npath: $t\n\nblob $hex}; my $end = ''; $git->cat_file($hex, sub { my ($cat, $left) = @_; # $$left == $size $s .= 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("$s\n$BINARY_MSG"); return; } $fh->write($s."
");
		$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 $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; # 'raw' 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, $res, $hex) = @_; my $git = $req->{-repo}->{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); my $pfx; $req->{thtml} .= "\npath: $t\n\nmode\tsize\tname\n"; if (defined(my $last = $req->{extra}->[-1])) { $pfx = PublicInbox::Hval->utf8($last)->as_path; } elsif (defined(my $tip = $req->{tip})) { $pfx = $tip; } else { $pfx = 'tree/' . $req->{-repo}->tip; } $req->{tpfx} = $pfx; my $env = $req->{env}; $env->{'qspawn.response'} = $res; $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;