]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/RepoAtom.pm
No ext_urls
[public-inbox.git] / lib / PublicInbox / RepoAtom.pm
1 # Copyright (C) all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3 #
4 # git log => Atom feed (cgit-compatible: $REPO/atom/[PATH]?h=$tip
5 package PublicInbox::RepoAtom;
6 use v5.12;
7 use parent qw(PublicInbox::GzipFilter);
8 use POSIX qw(strftime);
9 use URI::Escape qw(uri_escape);
10 use Scalar::Util ();
11 use PublicInbox::Hval qw(ascii_html);
12
13 # git for-each-ref and log use different format fields :<
14 my $ATOM_FMT = '--pretty=tformat:'.join('%n',
15                                 map { "%$_" } qw(H ct an ae at s b)).'%x00';
16
17 my $EACH_REF_FMT = '--format='.join(';', map { "\$r{'$_'}=%($_)" } qw(
18         objectname refname:short creator contents:subject contents:body
19         *subject *body)).'%00';
20
21 sub atom_ok { # parse_hdr for qspawn
22         my ($r, $bref, $ctx) = @_;
23         return [ 404, [], [ "Not Found\n"] ] if $r == 0;
24         bless $ctx, __PACKAGE__;
25         my $h = [ 'Content-Type' => 'application/atom+xml; charset=UTF-8' ];
26         $ctx->{gz} = $ctx->can('gz_or_noop')->($h, $ctx->{env});
27         my $title = ascii_html(delete $ctx->{-feed_title});
28         my $desc = ascii_html($ctx->{git}->description);
29         my $url = ascii_html($ctx->{git}->base_url($ctx->{env}));
30         $ctx->{-base_url} = $url;
31         $ctx->zmore(<<EOM);
32 <?xml version="1.0"?>
33 <feed xmlns="http://www.w3.org/2005/Atom">
34 <title>$title</title><subtitle>$desc</subtitle><link
35 rel="alternate" type="text/html" href="$url"/>
36 EOM
37         [ 200, $h, $ctx ]; # [2] is qspawn.filter
38 }
39
40 # called by GzipFilter->close
41 sub zflush { $_[0]->SUPER::zflush('</feed>') }
42
43 # called by GzipFilter->write or GetlineBody->getline
44 sub translate {
45         my $self = shift;
46         my $rec = $_[0] // return $self->zflush; # getline
47         my @out;
48         my $lbuf = delete($self->{lbuf}) // shift;
49         $lbuf .= shift while @_;
50         my $is_tag = $self->{-is_tag};
51         my ($H, $ct, $an, $ae, $at, $s, $bdy);
52         while ($lbuf =~ s/\A([^\0]+)\0\n//s) {
53                 utf8::decode($bdy = $1);
54                 if ($is_tag) {
55                         my %r;
56                         eval "$bdy";
57                         for (qw(contents:subject contents:body)) {
58                                 $r{$_} =~ /\S/ or delete($r{$_})
59                         }
60                         $H = $r{objectname};
61                         $s = $r{'contents:subject'} // $r{'*subject'};
62                         $bdy = $r{'contents:body'} // $r{'*body'};
63                         $s .= " ($r{'refname:short'})";
64                         $_ = ascii_html($_) for ($s, $bdy, $r{creator});
65                         ($an, $ae, $at) = split(/\s*&[gl]t;\s*/, $r{creator});
66                         $at =~ s/ .*\z//; # no TZ
67                         $ct = $at = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($at));
68                 } else {
69                         $bdy = ascii_html($bdy);
70                         ($H, $ct, $an, $ae, $at, $s, $bdy) =
71                                                         split(/\n/, $bdy, 7);
72                         $at = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($at));
73                         $ct = strftime('%Y-%m-%dT%H:%M:%SZ', gmtime($ct));
74                 }
75                 $bdy //= '';
76                 push @out, <<"";
77 <entry><title>$s</title><updated>$ct</updated><author><name>$an</name>
78 <email>$ae</email></author><published>$at</published><link
79 rel="alternate" type="text/html" href="$self->{-base_url}$H/s/"
80 /><id>$H</id>
81
82                 push @out, <<'', $bdy, '</pre></div></content>' if $bdy ne '';
83 <content type="xhtml"><div
84 xmlns="http://www.w3.org/1999/xhtml"><pre style="white-space:pre-wrap">
85
86                 push @out, '</entry>';
87         }
88         $self->{lbuf} = $lbuf;
89         chomp @out;
90         $self->SUPER::translate(@out);
91 }
92
93 # $REPO/tags.atom endpoint
94 sub srv_tags_atom {
95         my ($ctx) = @_;
96         my $max = 50; # TODO configurable
97         my @cmd = ('git', "--git-dir=$ctx->{git}->{git_dir}",
98                         qw(for-each-ref --sort=-creatordate), "--count=$max",
99                         '--perl', $EACH_REF_FMT, 'refs/tags');
100         $ctx->{-feed_title} = "$ctx->{git}->{nick} tags";
101         my $qsp = PublicInbox::Qspawn->new(\@cmd);
102         $ctx->{-is_tag} = 1;
103         $qsp->psgi_return($ctx->{env}, undef, \&atom_ok, $ctx);
104 }
105
106 sub srv_atom {
107         my ($ctx, $path) = @_;
108         return if index($path, '//') >= 0 || index($path, '/') == 0;
109         my $max = 50; # TODO configurable
110         my @cmd = ('git', "--git-dir=$ctx->{git}->{git_dir}",
111                         qw(log --no-notes --no-color --no-abbrev),
112                         $ATOM_FMT, "-$max");
113         my $tip = $ctx->{qp}->{h}; # same as cgit
114         $ctx->{-feed_title} = $ctx->{git}->{nick};
115         $ctx->{-feed_title} .= " $path" if $path ne '';
116         if (defined($tip)) {
117                 push @cmd, $tip;
118                 $ctx->{-feed_title} .= ", $tip";
119         }
120         # else: let git decide based on HEAD if $tip isn't defined
121         push @cmd, '--';
122         push @cmd, $path if $path ne '';
123         my $qsp = PublicInbox::Qspawn->new(\@cmd, undef,
124                                         { quiet => 1, 2 => $ctx->{lh} });
125         $qsp->psgi_return($ctx->{env}, undef, \&atom_ok, $ctx);
126 }
127
128 1;