Added stubs for further missing 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 __PACKAGE__->config->{namespace} = '';
8
9 use IO::Capture::Stdout;
10 use Sys::Hostname ();
11 use XML::Atom::Feed;
12 use XML::Atom::Entry;
13 use XML::RSS;
14
15 =head1 NAME
16
17 Gitalist::Controller::Root - Root Controller for Gitalist
18
19 =head1 DESCRIPTION
20
21 [enter your description here]
22
23 =head1 METHODS
24
25 =cut
26
27 =head2 index
28
29 =cut
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     my ($self, $c) = @_;
399     $c->stash(template => 'search_help.tt2');
400 }
401
402 sub atom : Local {
403   my($self, $c) = @_;
404
405   my $feed = XML::Atom::Feed->new;
406
407   my $host = lc Sys::Hostname::hostname();
408   $feed->title($host . ' - ' . Gitalist->config->{name});
409   $feed->updated(~~DateTime->now);
410
411   my $project = $c->stash->{Project};
412   my %logargs = (
413       sha1   => $project->head_hash,
414       count  => Gitalist->config->{paging}{log} || 25,
415       ($c->req->param('f') ? (file => $c->req->param('f')) : ())
416   );
417
418   my $mk_title = $c->stash->{short_cmt};
419   for my $commit ($project->list_revs(%logargs)) {
420     my $entry = XML::Atom::Entry->new;
421     $entry->title( $mk_title->($commit->comment) );
422     $entry->id($c->uri_for('commit', {h=>$commit->sha1}));
423     # XXX Needs work ...
424     $entry->content($commit->comment);
425     $feed->add_entry($entry);
426   }
427
428   $c->response->body($feed->as_xml);
429   $c->response->content_type('application/atom+xml');
430   $c->response->status(200);
431 }
432
433 sub rss : Local {
434   my ($self, $c) = @_;
435
436   my $project = $c->stash->{Project};
437
438   my $rss = XML::RSS->new(version => '2.0');
439   $rss->channel(
440     title          => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name},
441     link           => $c->uri_for('summary', {p=>$project->name}),
442     language       => 'en',
443     description    => $project->description,
444     pubDate        => DateTime->now,
445     lastBuildDate  => DateTime->now,
446   );
447
448   my %logargs = (
449       sha1   => $project->head_hash,
450       count  => Gitalist->config->{paging}{log} || 25,
451       ($c->req->param('f') ? (file => $c->req->param('f')) : ())
452   );
453   my $mk_title = $c->stash->{short_cmt};
454   for my $commit ($project->list_revs(%logargs)) {
455     # XXX Needs work ....
456     $rss->add_item(
457         title       => $mk_title->($commit->comment),
458         permaLink   => $c->uri_for(commit => {h=>$commit->sha1}),
459         description => $commit->comment,
460     );
461   }
462
463   $c->response->body($rss->as_string);
464   $c->response->content_type('application/rss+xml');
465   $c->response->status(200);
466 }
467
468 sub patch : Local {
469     my ($self, $c) = @_;
470     $c->detach('patches', [1]);
471 }
472
473 sub patches : Local {
474     my ($self, $c, $count) = @_;
475     $count ||= Gitalist->config->{patches}{max};
476     my $commit = $self->_get_object($c);
477     my $parent = $c->req->param('hp') || undef;
478     my $patch = $commit->get_patch( $parent, $count );
479     $c->response->body($patch);
480     $c->response->content_type('text/plain');
481     $c->response->status(200);
482 }
483
484 sub snapshot : Local {
485     # FIXME - implement snapshot
486     Carp::croak "Not implemented.";
487 }
488
489 =head2 auto
490
491 Populate the header and footer. Perhaps not the best location.
492
493 =cut
494
495 sub auto : Private {
496   my($self, $c) = @_;
497
498   my $project = $c->req->param('p');
499   if (defined $project) {
500     eval {
501       $c->stash(Project => $c->model('GitRepos')->project($project));
502     };
503     if ($@) {
504       $c->detach('error_404');
505     }
506   }
507
508   my $a_project = $c->stash->{Project} || $c->model()->projects->[0];
509   $c->stash(
510     git_version => $a_project->run_cmd('--version'),
511     version     => $Gitalist::VERSION,
512
513     # XXX Move these to a plugin!
514     time_since => sub {
515       return 'never' unless $_[0];
516       return age_string(time - $_[0]->epoch);
517     },
518     short_cmt => sub {
519       my $cmt = shift;
520       my($line) = split /\n/, $cmt;
521       $line =~ s/^(.{70,80}\b).*/$1 …/;
522       return $line;
523     },
524     abridged_description => sub {
525         join(' ', grep { defined } (split / /, shift)[0..10]);
526     },
527   );
528 }
529
530 sub tags : Local {
531     # FIXME - implement snapshot
532     Carp::croak "Not implemented.";
533 }
534 sub project_index : Local {
535     # FIXME - implement snapshot
536     Carp::croak "Not implemented.";
537 }
538 sub opml : Local {
539     # FIXME - implement snapshot
540     Carp::croak "Not implemented.";
541 }
542 sub blame : Local {
543     # FIXME - implement snapshot
544     Carp::croak "Not implemented.";
545 }
546
547 =head2 end
548
549 Attempt to render a view, if needed.
550
551 =cut
552
553 sub end : ActionClass('RenderView') {
554     my ($self, $c) = @_;
555     # Give project views the current HEAD.
556     if ($c->stash->{Project}) {
557         $c->stash->{HEAD} = $c->stash->{Project}->head_hash;
558     }
559 }
560
561 sub error_404 :Private {
562     my ($self, $c) = @_;
563     $c->response->status(404);
564     $c->stash(
565         title => 'Page not found',
566         content => 'Page not found',
567     );
568 }
569
570 sub age_string {
571   my $age = shift;
572   my $age_str;
573
574   if ( $age > 60 * 60 * 24 * 365 * 2 ) {
575     $age_str  = ( int $age / 60 / 60 / 24 / 365 );
576     $age_str .= " years ago";
577   }
578   elsif ( $age > 60 * 60 * 24 * ( 365 / 12 ) * 2 ) {
579     $age_str  = int $age / 60 / 60 / 24 / ( 365 / 12 );
580     $age_str .= " months ago";
581   }
582   elsif ( $age > 60 * 60 * 24 * 7 * 2 ) {
583     $age_str  = int $age / 60 / 60 / 24 / 7;
584     $age_str .= " weeks ago";
585   }
586   elsif ( $age > 60 * 60 * 24 * 2 ) {
587     $age_str  = int $age / 60 / 60 / 24;
588     $age_str .= " days ago";
589   }
590   elsif ( $age > 60 * 60 * 2 ) {
591     $age_str  = int $age / 60 / 60;
592     $age_str .= " hours ago";
593   }
594   elsif ( $age > 60 * 2 ) {
595     $age_str  = int $age / 60;
596     $age_str .= " min ago";
597   }
598   elsif ( $age > 2 ) {
599     $age_str  = int $age;
600     $age_str .= " sec ago";
601   }
602   else {
603     $age_str .= " right now";
604   }
605   return $age_str;
606 }
607
608
609 =head1 AUTHOR
610
611 Dan Brook
612
613 =head1 LICENSE
614
615 This library is free software. You can redistribute it and/or modify
616 it under the same terms as Perl itself.
617
618 =cut
619
620 __PACKAGE__->meta->make_immutable;