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
}
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 {
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;
}
# $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;
}
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;
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'};
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) {
--- /dev/null
+# 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;
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 { "%($_)" }
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};
$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 };