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