# 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(\n parent $_[0]); } elsif (@_ > 0) { # merge commit "\n parents " . join("\n ", map { qq($_) } @_); } else { ''; # root commit } } sub flush_log_hdr ($$$) { my ($req, $dst, $hdr) = @_; my $lpfx = $req->{lpfx}; 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($lpfx, $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}->{git};
	my $lpfx = $req->{lpfx};
	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 = $req->{-repo}; my $max = $repo->{max_commit_count} || 50; my $tip = $req->{tip} || $repo->tip; $req->{lpfx} = $req->{relcmd}; $max = int($max); $max = 50 if $max == 0; my $env = $req->{env}; my $git = $repo->{git}; my $cmd = $git->cmd(qw(log --no-notes --no-color --no-abbrev), $LOG_FMT, "-$max", $tip, '--'); my $rdr = { 2 => $git->err_begin }; my $title = 'log: '.$repo->{repo}.' ('.utf8_html($tip).')'; $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;