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