e6d9edbf4d35aa323d8ccba9351c6e0712dc3ced
[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     my ($self, $c) = @_;
486     my $format = $c->req->param('sf') || 'tgz';
487     die unless $format;
488     my $sha1 = $c->req->param('h') || $self->_get_object($c)->sha1;
489     my @snap = $c->stash->{Project}->snapshot(
490         sha1 => $sha1,
491         format => $format
492     );
493     $c->response->status(200);
494     $c->response->headers->header( 'Content-Disposition' =>
495                                        "attachment; filename=$snap[0]");
496     $c->response->body($snap[1]);
497 }
498
499 =head2 auto
500
501 Populate the header and footer. Perhaps not the best location.
502
503 =cut
504
505 sub auto : Private {
506   my($self, $c) = @_;
507
508   my $project = $c->req->param('p');
509   if (defined $project) {
510     eval {
511       $c->stash(Project => $c->model('GitRepos')->project($project));
512     };
513     if ($@) {
514       $c->detach('error_404');
515     }
516   }
517
518   my $a_project = $c->stash->{Project} || $c->model()->projects->[0];
519   $c->stash(
520     git_version => $a_project->run_cmd('--version'),
521     version     => $Gitalist::VERSION,
522
523     # XXX Move these to a plugin!
524     time_since => sub {
525       return 'never' unless $_[0];
526       return age_string(time - $_[0]->epoch);
527     },
528     short_cmt => sub {
529       my $cmt = shift;
530       my($line) = split /\n/, $cmt;
531       $line =~ s/^(.{70,80}\b).*/$1 …/;
532       return $line;
533     },
534     abridged_description => sub {
535         join(' ', grep { defined } (split / /, shift)[0..10]);
536     },
537   );
538 }
539
540 sub tags : Local {
541     # FIXME - implement snapshot
542     Carp::croak "Not implemented.";
543 }
544 sub project_index : Local {
545     # FIXME - implement snapshot
546     Carp::croak "Not implemented.";
547 }
548 sub opml : Local {
549     # FIXME - implement snapshot
550     Carp::croak "Not implemented.";
551 }
552 sub blame : Local {
553     # FIXME - implement snapshot
554     Carp::croak "Not implemented.";
555 }
556
557 =head2 end
558
559 Attempt to render a view, if needed.
560
561 =cut
562
563 sub end : ActionClass('RenderView') {
564     my ($self, $c) = @_;
565     # Give project views the current HEAD.
566     if ($c->stash->{Project}) {
567         $c->stash->{HEAD} = $c->stash->{Project}->head_hash;
568     }
569 }
570
571 sub error_404 :Private {
572     my ($self, $c) = @_;
573     $c->response->status(404);
574     $c->stash(
575         title => 'Page not found',
576         content => 'Page not found',
577     );
578 }
579
580 sub age_string {
581   my $age = shift;
582   my $age_str;
583
584   if ( $age > 60 * 60 * 24 * 365 * 2 ) {
585     $age_str  = ( int $age / 60 / 60 / 24 / 365 );
586     $age_str .= " years ago";
587   }
588   elsif ( $age > 60 * 60 * 24 * ( 365 / 12 ) * 2 ) {
589     $age_str  = int $age / 60 / 60 / 24 / ( 365 / 12 );
590     $age_str .= " months ago";
591   }
592   elsif ( $age > 60 * 60 * 24 * 7 * 2 ) {
593     $age_str  = int $age / 60 / 60 / 24 / 7;
594     $age_str .= " weeks ago";
595   }
596   elsif ( $age > 60 * 60 * 24 * 2 ) {
597     $age_str  = int $age / 60 / 60 / 24;
598     $age_str .= " days ago";
599   }
600   elsif ( $age > 60 * 60 * 2 ) {
601     $age_str  = int $age / 60 / 60;
602     $age_str .= " hours ago";
603   }
604   elsif ( $age > 60 * 2 ) {
605     $age_str  = int $age / 60;
606     $age_str .= " min ago";
607   }
608   elsif ( $age > 2 ) {
609     $age_str  = int $age;
610     $age_str .= " sec ago";
611   }
612   else {
613     $age_str .= " right now";
614   }
615   return $age_str;
616 }
617
618
619 =head1 AUTHOR
620
621 Dan Brook
622
623 =head1 LICENSE
624
625 This library is free software. You can redistribute it and/or modify
626 it under the same terms as Perl itself.
627
628 =cut
629
630 __PACKAGE__->meta->make_immutable;