Switch to chained dispatch
[catagits/Gitalist.git] / lib / Gitalist / Controller / Root.pm
1 package Gitalist::Controller::Root;
2
3 use Moose;
4 use Moose::Autobox;
5 use Sys::Hostname ();
6 use XML::Atom::Feed;
7 use XML::Atom::Entry;
8 use XML::RSS;
9 use XML::OPML::SimpleGen;
10
11 use Gitalist::Utils qw/ age_string /;
12
13 use namespace::autoclean;
14
15 BEGIN { extends 'Catalyst::Controller' }
16
17 __PACKAGE__->config->{namespace} = '';
18
19 sub root : Chained('/') PathPart('') CaptureArgs(0) {}
20
21 sub _get_object {
22   my($self, $c, $haveh) = @_;
23
24   my $h = $haveh || $c->req->param('h') || '';
25   my $f = $c->req->param('f');
26
27   my $m = $c->stash->{Project};
28   my $pd = $m->path;
29
30   # Either use the provided h(ash) parameter, the f(ile) parameter or just use HEAD.
31   my $hash = ($h =~ /[^a-f0-9]/ ? $m->head_hash($h) : $h)
32           || ($f && $m->hash_by_path($f))
33           || $m->head_hash
34           # XXX This could definitely use more context.
35           || Carp::croak("Couldn't find a hash for the commit object!");
36
37   my $obj = $m->get_object($hash)
38     or Carp::croak("Couldn't find a object for '$hash' in '$pd'!");
39
40   return $obj;
41 }
42
43 sub index : Chained('base') PathPart('') Args(0) {
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   );
66 }
67
68 # FIXME - WTF is this for?
69 sub project_index : Chained('base') Args(0) {
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
83 =head2 summary
84
85 A summary of what's happening in the repo.
86
87 =cut
88
89 sub summary : Chained('base') Args(0) {
90   my ( $self, $c ) = @_;
91   my $project = $c->stash->{Project};
92   $c->detach('error_404') unless $project;
93   my $commit = $self->_get_object($c);
94   my @heads  = @{$project->heads};
95   my $maxitems = Gitalist->config->{paging}{summary} || 10;
96   $c->stash(
97     commit    => $commit,
98     log_lines => [$project->list_revs(
99         sha1 => $commit->sha1,
100         count => $maxitems,
101     )],
102     refs      => $project->references,
103     heads     => [ @heads[0 .. ($#heads < $maxitems ? $#heads : $maxitems)] ],
104     action    => 'summary',
105   );
106 }
107
108 =head2 heads
109
110 The current list of heads (aka branches) in the repo.
111
112 =cut
113
114 sub heads : Chained('base') Args(0) {
115   my ( $self, $c ) = @_;
116   my $project = $c->stash->{Project};
117   $c->stash(
118     commit => $self->_get_object($c),
119     heads  => $project->heads,
120     action => 'heads',
121   );
122 }
123
124 =head2 tags
125
126 The current list of tags in the repo.
127
128 =cut
129
130 sub tags : Chained('base') Args(0) {
131   my ( $self, $c ) = @_;
132   my $project = $c->stash->{Project};
133   $c->stash(
134     commit => $self->_get_object($c),
135     tags   => $project->tags,
136     action => 'tags',
137   );
138 }
139
140 sub blame : Chained('base') Args(0) {
141   my($self, $c) = @_;
142
143   my $project = $c->stash->{Project};
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
152   my $blame = $project->get_object($hb)->blame($filename);
153   $c->stash(
154     blame    => $blame,
155     head     => $project->get_object($hb),
156     filename => $filename,
157
158     # XXX Hack hack hack, see View::SyntaxHighlight
159     language => ($filename =~ /\.p[lm]$/i ? 'Perl' : ''),
160     blob     => join("\n", map $_->{line}, @$blame),
161   );
162
163   $c->forward('View::SyntaxHighlight')
164     unless $c->stash->{no_wrapper};
165 }
166
167 sub _blob_objs {
168   my ( $self, $c ) = @_;
169   my $project = $c->stash->{Project};
170   my $h  = $c->req->param('h')
171        || $project->hash_by_path($c->req->param('hb'), $c->req->param('f'))
172        || die "No file or sha1 provided.";
173   my $hb = $c->req->param('hb')
174        || $project->head_hash
175        || die "Couldn't discern the corresponding head.";
176
177   my $filename = $c->req->param('f') || '';
178
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
189 The blob action i.e the contents of a file.
190
191 =cut
192
193 sub blob : Chained('base') Args(0) {
194   my ( $self, $c ) = @_;
195
196   my($blob, $head, $filename) = $self->_blob_objs($c);
197   $c->stash(
198     blob     => $blob->content,
199     head     => $head,
200     filename => $filename,
201     # XXX Hack hack hack, see View::SyntaxHighlight
202     language => ($filename =~ /\.p[lm]$/i ? 'Perl' : ''),
203     action   => 'blob',
204   );
205
206   $c->forward('View::SyntaxHighlight')
207     unless $c->stash->{no_wrapper};
208 }
209
210 =head2 blob_plain
211
212 The plain text version of blob, where file is rendered as is.
213
214 =cut
215
216 sub blob_plain : Chained('base') Args(0) {
217   my($self, $c) = @_;
218
219   my($blob) = $self->_blob_objs($c);
220   $c->response->content_type('text/plain; charset=utf-8');
221   $c->response->body($blob->content);
222   $c->response->status(200);
223 }
224
225 =head2 blobdiff_plain
226
227 The plain text version of blobdiff.
228
229 =cut
230
231 sub blobdiff_plain : Chained('base') Args(0) {
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');
238 }
239
240 =head2 blobdiff
241
242 Exposes a given diff of a blob.
243
244 =cut
245
246 sub blobdiff : Chained('base') Args(0) {
247   my ( $self, $c ) = @_;
248   my $commit = $self->_get_object($c, $c->req->param('hb'));
249   my $filename = $c->req->param('f')
250               || croak("No file specified!");
251   my($tree, $patch) = $c->stash->{Project}->diff(
252     commit => $commit,
253     patch  => 1,
254     parent => $c->req->param('hpb') || undef,
255     file   => $filename,
256   );
257   $c->stash(
258     commit    => $commit,
259     diff      => $patch,
260     filename  => $filename,
261     # XXX Hack hack hack, see View::SyntaxHighlight
262     blobs     => [$patch->[0]->{diff}],
263     language  => 'Diff',
264     action    => 'blobdiff',
265   );
266
267   $c->forward('View::SyntaxHighlight')
268     unless $c->stash->{no_wrapper};
269 }
270
271 =head2 commit
272
273 Exposes a given commit.
274
275 =cut
276
277 sub commit : Chained('base') Args(0) {
278   my ( $self, $c ) = @_;
279   my $project = $c->stash->{Project};
280   my $commit = $self->_get_object($c);
281   $c->stash(
282       commit      => $commit,
283       diff_tree   => ($project->diff(commit => $commit))[0],
284       refs      => $project->references,
285       action      => 'commit',
286   );
287 }
288
289 =head2 commitdiff
290
291 Exposes a given diff of a commit.
292
293 =cut
294
295 sub commitdiff : Chained('base') Args(0) {
296   my ( $self, $c ) = @_;
297   my $commit = $self->_get_object($c);
298   my($tree, $patch) = $c->stash->{Project}->diff(
299       commit => $commit,
300       parent => $c->req->param('hp') || undef,
301       patch  => 1,
302   );
303   $c->stash(
304     commit    => $commit,
305     diff_tree => $tree,
306     diff      => $patch,
307     # XXX Hack hack hack, see View::SyntaxHighlight
308     blobs     => [map $_->{diff}, @$patch],
309     language  => 'Diff',
310     action    => 'commitdiff',
311   );
312
313   $c->forward('View::SyntaxHighlight')
314     unless $c->stash->{no_wrapper};
315 }
316
317 sub commitdiff_plain : Chained('base') Args(0) {
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');
324 }
325
326 =head2 shortlog
327
328 Expose an abbreviated log of a given sha1.
329
330 =cut
331
332 sub shortlog : Chained('base') Args(0) {
333   my ( $self, $c ) = @_;
334
335   my $project  = $c->stash->{Project};
336   my $commit   = $self->_get_object($c, $c->req->param('hb'));
337   my $filename = $c->req->param('f') || '';
338
339   my %logargs = (
340       sha1   => $commit->sha1,
341       count  => Gitalist->config->{paging}{log} || 25,
342       ($filename ? (file => $filename) : ())
343   );
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
349   $c->stash(
350       commit    => $commit,
351       log_lines => [$project->list_revs(%logargs)],
352       refs      => $project->references,
353       page      => $page,
354       filename  => $filename,
355       action    => 'shortlog',
356   );
357 }
358
359 =head2 log
360
361 Calls shortlog internally. Perhaps that should be reversed ...
362
363 =cut
364
365 sub log : Chained('base') Args(0) {
366     $_[0]->shortlog($_[1]);
367     $_[1]->stash->{action} = 'log';
368 }
369
370 # For legacy support.
371 sub history : Chained('base') Args(0) {
372     my ( $self, $c ) = @_;
373     $self->shortlog($c);
374     my $project = $c->stash->{Project};
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            );
384 }
385
386 =head2 tree
387
388 The tree of a given commit.
389
390 =cut
391
392 sub tree : Chained('base') Args(0) {
393   my ( $self, $c ) = @_;
394   my $project = $c->stash->{Project};
395   my $commit  = $self->_get_object($c, $c->req->param('hb'));
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   ;
401   $c->stash(
402       commit    => $commit,
403       tree      => $tree,
404       tree_list => [$project->list_tree($tree->sha1)],
405       path      => $c->req->param('f') || '',
406       action    => 'tree',
407   );
408 }
409
410 =head2 reflog
411
412 Expose the local reflog. This may go away.
413
414 =cut
415
416 sub reflog : Chained('base') Args(0) {
417   my ( $self, $c ) = @_;
418   my @log = $c->stash->{Project}->reflog(
419       '--since=yesterday'
420   );
421
422   $c->stash(
423       log    => \@log,
424       action => 'reflog',
425   );
426 }
427
428 =head2 search
429
430 The action for the search form.
431
432 =cut
433
434 sub search : Chained('base') Args(0) {
435   my($self, $c) = @_;
436   $c->stash(current_action => 'GitRepos');
437   my $project = $c->stash->{Project};
438   my $commit  = $self->_get_object($c);
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')) : ()),
444     search => {
445       type   => $c->req->param('type'),
446       text   => $c->req->param('text'),
447       regexp => $c->req->param('regexp') || 0,
448     },
449   );
450
451   $c->stash(
452       commit  => $commit,
453       results => [$project->list_revs(%logargs)],
454       action  => 'search',
455           # This could be added - page      => $page,
456   );
457 }
458
459 =head2 search_help
460
461 Provides some help for the search form.
462
463 =cut
464
465 sub search_help : Chained('base') Args(0) {
466     my ($self, $c) = @_;
467     $c->stash(template => 'search_help.tt2');
468 }
469
470 =head2 atom
471
472 Provides an atom feed for a given project.
473
474 =cut
475
476 sub atom : Chained('base') Args(0) {
477   my($self, $c) = @_;
478
479   my $feed = XML::Atom::Feed->new;
480
481   my $host = lc Sys::Hostname::hostname();
482   $feed->title($host . ' - ' . Gitalist->config->{name});
483   $feed->updated(~~DateTime->now);
484
485   my $project = $c->stash->{Project};
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
502   $c->response->body($feed->as_xml);
503   $c->response->content_type('application/atom+xml');
504   $c->response->status(200);
505 }
506
507 =head2 rss
508
509 Provides an RSS feed for a given project.
510
511 =cut
512
513 sub rss : Chained('base') Args(0) {
514   my ($self, $c) = @_;
515
516   my $project = $c->stash->{Project};
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);
546 }
547
548 sub opml : Chained('base') Args(0) {
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
571 =head2 patch
572
573 A raw patch for a given commit.
574
575 =cut
576
577 sub patch : Chained('base') Args(0) {
578     my ($self, $c) = @_;
579     $c->detach('patches', [1]);
580 }
581
582 =head2 patches
583
584 The patcheset for a given commit ???
585
586 =cut
587
588 sub patches : Chained('base') Args(0) {
589     my ($self, $c, $count) = @_;
590     $count ||= Gitalist->config->{patches}{max};
591     my $commit = $self->_get_object($c);
592     my $parent = $c->req->param('hp') || undef;
593     my $patch = $commit->get_patch( $parent, $count );
594     $c->response->body($patch);
595     $c->response->content_type('text/plain');
596     $c->response->status(200);
597 }
598
599 =head2 snapshot
600
601 Provides a snapshot of a given commit.
602
603 =cut
604
605 sub snapshot : Chained('base') Args(0) {
606     my ($self, $c) = @_;
607     my $format = $c->req->param('sf') || 'tgz';
608     die unless $format;
609     my $sha1 = $c->req->param('h') || $self->_get_object($c)->sha1;
610     my @snap = $c->stash->{Project}->snapshot(
611         sha1 => $sha1,
612         format => $format
613     );
614     $c->response->status(200);
615     $c->response->headers->header( 'Content-Disposition' =>
616                                        "attachment; filename=$snap[0]");
617     $c->response->body($snap[1]);
618 }
619
620
621 sub base : Chained('/root') PathPart('') CaptureArgs(0) {
622   my($self, $c) = @_;
623
624   my $project = $c->req->param('p');
625   if (defined $project) {
626     eval {
627       $c->stash(Project => $c->model('GitRepos')->project($project));
628     };
629     if ($@) {
630       $c->detach('/error_404');
631     }
632   }
633
634   my $a_project = $c->stash->{Project} || $c->model()->projects->[0];
635   $c->stash(
636     git_version => $a_project->run_cmd('--version'),
637     version     => $Gitalist::VERSION,
638
639     # XXX Move these to a plugin!
640     time_since => sub {
641       return 'never' unless $_[0];
642       return age_string(time - $_[0]->epoch);
643     },
644     short_cmt => sub {
645       my $cmt = shift;
646       my($line) = split /\n/, $cmt;
647       $line =~ s/^(.{70,80}\b).*/$1 \x{2026}/;
648       return $line;
649     },
650     abridged_description => sub {
651         join(' ', grep { defined } (split / /, shift)[0..10]);
652     },
653   );
654 }
655
656 sub end : ActionClass('RenderView') {
657     my ($self, $c) = @_;
658     # Give project views the current HEAD.
659     if ($c->stash->{Project}) {
660         $c->stash->{HEAD} = $c->stash->{Project}->head_hash;
661     }
662 }
663
664 sub error_404 : Action {
665     my ($self, $c) = @_;
666     $c->response->status(404);
667     $c->response->body('Page not found');
668 }
669
670 __PACKAGE__->meta->make_immutable;
671
672 __END__
673
674 =head1 NAME
675
676 Gitalist::Controller::Root - Root controller for the application
677
678 =head1 DESCRIPTION
679
680 This controller handles all of the root level paths for the application
681
682 =head1 METHODS
683
684 =head2 root
685
686 Root of chained actions
687
688 =head2 base
689
690 Populate the header and footer. Perhaps not the best location.
691
692 =head2 index
693
694 Provides the project listing.
695
696 =head2 end
697
698 Attempt to render a view, if needed.
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
711
712 =head1 AUTHORS
713
714 See L<Gitalist> for authors.
715
716 =head1 LICENSE
717
718 See L<Gitalist> for the license.
719
720 =cut