user/dev discussion of public-inbox itself
 help / color / mirror / code / Atom feed
From: Eric Wong <e@80x24.org>
To: meta@public-inbox.org
Subject: [PATCH] www_coderepo: implement /$CODE_REPO/atom/ endpoint
Date: Tue,  3 Jan 2023 11:35:15 +0000	[thread overview]
Message-ID: <20230103113515.1100917-1-e@80x24.org> (raw)

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 <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+#
+# 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(<<EOM);
+<?xml version="1.0"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+<title>$title</title><subtitle>$desc</subtitle><link
+rel="alternate" type="text/html" href="$url"/>
+EOM
+	[ 200, $h, $ctx ]; # [2] is qspawn.filter
+}
+
+# called by GzipFilter->close
+sub zflush { $_[0]->SUPER::zflush('</feed>') }
+
+# 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, '</pre></div></content></entry>'
+<entry><title>$s</title><updated>$ct</updated><author><name>$an</name>
+<email>$ae</email></author><published>$at</published><link
+rel="alternate" type="text/html" href="$self->{-base_url}$H/s/"
+/><id>$H</id><content type="xhtml"><div
+xmlns="http://www.w3.org/1999/xhtml"><pre style="white-space:pre-wrap">
+
+	}
+	$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 };

                 reply	other threads:[~2023-01-03 11:35 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://public-inbox.org/README

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20230103113515.1100917-1-e@80x24.org \
    --to=e@80x24.org \
    --cc=meta@public-inbox.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://80x24.org/public-inbox.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).