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 820BD1F5A4 for ; Sat, 28 Jan 2023 11:03:23 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=80x24.org; s=selector1; t=1674903803; bh=9qytKiCD3k7TvzUi/OZ9c0um1Sq0KykoLzn3Rg+p15o=; h=From:To:Subject:Date:In-Reply-To:References:From; b=R/AVNu1mUnJSDB8uQWosvwqXzT8opmVulYvq76qAAFzpVf0aUrKpEAwEOPuA62tyY +nFOzC+jUcI3CGhkuYM3DzCU8ajJkHIVxWUhGeCqrN3aYaaCClH7bUT6MgD2U2umWG xCCRLwm1d/TOI4feTmJ/PFIYpHMtYuvdF0gN42Us= From: Eric Wong To: meta@public-inbox.org Subject: [PATCH 2/7] www_coderepo: support /$REPO/tags.atom endpoint Date: Sat, 28 Jan 2023 11:02:50 +0000 Message-Id: <20230128110255.2950084-3-e@80x24.org> In-Reply-To: <20230128110255.2950084-1-e@80x24.org> References: <20230128110255.2950084-1-e@80x24.org> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit List-Id: Providing an Atom feed for tags can be a nice way for users to subscribe to new releases without excessive noise. --- lib/PublicInbox/RepoAtom.pm | 62 ++++++++++++++++++++++++++++------ lib/PublicInbox/WwwCoderepo.pm | 3 ++ t/solver_git.t | 8 +++++ 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/lib/PublicInbox/RepoAtom.pm b/lib/PublicInbox/RepoAtom.pm index 66f12157..4a013147 100644 --- a/lib/PublicInbox/RepoAtom.pm +++ b/lib/PublicInbox/RepoAtom.pm @@ -10,10 +10,15 @@ use URI::Escape qw(uri_escape); use Scalar::Util (); use PublicInbox::Hval qw(ascii_html); +# git for-each-ref and log use different format fields :< 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 $EACH_REF_FMT = '--format='.join(';', map { "\$r{'$_'}=%($_)" } qw( + objectname refname:short creator contents:subject contents:body + *subject *body)).'%00'; + +sub atom_ok { # parse_hdr for qspawn my ($r, $bref, $ctx) = @_; return [ 404, [], [ "Not Found\n"] ] if $r == 0; bless $ctx, __PACKAGE__; @@ -42,28 +47,62 @@ sub translate { my @out; my $lbuf = delete($self->{lbuf}) // shift; $lbuf .= shift if @_; + my $is_tag = $self->{-is_tag}; + my ($H, $ct, $an, $ae, $at, $s, $bdy); 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; + utf8::decode($bdy = $1); + if ($is_tag) { + my %r; + eval "$bdy"; + for (qw(contents:subject contents:body)) { + $r{$_} =~ /\S/ or delete($r{$_}) + } + $H = $r{objectname}; + $s = $r{'contents:subject'} // $r{'*subject'}; + $bdy = $r{'contents:body'} // $r{'*body'}; + $s .= " ($r{'refname:short'})"; + $_ = ascii_html($_) for ($s, $bdy, $r{creator}); + ($an, $ae, $at) = split(/\s*&[gl]t;\s*/, $r{creator}); + $at =~ s/ .*\z//; # no TZ + $ct = $at = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($at)); + } else { + $bdy = ascii_html($bdy); + ($H, $ct, $an, $ae, $at, $s, $bdy) = + split(/\n/, $bdy, 7); + $at = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($at)); + $ct = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($ct)); + } $bdy //= ''; - $_ = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($_)) for ($ct, $at); - - push @out, <<"", $bdy, '' + push @out, <<""; $s$ct$an $ae$at$H
$H + + push @out, <<'', $bdy, '
' if $bdy ne ''; +
 
+		push @out, '';
 	}
 	$self->{lbuf} = $lbuf;
 	chomp @out;
 	$self->SUPER::translate(@out);
 }
 
+# $REPO/tags.atom endpoint
+sub srv_tags_atom {
+	my ($ctx) = @_;
+	my $max = 50; # TODO configurable
+	my @cmd = ('git', "--git-dir=$ctx->{git}->{git_dir}",
+			qw(for-each-ref --sort=-creatordate), "--count=$max",
+			'--perl', $EACH_REF_FMT, 'refs/tags');
+	$ctx->{-feed_title} = "$ctx->{git}->{nick} tags";
+	my $qsp = PublicInbox::Qspawn->new(\@cmd);
+	$ctx->{-is_tag} = 1;
+	$qsp->psgi_return($ctx->{env}, undef, \&atom_ok, $ctx);
+}
+
 sub srv_atom {
 	my ($ctx, $path) = @_;
 	return if index($path, '//') >= 0 || index($path, '/') == 0;
@@ -73,6 +112,7 @@ sub srv_atom {
 			$ATOM_FMT, "-$max");
 	my $tip = $ctx->{qp}->{h}; # same as cgit
 	$ctx->{-feed_title} = $ctx->{git}->{nick};
+	$ctx->{-feed_title} .= " $path" if $path ne '';
 	if (defined($tip)) {
 		push @cmd, $tip;
 		$ctx->{-feed_title} .= ", $tip";
@@ -81,7 +121,7 @@ sub srv_atom {
 	push @cmd, '--';
 	push @cmd, $path if $path ne '';
 	my $qsp = PublicInbox::Qspawn->new(\@cmd);
-	$qsp->psgi_return($ctx->{env}, undef, \&log2atom_ok, $ctx);
+	$qsp->psgi_return($ctx->{env}, undef, \&atom_ok, $ctx);
 }
 
 1;
diff --git a/lib/PublicInbox/WwwCoderepo.pm b/lib/PublicInbox/WwwCoderepo.pm
index 8dcd9772..e3d45c56 100644
--- a/lib/PublicInbox/WwwCoderepo.pm
+++ b/lib/PublicInbox/WwwCoderepo.pm
@@ -240,6 +240,9 @@ sub srv { # endpoint called by PublicInbox::WWW
 	} elsif ($path_info =~ m!\A/(.+?)/atom/(.*)\z! and
 			($ctx->{git} = $cr->{$1})) {
 		PublicInbox::RepoAtom::srv_atom($ctx, $2) // r(404);
+	} elsif ($path_info =~ m!\A/(.+?)/tags\.atom\z! and
+			($ctx->{git} = $cr->{$1})) {
+		PublicInbox::RepoAtom::srv_tags_atom($ctx);
 	} elsif ($path_info =~ m!\A/(.+?)\z! and ($git = $cr->{$1})) {
 		my $qs = $ctx->{env}->{QUERY_STRING};
 		my $url = $git->base_url($ctx->{env});
diff --git a/t/solver_git.t b/t/solver_git.t
index 7743913b..0090bc06 100644
--- a/t/solver_git.t
+++ b/t/solver_git.t
@@ -403,6 +403,14 @@ EOF
 		is($res->code, 404, 'got 404 for non-existent ref README');
 		$res = $cb->(GET('/public-inbox/tree/Documentation?h=no-dir'));
 		is($res->code, 404, 'got 404 for non-existent ref directory');
+
+		$res = $cb->(GET('/public-inbox/tags.atom'));
+		is($res->code, 200, 'Atom feed');
+		SKIP: {
+			require_mods('XML::TreePP', 1);
+			my $t = XML::TreePP->new->parse($res->content);
+			ok(scalar @{$t->{feed}->{entry}}, 'got tag entries');
+		}
 	};
 	test_psgi(sub { $www->call(@_) }, $client);
 	my $env = { PI_CONFIG => $cfgpath, TMPDIR => $tmpdir };