Fuck that shit
[catagits/Gitalist.git] / lib / Gitalist / Controller / Root.pm
CommitLineData
89de6a33 1package Gitalist::Controller::Root;
89de6a33 2
c113db92 3use Moose;
cce8c9b6 4use Moose::Autobox;
f796a861 5use Sys::Hostname ();
d17ce39c 6use XML::Atom::Feed;
7use XML::Atom::Entry;
f796a861 8use XML::RSS;
286cea09 9use XML::OPML::SimpleGen;
d17ce39c 10
c113db92 11use Gitalist::Utils qw/ age_string /;
89de6a33 12
c113db92 13use namespace::autoclean;
89de6a33 14
c113db92 15BEGIN { extends 'Catalyst::Controller' }
89de6a33 16
c113db92 17__PACKAGE__->config->{namespace} = '';
89de6a33 18
c113db92 19sub root : Chained('/') PathPart('') CaptureArgs(0) {}
89de6a33 20
832cbc81 21sub _get_object {
b4b4d0fd 22 my($self, $c, $haveh) = @_;
9dc3b9a5 23
c1f608c8 24 my $h = $haveh || $c->req->param('h') || '';
0ee97fec 25 my $f = $c->req->param('f');
8dbe8024 26
87581f05 27 my $m = $c->stash->{Repository};
1aad4e81 28 my $pd = $m->path;
0ee97fec 29
9dc3b9a5 30 # Either use the provided h(ash) parameter, the f(ile) parameter or just use HEAD.
a7cc1ede 31 my $hash = ($h =~ /[^a-f0-9]/ ? $m->head_hash($h) : $h)
32 || ($f && $m->hash_by_path($f))
33 || $m->head_hash
9dc3b9a5 34 # XXX This could definitely use more context.
35 || Carp::croak("Couldn't find a hash for the commit object!");
36
c046a52f 37 my $obj = $m->get_object($hash)
38 or Carp::croak("Couldn't find a object for '$hash' in '$pd'!");
9dc3b9a5 39
c046a52f 40 return $obj;
9dc3b9a5 41}
42
5bb401c6 43sub index : Chained('base') PathPart('') Args(0) {
7bc165b3 44 my ( $self, $c ) = @_;
45
46 $c->detach($c->req->param('a'))
47 if $c->req->param('a');
48
82bc0f05 49 my @list = @{ $c->model()->repositories };
50 die 'No repositories found in '. $c->model->repo_dir
7bc165b3 51 unless @list;
52
53 my $search = $c->req->param('s') || '';
54 if($search) {
55 @list = grep {
56 index($_->name, $search) > -1
57 or ( $_->description !~ /^Unnamed repository/ and index($_->description, $search) > -1 )
58 } @list
59 }
60
61 $c->stash(
62 search_text => $search,
82bc0f05 63 repositories => \@list,
7bc165b3 64 action => 'index',
65 );
04d1d917 66}
67
790ce598 68=head2 summary
69
70A summary of what's happening in the repo.
71
72=cut
73
5bb401c6 74sub summary : Chained('base') Args(0) {
790ce598 75 my ( $self, $c ) = @_;
82bc0f05 76 my $repository = $c->stash->{Repository};
77 $c->detach('error_404') unless $repository;
832cbc81 78 my $commit = $self->_get_object($c);
82bc0f05 79 my @heads = @{$repository->heads};
06281e11 80 my $maxitems = Gitalist->config->{paging}{summary} || 10;
790ce598 81 $c->stash(
82 commit => $commit,
82bc0f05 83 log_lines => [$repository->list_revs(
4111e151 84 sha1 => $commit->sha1,
06281e11 85 count => $maxitems,
fde5091f 86 )],
82bc0f05 87 refs => $repository->references,
0969b411 88 heads => [ @heads[0 .. ($#heads < $maxitems ? $#heads : $maxitems)] ],
790ce598 89 action => 'summary',
06281e11 90 );
790ce598 91}
92
93=head2 heads
94
95The current list of heads (aka branches) in the repo.
96
97=cut
98
5bb401c6 99sub heads : Chained('base') Args(0) {
790ce598 100 my ( $self, $c ) = @_;
82bc0f05 101 my $repository = $c->stash->{Repository};
790ce598 102 $c->stash(
832cbc81 103 commit => $self->_get_object($c),
82bc0f05 104 heads => $repository->heads,
790ce598 105 action => 'heads',
106 );
107}
108
ea19a20c 109=head2 tags
110
111The current list of tags in the repo.
112
113=cut
114
5bb401c6 115sub tags : Chained('base') Args(0) {
ea19a20c 116 my ( $self, $c ) = @_;
82bc0f05 117 my $repository = $c->stash->{Repository};
ea19a20c 118 $c->stash(
119 commit => $self->_get_object($c),
82bc0f05 120 tags => $repository->tags,
ea19a20c 121 action => 'tags',
122 );
123}
124
5bb401c6 125sub blame : Chained('base') Args(0) {
18a8059a 126 my($self, $c) = @_;
127
82bc0f05 128 my $repository = $c->stash->{Repository};
18a8059a 129 my $h = $c->req->param('h')
82bc0f05 130 || $repository->hash_by_path($c->req->param('hb'), $c->req->param('f'))
18a8059a 131 || die "No file or sha1 provided.";
132 my $hb = $c->req->param('hb')
82bc0f05 133 || $repository->head_hash
18a8059a 134 || die "Couldn't discern the corresponding head.";
135 my $filename = $c->req->param('f') || '';
136
61f14672 137 my $blame = $repository->get_object($hb)->blame($filename, $h);
18a8059a 138 $c->stash(
6bd8b34b 139 blame => $blame,
82bc0f05 140 head => $repository->get_object($hb),
18a8059a 141 filename => $filename,
6bd8b34b 142
143 # XXX Hack hack hack, see View::SyntaxHighlight
144 language => ($filename =~ /\.p[lm]$/i ? 'Perl' : ''),
145 blob => join("\n", map $_->{line}, @$blame),
18a8059a 146 );
6bd8b34b 147
148 $c->forward('View::SyntaxHighlight')
149 unless $c->stash->{no_wrapper};
18a8059a 150}
151
b5f3d3e7 152sub _blob_objs {
295c9703 153 my ( $self, $c ) = @_;
82bc0f05 154 my $repository = $c->stash->{Repository};
c8870bd3 155 my $h = $c->req->param('h')
82bc0f05 156 || $repository->hash_by_path($c->req->param('hb'), $c->req->param('f'))
c8870bd3 157 || die "No file or sha1 provided.";
158 my $hb = $c->req->param('hb')
82bc0f05 159 || $repository->head_hash
c8870bd3 160 || die "Couldn't discern the corresponding head.";
161
f5da8e7a 162 my $filename = $c->req->param('f') || '';
163
82bc0f05 164 my $blob = $repository->get_object($h);
165 $blob = $repository->get_object(
166 $repository->hash_by_path($h || $hb, $filename)
b5f3d3e7 167 ) if $blob->type ne 'blob';
168
82bc0f05 169 return $blob, $repository->get_object($hb), $filename;
b5f3d3e7 170}
171
172=head2 blob
173
174The blob action i.e the contents of a file.
175
176=cut
177
5bb401c6 178sub blob : Chained('base') Args(0) {
b5f3d3e7 179 my ( $self, $c ) = @_;
180
181 my($blob, $head, $filename) = $self->_blob_objs($c);
295c9703 182 $c->stash(
b5f3d3e7 183 blob => $blob->content,
184 head => $head,
f5da8e7a 185 filename => $filename,
186 # XXX Hack hack hack, see View::SyntaxHighlight
6bd8b34b 187 language => ($filename =~ /\.p[lm]$/i ? 'Perl' : ''),
c8870bd3 188 action => 'blob',
295c9703 189 );
7e54e579 190
c8a42dd5 191 $c->forward('View::SyntaxHighlight')
192 unless $c->stash->{no_wrapper};
193}
194
b5f3d3e7 195=head2 blob_plain
196
197The plain text version of blob, where file is rendered as is.
198
199=cut
200
5bb401c6 201sub blob_plain : Chained('base') Args(0) {
c8a42dd5 202 my($self, $c) = @_;
203
b5f3d3e7 204 my($blob) = $self->_blob_objs($c);
c8a42dd5 205 $c->response->content_type('text/plain; charset=utf-8');
b5f3d3e7 206 $c->response->body($blob->content);
207 $c->response->status(200);
c8a42dd5 208}
209
b5f3d3e7 210=head2 blobdiff_plain
211
212The plain text version of blobdiff.
213
214=cut
215
5bb401c6 216sub blobdiff_plain : Chained('base') Args(0) {
c8a42dd5 217 my($self, $c) = @_;
218
219 $c->stash(no_wrapper => 1);
220 $c->response->content_type('text/plain; charset=utf-8');
221
222 $c->forward('blobdiff');
295c9703 223}
224
6cf4366a 225=head2 blobdiff
226
227Exposes a given diff of a blob.
228
229=cut
230
5bb401c6 231sub blobdiff : Chained('base') Args(0) {
6cf4366a 232 my ( $self, $c ) = @_;
832cbc81 233 my $commit = $self->_get_object($c, $c->req->param('hb'));
6cf4366a 234 my $filename = $c->req->param('f')
235 || croak("No file specified!");
87581f05 236 my($tree, $patch) = $c->stash->{Repository}->diff(
ad8884fc 237 commit => $commit,
ad8884fc 238 patch => 1,
c8a42dd5 239 parent => $c->req->param('hpb') || undef,
240 file => $filename,
6cf4366a 241 );
242 $c->stash(
243 commit => $commit,
ad8884fc 244 diff => $patch,
592b68ef 245 filename => $filename,
6cf4366a 246 # XXX Hack hack hack, see View::SyntaxHighlight
ad8884fc 247 blobs => [$patch->[0]->{diff}],
6cf4366a 248 language => 'Diff',
249 action => 'blobdiff',
250 );
251
c8a42dd5 252 $c->forward('View::SyntaxHighlight')
253 unless $c->stash->{no_wrapper};
6cf4366a 254}
255
1625cd5f 256=head2 commit
257
47495599 258Exposes a given commit.
1625cd5f 259
260=cut
261
5bb401c6 262sub commit : Chained('base') Args(0) {
d7c9a32f 263 my ( $self, $c ) = @_;
82bc0f05 264 my $repository = $c->stash->{Repository};
832cbc81 265 my $commit = $self->_get_object($c);
b7aca93a 266 $c->stash(
790ce598 267 commit => $commit,
82bc0f05 268 diff_tree => ($repository->diff(commit => $commit))[0],
269 refs => $repository->references,
790ce598 270 action => 'commit',
b7aca93a 271 );
d7c9a32f 272}
273
2247133f 274=head2 commitdiff
275
276Exposes a given diff of a commit.
277
278=cut
279
5bb401c6 280sub commitdiff : Chained('base') Args(0) {
2247133f 281 my ( $self, $c ) = @_;
832cbc81 282 my $commit = $self->_get_object($c);
87581f05 283 my($tree, $patch) = $c->stash->{Repository}->diff(
ad8884fc 284 commit => $commit,
fc948aee 285 parent => $c->req->param('hp') || undef,
ad8884fc 286 patch => 1,
287 );
2247133f 288 $c->stash(
f5da8e7a 289 commit => $commit,
ad8884fc 290 diff_tree => $tree,
291 diff => $patch,
f5da8e7a 292 # XXX Hack hack hack, see View::SyntaxHighlight
ad8884fc 293 blobs => [map $_->{diff}, @$patch],
f5da8e7a 294 language => 'Diff',
295 action => 'commitdiff',
2247133f 296 );
f5da8e7a 297
c8a42dd5 298 $c->forward('View::SyntaxHighlight')
299 unless $c->stash->{no_wrapper};
300}
301
5bb401c6 302sub commitdiff_plain : Chained('base') Args(0) {
c8a42dd5 303 my($self, $c) = @_;
304
305 $c->stash(no_wrapper => 1);
306 $c->response->content_type('text/plain; charset=utf-8');
307
308 $c->forward('commitdiff');
2247133f 309}
310
47495599 311=head2 shortlog
312
313Expose an abbreviated log of a given sha1.
314
315=cut
316
5bb401c6 317sub shortlog : Chained('base') Args(0) {
47495599 318 my ( $self, $c ) = @_;
592b68ef 319
82bc0f05 320 my $repository = $c->stash->{Repository};
c046a52f 321 my $commit = $self->_get_object($c, $c->req->param('hb'));
592b68ef 322 my $filename = $c->req->param('f') || '';
323
fde5091f 324 my %logargs = (
325 sha1 => $commit->sha1,
f740f8a9 326 count => Gitalist->config->{paging}{log} || 25,
592b68ef 327 ($filename ? (file => $filename) : ())
63e220b9 328 );
fde5091f 329
330 my $page = $c->req->param('pg') || 0;
331 $logargs{skip} = $c->req->param('pg') * $logargs{count}
332 if $c->req->param('pg');
333
47495599 334 $c->stash(
790ce598 335 commit => $commit,
82bc0f05 336 log_lines => [$repository->list_revs(%logargs)],
337 refs => $repository->references,
b4b4d0fd 338 page => $page,
592b68ef 339 filename => $filename,
340 action => 'shortlog',
47495599 341 );
342}
343
790ce598 344=head2 log
345
346Calls shortlog internally. Perhaps that should be reversed ...
347
348=cut
5bb401c6 349
350sub log : Chained('base') Args(0) {
790ce598 351 $_[0]->shortlog($_[1]);
352 $_[1]->stash->{action} = 'log';
353}
354
c8a42dd5 355# For legacy support.
5bb401c6 356sub history : Chained('base') Args(0) {
cce8c9b6 357 my ( $self, $c ) = @_;
358 $self->shortlog($c);
82bc0f05 359 my $repository = $c->stash->{Repository};
360 my $file = $repository->get_object(
361 $repository->hash_by_path(
362 $repository->head_hash,
cce8c9b6 363 $c->stash->{filename}
364 )
365 );
366 $c->stash( action => 'history',
367 filetype => $file->type,
368 );
c8a42dd5 369}
370
b3fa97cd 371=head2 tree
372
373The tree of a given commit.
374
375=cut
376
5bb401c6 377sub tree : Chained('base') Args(0) {
b3fa97cd 378 my ( $self, $c ) = @_;
82bc0f05 379 my $repository = $c->stash->{Repository};
832cbc81 380 my $commit = $self->_get_object($c, $c->req->param('hb'));
c046a52f 381 my $filename = $c->req->param('f') || '';
382 my $tree = $filename
82bc0f05 383 ? $repository->get_object($repository->hash_by_path($commit->sha1, $filename))
384 : $repository->get_object($commit->tree_sha1)
c046a52f 385 ;
b3fa97cd 386 $c->stash(
790ce598 387 commit => $commit,
b4b4d0fd 388 tree => $tree,
82bc0f05 389 tree_list => [$repository->list_tree($tree->sha1)],
832cbc81 390 path => $c->req->param('f') || '',
790ce598 391 action => 'tree',
b3fa97cd 392 );
393}
394
9dc3b9a5 395=head2 reflog
396
397Expose the local reflog. This may go away.
398
399=cut
400
5bb401c6 401sub reflog : Chained('base') Args(0) {
9dc3b9a5 402 my ( $self, $c ) = @_;
87581f05 403 my @log = $c->stash->{Repository}->reflog(
9dc3b9a5 404 '--since=yesterday'
405 );
406
407 $c->stash(
408 log => \@log,
409 action => 'reflog',
410 );
411}
412
ea19a20c 413=head2 search
414
415The action for the search form.
416
417=cut
418
5bb401c6 419sub search : Chained('base') Args(0) {
4df2f62f 420 my($self, $c) = @_;
82bc0f05 421 my $repository = $c->stash->{Repository};
832cbc81 422 my $commit = $self->_get_object($c);
4df2f62f 423 # Lifted from /shortlog.
424 my %logargs = (
425 sha1 => $commit->sha1,
426 count => Gitalist->config->{paging}{log},
427 ($c->req->param('f') ? (file => $c->req->param('f')) : ()),
72d72d4d 428 search => {
429 type => $c->req->param('type'),
430 text => $c->req->param('text'),
431 regexp => $c->req->param('regexp') || 0,
432 },
4df2f62f 433 );
434
435 $c->stash(
436 commit => $commit,
82bc0f05 437 results => [$repository->list_revs(%logargs)],
4df2f62f 438 action => 'search',
439 # This could be added - page => $page,
440 );
14664e1c 441}
442
ea19a20c 443=head2 search_help
444
445Provides some help for the search form.
446
447=cut
448
5bb401c6 449sub search_help : Chained('base') Args(0) {
2646511e 450 my ($self, $c) = @_;
451 $c->stash(template => 'search_help.tt2');
6cfcd548 452}
453
ea19a20c 454=head2 atom
455
82bc0f05 456Provides an atom feed for a given repository.
ea19a20c 457
458=cut
459
5bb401c6 460sub atom : Chained('base') Args(0) {
d17ce39c 461 my($self, $c) = @_;
6cfcd548 462
d17ce39c 463 my $feed = XML::Atom::Feed->new;
6cfcd548 464
d17ce39c 465 my $host = lc Sys::Hostname::hostname();
466 $feed->title($host . ' - ' . Gitalist->config->{name});
467 $feed->updated(~~DateTime->now);
468
82bc0f05 469 my $repository = $c->stash->{Repository};
d17ce39c 470 my %logargs = (
82bc0f05 471 sha1 => $repository->head_hash,
d17ce39c 472 count => Gitalist->config->{paging}{log} || 25,
473 ($c->req->param('f') ? (file => $c->req->param('f')) : ())
474 );
475
476 my $mk_title = $c->stash->{short_cmt};
82bc0f05 477 for my $commit ($repository->list_revs(%logargs)) {
d17ce39c 478 my $entry = XML::Atom::Entry->new;
479 $entry->title( $mk_title->($commit->comment) );
480 $entry->id($c->uri_for('commit', {h=>$commit->sha1}));
481 # XXX Needs work ...
482 $entry->content($commit->comment);
483 $feed->add_entry($entry);
484 }
485
f796a861 486 $c->response->body($feed->as_xml);
e75df318 487 $c->response->content_type('application/atom+xml');
f796a861 488 $c->response->status(200);
6cfcd548 489}
490
ea19a20c 491=head2 rss
492
82bc0f05 493Provides an RSS feed for a given repository.
ea19a20c 494
495=cut
496
5bb401c6 497sub rss : Chained('base') Args(0) {
f796a861 498 my ($self, $c) = @_;
499
82bc0f05 500 my $repository = $c->stash->{Repository};
f796a861 501
502 my $rss = XML::RSS->new(version => '2.0');
503 $rss->channel(
504 title => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name},
82bc0f05 505 link => $c->uri_for('summary', {p=>$repository->name}),
f796a861 506 language => 'en',
82bc0f05 507 description => $repository->description,
f796a861 508 pubDate => DateTime->now,
509 lastBuildDate => DateTime->now,
510 );
511
512 my %logargs = (
82bc0f05 513 sha1 => $repository->head_hash,
f796a861 514 count => Gitalist->config->{paging}{log} || 25,
515 ($c->req->param('f') ? (file => $c->req->param('f')) : ())
516 );
517 my $mk_title = $c->stash->{short_cmt};
82bc0f05 518 for my $commit ($repository->list_revs(%logargs)) {
f796a861 519 # XXX Needs work ....
520 $rss->add_item(
521 title => $mk_title->($commit->comment),
522 permaLink => $c->uri_for(commit => {h=>$commit->sha1}),
523 description => $commit->comment,
524 );
525 }
526
527 $c->response->body($rss->as_string);
528 $c->response->content_type('application/rss+xml');
529 $c->response->status(200);
14664e1c 530}
531
5bb401c6 532sub opml : Chained('base') Args(0) {
286cea09 533 my($self, $c) = @_;
534
535 my $opml = XML::OPML::SimpleGen->new();
536
537 $opml->head(title => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name});
538
82bc0f05 539 my @list = @{ $c->model()->repositories };
540 die 'No repositories found in '. $c->model->repo_dir
286cea09 541 unless @list;
542
543 for my $proj ( @list ) {
544 $opml->insert_outline(
545 text => $proj->name. ' - '. $proj->description,
546 xmlUrl => $c->uri_for(rss => {p => $proj->name}),
547 );
548 }
549
550 $c->response->body($opml->as_string);
551 $c->response->content_type('application/rss');
552 $c->response->status(200);
553}
554
ea19a20c 555=head2 patch
556
557A raw patch for a given commit.
558
559=cut
560
5bb401c6 561sub patch : Chained('base') Args(0) {
377bf360 562 my ($self, $c) = @_;
61ba8635 563 $c->detach('patches', [1]);
564}
565
ea19a20c 566=head2 patches
567
568The patcheset for a given commit ???
569
570=cut
571
5bb401c6 572sub patches : Chained('base') Args(0) {
61ba8635 573 my ($self, $c, $count) = @_;
574 $count ||= Gitalist->config->{patches}{max};
377bf360 575 my $commit = $self->_get_object($c);
576 my $parent = $c->req->param('hp') || undef;
f707d264 577 my $patch = $commit->get_patch( $parent, $count );
377bf360 578 $c->response->body($patch);
579 $c->response->content_type('text/plain');
580 $c->response->status(200);
6cfcd548 581}
582
ea19a20c 583=head2 snapshot
584
585Provides a snapshot of a given commit.
586
587=cut
588
5bb401c6 589sub snapshot : Chained('base') Args(0) {
30db8f5b 590 my ($self, $c) = @_;
63afe2db 591 my $format = $c->req->param('sf') || 'tgz';
30db8f5b 592 die unless $format;
2e79039a 593 my $sha1 = $c->req->param('h') || $self->_get_object($c)->sha1;
87581f05 594 my @snap = $c->stash->{Repository}->snapshot(
c0dbe239 595 sha1 => $sha1,
596 format => $format
597 );
30db8f5b 598 $c->response->status(200);
599 $c->response->headers->header( 'Content-Disposition' =>
c0dbe239 600 "attachment; filename=$snap[0]");
601 $c->response->body($snap[1]);
6cfcd548 602}
603
1625cd5f 604
5bb401c6 605sub base : Chained('/root') PathPart('') CaptureArgs(0) {
4621ecf0 606 my($self, $c) = @_;
04d1d917 607
82bc0f05 608 my $repository = $c->req->param('p');
609 if (defined $repository) {
da8f4f82 610 eval {
7bf1a6f5 611 $c->stash(Repository => $c->model()->get_repository($repository));
da8f4f82 612 };
613 if ($@) {
5232dbdd 614 $c->detach('/error_404');
da8f4f82 615 }
616 }
617
82bc0f05 618 my $a_repository = $c->stash->{Repository} || $c->model()->repositories->[0];
4621ecf0 619 $c->stash(
82bc0f05 620 git_version => $a_repository->run_cmd('--version'),
da8f4f82 621 version => $Gitalist::VERSION,
622
623 # XXX Move these to a plugin!
4621ecf0 624 time_since => sub {
ef11b09e 625 return 'never' unless $_[0];
4621ecf0 626 return age_string(time - $_[0]->epoch);
627 },
628 short_cmt => sub {
629 my $cmt = shift;
630 my($line) = split /\n/, $cmt;
741e110e 631 $line =~ s/^(.{70,80}\b).*/$1 \x{2026}/;
4621ecf0 632 return $line;
633 },
5cd9b9f9 634 abridged_description => sub {
635 join(' ', grep { defined } (split / /, shift)[0..10]);
636 },
4621ecf0 637 );
04d1d917 638}
d9a9b56b 639
fde5091f 640sub end : ActionClass('RenderView') {
1aad4e81 641 my ($self, $c) = @_;
82bc0f05 642 # Give repository views the current HEAD.
87581f05 643 if ($c->stash->{Repository}) {
644 $c->stash->{HEAD} = $c->stash->{Repository}->head_hash;
1aad4e81 645 }
1fd8159c 646}
d9a9b56b 647
c113db92 648sub error_404 : Action {
4111e151 649 my ($self, $c) = @_;
650 $c->response->status(404);
5232dbdd 651 $c->response->body('Page not found');
4111e151 652}
653
775e96e0 654__PACKAGE__->meta->make_immutable;
655
656__END__
657
d137f7d5 658=head1 NAME
659
660Gitalist::Controller::Root - Root controller for the application
661
662=head1 DESCRIPTION
663
664This controller handles all of the root level paths for the application
665
666=head1 METHODS
667
5bb401c6 668=head2 root
669
670Root of chained actions
671
672=head2 base
673
674Populate the header and footer. Perhaps not the best location.
675
676=head2 index
677
82bc0f05 678Provides the repository listing.
5bb401c6 679
680=head2 end
681
682Attempt to render a view, if needed.
d137f7d5 683
684=head2 blame
685
686=head2 commitdiff_plain
687
688=head2 error_404
689
690=head2 history
691
692=head2 opml
693
82bc0f05 694=head2 repository_index
da8f4f82 695
775e96e0 696=head1 AUTHORS
89de6a33 697
775e96e0 698See L<Gitalist> for authors.
89de6a33 699
700=head1 LICENSE
701
775e96e0 702See L<Gitalist> for the license.
89de6a33 703
704=cut