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