Make tags appear on the summary page. Remove code from Root controller
[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->{Repository};
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 $search = $c->req->param('s') || '';
50
51   $c->stash(
52     search_text => $search,
53   );
54 }
55
56 sub blame : Chained('base') Args(0) {
57   my($self, $c) = @_;
58
59   my $repository = $c->stash->{Repository};
60   my $h  = $c->req->param('h')
61        || $repository->hash_by_path($c->req->param('hb'), $c->req->param('f'))
62        || die "No file or sha1 provided.";
63   my $hb = $c->req->param('hb')
64        || $repository->head_hash
65        || die "Couldn't discern the corresponding head.";
66   my $filename = $c->req->param('f') || '';
67
68   my $blame = $repository->get_object($hb)->blame($filename, $h);
69   $c->stash(
70     blame    => $blame,
71     head     => $repository->get_object($hb),
72     filename => $filename,
73
74     # XXX Hack hack hack, see View::SyntaxHighlight
75     language => ($filename =~ /\.p[lm]$/i ? 'Perl' : ''),
76     blob     => join("\n", map $_->{line}, @$blame),
77   );
78
79   $c->forward('View::SyntaxHighlight')
80     unless $c->stash->{no_wrapper};
81 }
82
83 sub _blob_objs {
84   my ( $self, $c ) = @_;
85   my $repository = $c->stash->{Repository};
86   my $h  = $c->req->param('h')
87        || $repository->hash_by_path($c->req->param('hb'), $c->req->param('f'))
88        || die "No file or sha1 provided.";
89   my $hb = $c->req->param('hb')
90        || $repository->head_hash
91        || die "Couldn't discern the corresponding head.";
92
93   my $filename = $c->req->param('f') || '';
94
95   my $blob = $repository->get_object($h);
96   $blob = $repository->get_object(
97     $repository->hash_by_path($h || $hb, $filename)
98   ) if $blob->type ne 'blob';
99
100   return $blob, $repository->get_object($hb), $filename;
101 }
102
103 =head2 blob
104
105 The blob action i.e the contents of a file.
106
107 =cut
108
109 sub blob : Chained('base') Args(0) {
110   my ( $self, $c ) = @_;
111
112   my($blob, $head, $filename) = $self->_blob_objs($c);
113   $c->stash(
114     blob     => $blob->content,
115     head     => $head,
116     filename => $filename,
117     # XXX Hack hack hack, see View::SyntaxHighlight
118     language => ($filename =~ /\.p[lm]$/i ? 'Perl' : ''),
119   );
120
121   $c->forward('View::SyntaxHighlight')
122     unless $c->stash->{no_wrapper};
123 }
124
125 =head2 blob_plain
126
127 The plain text version of blob, where file is rendered as is.
128
129 =cut
130
131 sub blob_plain : Chained('base') Args(0) {
132   my($self, $c) = @_;
133
134   my($blob) = $self->_blob_objs($c);
135   $c->response->content_type('text/plain; charset=utf-8');
136   $c->response->body($blob->content);
137   $c->response->status(200);
138 }
139
140 =head2 blobdiff_plain
141
142 The plain text version of blobdiff.
143
144 =cut
145
146 sub blobdiff_plain : Chained('base') Args(0) {
147   my($self, $c) = @_;
148
149   $c->stash(no_wrapper => 1);
150   $c->response->content_type('text/plain; charset=utf-8');
151
152   $c->forward('blobdiff');
153 }
154
155 =head2 blobdiff
156
157 Exposes a given diff of a blob.
158
159 =cut
160
161 sub blobdiff : Chained('base') Args(0) {
162   my ( $self, $c ) = @_;
163   my $commit = $self->_get_object($c, $c->req->param('hb'));
164   my $filename = $c->req->param('f')
165               || croak("No file specified!");
166   my($tree, $patch) = $c->stash->{Repository}->diff(
167     commit => $commit,
168     patch  => 1,
169     parent => $c->req->param('hpb') || undef,
170     file   => $filename,
171   );
172   $c->stash(
173     commit    => $commit,
174     diff      => $patch,
175     filename  => $filename,
176     # XXX Hack hack hack, see View::SyntaxHighlight
177     blobs     => [$patch->[0]->{diff}],
178     language  => 'Diff',
179   );
180
181   $c->forward('View::SyntaxHighlight')
182     unless $c->stash->{no_wrapper};
183 }
184
185 =head2 commit
186
187 Exposes a given commit.
188
189 =cut
190
191 sub commit : Chained('base') Args(0) {
192   my ( $self, $c ) = @_;
193   my $repository = $c->stash->{Repository};
194   my $commit = $self->_get_object($c);
195   $c->stash(
196       commit      => $commit,
197       diff_tree   => ($repository->diff(commit => $commit))[0],
198       refs      => $repository->references,
199   );
200 }
201
202 =head2 commitdiff
203
204 Exposes a given diff of a commit.
205
206 =cut
207
208 sub commitdiff : Chained('base') Args(0) {
209   my ( $self, $c ) = @_;
210   my $commit = $self->_get_object($c);
211   my($tree, $patch) = $c->stash->{Repository}->diff(
212       commit => $commit,
213       parent => $c->req->param('hp') || undef,
214       patch  => 1,
215   );
216   $c->stash(
217     commit    => $commit,
218     diff_tree => $tree,
219     diff      => $patch,
220     # XXX Hack hack hack, see View::SyntaxHighlight
221     blobs     => [map $_->{diff}, @$patch],
222     language  => 'Diff',
223   );
224
225   $c->forward('View::SyntaxHighlight')
226     unless $c->stash->{no_wrapper};
227 }
228
229 sub commitdiff_plain : Chained('base') Args(0) {
230   my($self, $c) = @_;
231
232   $c->stash(no_wrapper => 1);
233   $c->response->content_type('text/plain; charset=utf-8');
234
235   $c->forward('commitdiff');
236 }
237
238 # For legacy support.
239 sub history : Chained('base') Args(0) {
240     my ( $self, $c ) = @_;
241     $self->shortlog($c);
242     my $repository = $c->stash->{Repository};
243     my $file = $repository->get_object(
244         $repository->hash_by_path(
245             $repository->head_hash,
246             $c->stash->{filename}
247         )
248     );
249      $c->stash(
250                filetype => $file->type,
251            );
252 }
253
254 =head2 tree
255
256 The tree of a given commit.
257
258 =cut
259
260 sub tree : Chained('base') Args(0) {
261   my ( $self, $c ) = @_;
262   my $repository = $c->stash->{Repository};
263   my $commit  = $self->_get_object($c, $c->req->param('hb'));
264   my $filename = $c->req->param('f') || '';
265   my $tree    = $filename
266     ? $repository->get_object($repository->hash_by_path($commit->sha1, $filename))
267     : $repository->get_object($commit->tree_sha1)
268   ;
269   $c->stash(
270       commit    => $commit,
271       tree      => $tree,
272       tree_list => [$repository->list_tree($tree->sha1)],
273       path      => $c->req->param('f') || '',
274   );
275 }
276
277 =head2 reflog
278
279 Expose the local reflog. This may go away.
280
281 =cut
282
283 sub reflog : Chained('base') Args(0) {
284   my ( $self, $c ) = @_;
285   my @log = $c->stash->{Repository}->reflog(
286       '--since=yesterday'
287   );
288
289   $c->stash(
290       log    => \@log,
291   );
292 }
293
294 =head2 search
295
296 The action for the search form.
297
298 =cut
299
300 sub search : Chained('base') Args(0) {
301   my($self, $c) = @_;
302   my $repository = $c->stash->{Repository};
303   my $commit  = $self->_get_object($c);
304   # Lifted from /shortlog.
305   my %logargs = (
306     sha1   => $commit->sha1,
307     count  => Gitalist->config->{paging}{log},
308     ($c->req->param('f') ? (file => $c->req->param('f')) : ()),
309     search => {
310       type   => $c->req->param('type'),
311       text   => $c->req->param('text'),
312       regexp => $c->req->param('regexp') || 0,
313     },
314   );
315
316   $c->stash(
317       commit  => $commit,
318       results => [$repository->list_revs(%logargs)],
319           # This could be added - page      => $page,
320   );
321 }
322
323 =head2 search_help
324
325 Provides some help for the search form.
326
327 =cut
328
329 sub search_help : Chained('base') Args(0) {
330     my ($self, $c) = @_;
331     $c->stash(template => 'search_help.tt2');
332 }
333
334 =head2 atom
335
336 Provides an atom feed for a given repository.
337
338 =cut
339
340 sub atom : Chained('base') Args(0) {
341   my($self, $c) = @_;
342
343   my $feed = XML::Atom::Feed->new;
344
345   my $host = lc Sys::Hostname::hostname();
346   $feed->title($host . ' - ' . Gitalist->config->{name});
347   $feed->updated(~~DateTime->now);
348
349   my $repository = $c->stash->{Repository};
350   my %logargs = (
351       sha1   => $repository->head_hash,
352       count  => Gitalist->config->{paging}{log} || 25,
353       ($c->req->param('f') ? (file => $c->req->param('f')) : ())
354   );
355
356   my $mk_title = $c->stash->{short_cmt};
357   for my $commit ($repository->list_revs(%logargs)) {
358     my $entry = XML::Atom::Entry->new;
359     $entry->title( $mk_title->($commit->comment) );
360     $entry->id($c->uri_for('commit', {h=>$commit->sha1}));
361     # XXX Needs work ...
362     $entry->content($commit->comment);
363     $feed->add_entry($entry);
364   }
365
366   $c->response->body($feed->as_xml);
367   $c->response->content_type('application/atom+xml');
368   $c->response->status(200);
369 }
370
371 =head2 rss
372
373 Provides an RSS feed for a given repository.
374
375 =cut
376
377 sub rss : Chained('base') Args(0) {
378   my ($self, $c) = @_;
379
380   my $repository = $c->stash->{Repository};
381
382   my $rss = XML::RSS->new(version => '2.0');
383   $rss->channel(
384     title          => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name},
385     link           => $c->uri_for('summary', {p=>$repository->name}),
386     language       => 'en',
387     description    => $repository->description,
388     pubDate        => DateTime->now,
389     lastBuildDate  => DateTime->now,
390   );
391
392   my %logargs = (
393       sha1   => $repository->head_hash,
394       count  => Gitalist->config->{paging}{log} || 25,
395       ($c->req->param('f') ? (file => $c->req->param('f')) : ())
396   );
397   my $mk_title = $c->stash->{short_cmt};
398   for my $commit ($repository->list_revs(%logargs)) {
399     # XXX Needs work ....
400     $rss->add_item(
401         title       => $mk_title->($commit->comment),
402         permaLink   => $c->uri_for(commit => {h=>$commit->sha1}),
403         description => $commit->comment,
404     );
405   }
406
407   $c->response->body($rss->as_string);
408   $c->response->content_type('application/rss+xml');
409   $c->response->status(200);
410 }
411
412 sub opml : Chained('base') Args(0) {
413   my($self, $c) = @_;
414
415   my $opml = XML::OPML::SimpleGen->new();
416
417   $opml->head(title => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name});
418
419   my @list = @{ $c->model()->repositories };
420   die 'No repositories found in '. $c->model->repo_dir
421     unless @list;
422
423   for my $proj ( @list ) {
424     $opml->insert_outline(
425       text   => $proj->name. ' - '. $proj->description,
426       xmlUrl => $c->uri_for(rss => {p => $proj->name}),
427     );
428   }
429
430   $c->response->body($opml->as_string);
431   $c->response->content_type('application/rss');
432   $c->response->status(200);
433 }
434
435 =head2 patch
436
437 A raw patch for a given commit.
438
439 =cut
440
441 sub patch : Chained('base') Args(0) {
442     my ($self, $c) = @_;
443     $c->detach('patches', [1]);
444 }
445
446 =head2 patches
447
448 The patcheset for a given commit ???
449
450 =cut
451
452 sub patches : Chained('base') Args(0) {
453     my ($self, $c, $count) = @_;
454     $count ||= Gitalist->config->{patches}{max};
455     my $commit = $self->_get_object($c);
456     my $parent = $c->req->param('hp') || undef;
457     my $patch = $commit->get_patch( $parent, $count );
458     $c->response->body($patch);
459     $c->response->content_type('text/plain');
460     $c->response->status(200);
461 }
462
463 =head2 snapshot
464
465 Provides a snapshot of a given commit.
466
467 =cut
468
469 sub snapshot : Chained('base') Args(0) {
470     my ($self, $c) = @_;
471     my $format = $c->req->param('sf') || 'tgz';
472     die unless $format;
473     my $sha1 = $c->req->param('h') || $self->_get_object($c)->sha1;
474     my @snap = $c->stash->{Repository}->snapshot(
475         sha1 => $sha1,
476         format => $format
477     );
478     $c->response->status(200);
479     $c->response->headers->header( 'Content-Disposition' =>
480                                        "attachment; filename=$snap[0]");
481     $c->response->body($snap[1]);
482 }
483
484
485 sub base : Chained('/root') PathPart('') CaptureArgs(0) {
486   my($self, $c) = @_;
487
488   my $git_version = `git --version`;
489   chomp($git_version);
490   $c->stash(
491     git_version => $git_version,
492     version     => $Gitalist::VERSION,
493
494     # XXX Move these to a plugin!
495     time_since => sub {
496       return 'never' unless $_[0];
497       return age_string(time - $_[0]->epoch);
498     },
499     short_cmt => sub {
500       my $cmt = shift;
501       my($line) = split /\n/, $cmt;
502       $line =~ s/^(.{70,80}\b).*/$1 \x{2026}/;
503       return $line;
504     },
505     abridged_description => sub {
506         join(' ', grep { defined } (split / /, shift)[0..10]);
507     },
508   );
509 }
510
511 sub end : ActionClass('RenderView') {
512     my ($self, $c) = @_;
513     # Give repository views the current HEAD.
514     if ($c->stash->{Repository}) {
515         $c->stash->{HEAD} = $c->stash->{Repository}->head_hash;
516     }
517 }
518
519 sub error_404 : Action {
520     my ($self, $c) = @_;
521     $c->response->status(404);
522     $c->response->body('Page not found');
523 }
524
525 __PACKAGE__->meta->make_immutable;
526
527 __END__
528
529 =head1 NAME
530
531 Gitalist::Controller::Root - Root controller for the application
532
533 =head1 DESCRIPTION
534
535 This controller handles all of the root level paths for the application
536
537 =head1 METHODS
538
539 =head2 root
540
541 Root of chained actions
542
543 =head2 base
544
545 Populate the header and footer. Perhaps not the best location.
546
547 =head2 index
548
549 Provides the repository listing.
550
551 =head2 end
552
553 Attempt to render a view, if needed.
554
555 =head2 blame
556
557 =head2 commitdiff_plain
558
559 =head2 error_404
560
561 =head2 history
562
563 =head2 opml
564
565 =head2 repository_index
566
567 =head1 AUTHORS
568
569 See L<Gitalist> for authors.
570
571 =head1 LICENSE
572
573 See L<Gitalist> for the license.
574
575 =cut