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