From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on dcvr.yhbt.net X-Spam-Level: X-Spam-ASN: X-Spam-Status: No, score=-4.2 required=3.0 tests=ALL_TRUSTED,BAYES_00, DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF shortcircuit=no autolearn=ham autolearn_force=no version=3.4.6 Received: from localhost (dcvr.yhbt.net [127.0.0.1]) by dcvr.yhbt.net (Postfix) with ESMTP id 674D11F47C for ; Tue, 3 Jan 2023 11:35:20 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=80x24.org; s=selector1; t=1672745720; bh=/jPYVGDIgQiP05NO3ZVp3GRh0/996J7PckluqqS2H2k=; h=From:To:Subject:Date:From; b=xUWi7fz0XD832Gx1I9uqwoY5hwh8W3bCCdvgmIthi80ECGJkzi2woEm+dptcWSV5t Sw6L2M+dctS71X6iJ90CFPr1ozPVj+11Gk5s0wvtP1e1yxapgLkZN/uZHYerD423u7 pE5+wbXHmqTn+Y/iWMau1bhlvOv26cUHS5yA1cpE= From: Eric Wong To: meta@public-inbox.org Subject: [PATCH] www_coderepo: implement /$CODE_REPO/atom/ endpoint Date: Tue, 3 Jan 2023 11:35:15 +0000 Message-Id: <20230103113515.1100917-1-e@80x24.org> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit List-Id: This should be similar or identical to what's in cgit; and tie into the rest of the www_coderepo stuff. --- MANIFEST | 1 + lib/PublicInbox/GzipFilter.pm | 5 +- lib/PublicInbox/Qspawn.pm | 13 +++-- lib/PublicInbox/RepoAtom.pm | 87 ++++++++++++++++++++++++++++++++++ lib/PublicInbox/WwwCoderepo.pm | 6 +++ t/solver_git.t | 17 +++++++ 6 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 lib/PublicInbox/RepoAtom.pm diff --git a/MANIFEST b/MANIFEST index 37357663..565317ce 100644 --- a/MANIFEST +++ b/MANIFEST @@ -307,6 +307,7 @@ lib/PublicInbox/PktOp.pm lib/PublicInbox/ProcessPipe.pm lib/PublicInbox/Qspawn.pm lib/PublicInbox/Reply.pm +lib/PublicInbox/RepoAtom.pm lib/PublicInbox/RepoSnapshot.pm lib/PublicInbox/SaPlugin/ListMirror.pm lib/PublicInbox/SaPlugin/ListMirror.pod diff --git a/lib/PublicInbox/GzipFilter.pm b/lib/PublicInbox/GzipFilter.pm index bd72afff..a11ba73f 100644 --- a/lib/PublicInbox/GzipFilter.pm +++ b/lib/PublicInbox/GzipFilter.pm @@ -123,8 +123,9 @@ sub http_out ($) { } sub write { + my $self = shift; # my $ret = bytes::length($_[1]); # XXX does anybody care? - http_out($_[0])->write(translate(@_)); + http_out($self)->write($self->translate(@_)); } sub zfh { @@ -166,7 +167,7 @@ sub zflush ($;@) { sub close { my ($self) = @_; my $http_out = http_out($self) // return; - $http_out->write(zflush($self)); + $http_out->write($self->zflush); (delete($self->{http_out}) // return)->close; } diff --git a/lib/PublicInbox/Qspawn.pm b/lib/PublicInbox/Qspawn.pm index 297a284f..ab0ef25c 100644 --- a/lib/PublicInbox/Qspawn.pm +++ b/lib/PublicInbox/Qspawn.pm @@ -40,7 +40,7 @@ my $def_limiter; # $cmd_env is the environ for the child process (not PSGI env) # $opt can include redirects and perhaps other process spawning options # {qsp_err} is an optional error buffer callers may access themselves -sub new ($$$;) { +sub new { my ($class, $cmd, $cmd_env, $opt) = @_; bless { args => [ $cmd, $cmd_env, $opt ] }, $class; } @@ -106,6 +106,7 @@ sub finalize ($$) { return unless $@; warn "E: $@"; # hope qspawn.wcb can handle it } + return if $self->{passed}; # another command chained it if (my $wcb = delete $env->{'qspawn.wcb'}) { # have we started writing, yet? require PublicInbox::WwwStatic; @@ -225,7 +226,12 @@ sub psgi_return_init_cb { my ($self) = @_; my $r = rd_hdr($self) or return; my $env = $self->{psgi_env}; - my $filter = delete($env->{'qspawn.filter'}) // (ref($r) eq 'ARRAY' ? + my $filter; + if (ref($r) eq 'ARRAY' && Scalar::Util::blessed($r->[2]) && + $r->[2]->can('attach')) { + $filter = pop @$r; + } + $filter //= delete($env->{'qspawn.filter'}) // (ref($r) eq 'ARRAY' ? PublicInbox::GzipFilter::qsp_maybe($r->[1], $env) : undef); my $wcb = delete $env->{'qspawn.wcb'}; @@ -241,7 +247,8 @@ sub psgi_return_init_cb { if (ref($r) eq 'ARRAY') { # error $wcb->($r) } elsif (ref($r) eq 'CODE') { # chain another command - $r->($wcb) + $r->($wcb); + $self->{passed} = 1; } # else do nothing } elsif ($async) { diff --git a/lib/PublicInbox/RepoAtom.pm b/lib/PublicInbox/RepoAtom.pm new file mode 100644 index 00000000..66f12157 --- /dev/null +++ b/lib/PublicInbox/RepoAtom.pm @@ -0,0 +1,87 @@ +# Copyright (C) all contributors +# License: AGPL-3.0+ +# +# git log => Atom feed (cgit-compatible: $REPO/atom/[PATH]?h=$tip +package PublicInbox::RepoAtom; +use v5.12; +use parent qw(PublicInbox::GzipFilter); +use POSIX qw(strftime); +use URI::Escape qw(uri_escape); +use Scalar::Util (); +use PublicInbox::Hval qw(ascii_html); + +my $ATOM_FMT = '--pretty=tformat:'.join('%n', + map { "%$_" } qw(H ct an ae at s b)).'%x00'; + +sub log2atom_ok { # parse_hdr for qspawn + my ($r, $bref, $ctx) = @_; + return [ 404, [], [ "Not Found\n"] ] if $r == 0; + bless $ctx, __PACKAGE__; + my $h = [ 'Content-Type' => 'application/atom+xml; charset=UTF-8' ]; + $ctx->{gz} = $ctx->can('gz_or_noop')->($h, $ctx->{env}); + my $title = ascii_html(delete $ctx->{-feed_title}); + my $desc = ascii_html($ctx->{git}->description); + my $url = ascii_html($ctx->{git}->base_url($ctx->{env})); + $ctx->{-base_url} = $url; + $ctx->zmore(< + +$title$desc +EOM + [ 200, $h, $ctx ]; # [2] is qspawn.filter +} + +# called by GzipFilter->close +sub zflush { $_[0]->SUPER::zflush('') } + +# called by GzipFilter->write or GetlineBody->getline +sub translate { + my $self = shift; + my $rec = $_[0] // return $self->zflush; # getline + my @out; + my $lbuf = delete($self->{lbuf}) // shift; + $lbuf .= shift if @_; + while ($lbuf =~ s/\A([^\0]+)\0\n//s) { + my $ent = $1; + utf8::decode($ent); + $ent = ascii_html($ent); + my ($H, $ct, $an, $ae, $at, $s, $bdy) = split(/\n/, $ent, 7); + undef $ent; + $bdy //= ''; + $_ = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($_)) for ($ct, $at); + + push @out, <<"", $bdy, '' +$s$ct$an +$ae$at$H
+
+	}
+	$self->{lbuf} = $lbuf;
+	chomp @out;
+	$self->SUPER::translate(@out);
+}
+
+sub srv_atom {
+	my ($ctx, $path) = @_;
+	return if index($path, '//') >= 0 || index($path, '/') == 0;
+	my $max = 50; # TODO configurable
+	my @cmd = ('git', "--git-dir=$ctx->{git}->{git_dir}",
+			qw(log --no-notes --no-color --no-abbrev),
+			$ATOM_FMT, "-$max");
+	my $tip = $ctx->{qp}->{h}; # same as cgit
+	$ctx->{-feed_title} = $ctx->{git}->{nick};
+	if (defined($tip)) {
+		push @cmd, $tip;
+		$ctx->{-feed_title} .= ", $tip";
+	}
+	# else: let git decide based on HEAD if $tip isn't defined
+	push @cmd, '--';
+	push @cmd, $path if $path ne '';
+	my $qsp = PublicInbox::Qspawn->new(\@cmd);
+	$qsp->psgi_return($ctx->{env}, undef, \&log2atom_ok, $ctx);
+}
+
+1;
diff --git a/lib/PublicInbox/WwwCoderepo.pm b/lib/PublicInbox/WwwCoderepo.pm
index 1a8754c4..3c929222 100644
--- a/lib/PublicInbox/WwwCoderepo.pm
+++ b/lib/PublicInbox/WwwCoderepo.pm
@@ -16,6 +16,7 @@ use PublicInbox::GitAsyncCat;
 use PublicInbox::WwwStream;
 use PublicInbox::Hval qw(ascii_html);
 use PublicInbox::RepoSnapshot;
