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