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