+use PublicInbox::RepoAtom;
 
 my $EACH_REF = "git for-each-ref --sort=-creatordate --format='%(HEAD)%00".
 	join('%00', map { "%($_)" }
@@ -227,6 +228,11 @@ sub srv { # endpoint called by PublicInbox::WWW
 		return PublicInbox::RepoSnapshot::srv($ctx, $2) // r(404);
 	}
 
+	if ($path_info =~ m!\A/(.+?)/atom/(.*)\z! and
+			($ctx->{git} = $self->{"\0$1"})) {
+		return PublicInbox::RepoAtom::srv_atom($ctx, $2) // r(404);
+	}
+
 	# enforce trailing slash:
 	if ($path_info =~ m!\A/(.+?)\z! and ($git = $self->{"\0$1"})) {
 		my $qs = $ctx->{env}->{QUERY_STRING};
diff --git a/t/solver_git.t b/t/solver_git.t
index 82222031..89ed0362 100644
--- a/t/solver_git.t
+++ b/t/solver_git.t
@@ -364,6 +364,23 @@ EOF
 		$fn = 'public-inbox-1.0.0.tar.bz2';
 		$res = $cb->(GET("/public-inbox/snapshot/$fn"));
 		is($res->code, 404, '404 on unconfigured snapshot format');
+
+		$res = $cb->(GET('/public-inbox/atom/'));
+		is($res->code, 200, 'Atom feed');
+		SKIP: {
+			require_mods('XML::TreePP', 1);
+			my $t = XML::TreePP->new->parse($res->content);
+			is(scalar @{$t->{feed}->{entry}}, 50,
+				'got 50 entries');
+
+			$res = $cb->(GET('/public-inbox/atom/COPYING'));
+			is($res->code, 200, 'file Atom feed');
+			$t = XML::TreePP->new->parse($res->content);
+			ok($t->{feed}->{entry}, 'got entry');
+
+			$res = $cb->(GET('/public-inbox/atom/README.md'));
+			is($res->code, 404, '404 on non-existent file Atom feed');
+		}
 	};
 	test_psgi(sub { $www->call(@_) }, $client);
 	my $env = { PI_CONFIG => $cfgpath, TMPDIR => $tmpdir };