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