822f3b51e0e9a92735487ecc5959b9b103abcf92
[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 main shim around C<gitweb.pm>.
34
35 =cut
36
37 sub run_gitweb {
38   my ( $self, $c ) = @_;
39
40   # XXX A slippery slope to be sure.
41   if($c->req->param('a')) {
42     my $capture = IO::Capture::Stdout->new();
43     $capture->start();
44     eval {
45       my $action = gitweb::main($c);
46       $action->();
47     };
48     $capture->stop();
49     
50     use Data::Dumper;
51     die Dumper($@)
52       if $@;
53   
54     my $output = join '', $capture->read;
55     $c->stash->{gitweb_output} = $output;
56     $c->stash->{template} = 'gitweb.tt2';
57   }
58 }
59
60 sub _get_commit {
61   my($self, $c) = @_;
62
63   my $h = $c->req->param('h');
64   my $f = $c->req->param('f');
65   my $m = $c->model('Git');
66
67   # Either use the provided h(ash) parameter, the f(ile) parameter or just use HEAD.
68   my $hash = ($h =~ /[^a-f0-9]/ ? $m->head_hash($h) : $h)
69           || ($f && $m->hash_by_path($f))
70           || $m->head_hash
71           # XXX This could definitely use more context.
72           || Carp::croak("Couldn't find a hash for the commit object!");
73
74     
75   (my $pd = $m->project_dir( $m->project )) =~ s{/\.git$}();
76   my $commit = $m->get_object($hash)
77     or Carp::croak("Couldn't find a commit object for '$hash' in '$pd'!");
78
79   return $commit;
80 }
81
82 =head2 index
83
84 Provides the project listing.
85
86 =cut
87
88 sub index :Path :Args(0) {
89   my ( $self, $c ) = @_;
90
91   # Leave actions up to gitweb at this point.
92   return $self->run_gitweb($c)
93     if $c->req->param('a');
94
95   my $list = $c->model('Git')->list_projects;
96   unless(@$list) {
97     die "No projects found";
98   }
99
100   $c->stash(
101     searchtext => $c->req->param('searchtext') || '',
102     projects   => $list,
103     action     => 'index',
104   );
105 }
106
107 =head2 summary
108
109 A summary of what's happening in the repo.
110
111 =cut
112
113 sub summary : Local {
114   my ( $self, $c ) = @_;
115
116   my $commit = $self->_get_commit($c);
117   $c->stash(
118     commit    => $commit,
119     info      => $c->model('Git')->project_info($c->model('Git')->project),
120     log_lines => [$c->model('Git')->list_revs(
121       sha1 => $commit->sha1, count => Gitalist->config->{paging}{summary}
122     )],
123     refs      => $c->model('Git')->references,
124     heads     => [$c->model('Git')->heads],
125     action    => 'summary',
126   );
127 }
128
129 =head2 heads
130
131 The current list of heads (aka branches) in the repo.
132
133 =cut
134
135 sub heads : Local {
136   my ( $self, $c ) = @_;
137
138   $c->stash(
139     commit => $self->_get_commit($c),
140     heads  => [$c->model('Git')->heads],
141     action => 'heads',
142   );
143 }
144
145 =head2 blob
146
147 The blob action i.e the contents of a file.
148
149 =cut
150
151 sub blob : Local {
152   my ( $self, $c ) = @_;
153
154   my $h  = $c->req->param('h')
155        || $c->model('Git')->hash_by_path($c->req->param('f'))
156        || die "No file or sha1 provided.";
157   my $hb = $c->req->param('hb')
158        || $c->model('Git')->head_hash
159        || die "Couldn't discern the corresponding head.";
160
161   my $filename = $c->req->param('f') || '';
162
163   $c->stash(
164     blob     => $c->model('Git')->get_object($h)->content,
165     head     => $c->model('Git')->get_object($hb),
166     filename => $filename,
167     # XXX Hack hack hack, see View::SyntaxHighlight
168     language => ($filename =~ /\.p[lm]$/ ? 'Perl' : ''),
169     action   => 'blob',
170   );
171
172   $c->forward('View::SyntaxHighlight');
173 }
174
175 =head2 blobdiff
176
177 Exposes a given diff of a blob.
178
179 =cut
180
181 sub blobdiff : Local {
182   my ( $self, $c ) = @_;
183
184   my $commit = $self->_get_commit($c);
185   my $filename = $c->req->param('f')
186               || croak("No file specified!");
187   my($tree, $patch) = $c->model('Git')->diff(
188     commit => $commit,
189     parent => $c->req->param('hp') || '',
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
214   my $commit = $self->_get_commit($c);
215   $c->stash(
216       commit      => $commit,
217       diff_tree   => ($c->model('Git')->diff(commit => $commit))[0],
218       branches_on => [$c->model('Git')->refs_for($commit->sha1)],
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
232   my $commit = $self->_get_commit($c);
233   my($tree, $patch) = $c->model('Git')->diff(
234       commit => $commit,
235       parent => $c->req->param('hp') || '',
236       patch  => 1,
237   );
238   $c->stash(
239     commit    => $commit,
240     diff_tree => $tree,
241     diff      => $patch,
242     # XXX Hack hack hack, see View::SyntaxHighlight
243     blobs     => [map $_->{diff}, @$patch],
244     language  => 'Diff',
245     action    => 'commitdiff',
246   );
247
248   $c->forward('View::SyntaxHighlight');
249 }
250
251 =head2 shortlog
252
253 Expose an abbreviated log of a given sha1.
254
255 =cut
256
257 sub shortlog : Local {
258   my ( $self, $c ) = @_;
259
260   my $commit  = $self->_get_commit($c);
261   my %logargs = (
262       sha1   => $commit->sha1,
263       count  => Gitalist->config->{paging}{log},
264       ($c->req->param('f') ? (file => $c->req->param('f')) : ())
265   );
266
267   my $page = $c->req->param('pg') || 0;
268   $logargs{skip} = $c->req->param('pg') * $logargs{count}
269     if $c->req->param('pg');
270
271   $c->stash(
272       commit    => $commit,
273       log_lines => [$c->model('Git')->list_revs(%logargs)],
274       refs      => $c->model('Git')->references,
275       action    => 'shortlog',
276       page      => $page + 1,
277   );
278 }
279
280 =head2 log
281
282 Calls shortlog internally. Perhaps that should be reversed ...
283
284 =cut
285 sub log : Local {
286     $_[0]->shortlog($_[1]);
287     $_[1]->stash->{action} = 'log';
288 }
289
290 =head2 tree
291
292 The tree of a given commit.
293
294 =cut
295
296 sub tree : Local {
297   my ( $self, $c ) = @_;
298
299   my $commit = $self->_get_commit($c);
300   $c->stash(
301       # XXX Useful defaults needed ...
302       commit    => $commit,
303       tree      => $c->model('Git')->get_object($c->req->param('hb')),
304       tree_list => [$c->model('Git')->list_tree($commit->sha1)],
305       action    => 'tree',
306   );
307 }
308
309 =head2 reflog
310
311 Expose the local reflog. This may go away.
312
313 =cut
314
315 sub reflog : Local {
316   my ( $self, $c ) = @_;
317
318   my @log = $c->model('Git')->reflog(
319       '--since=yesterday'
320   );
321
322   $c->stash(
323       log    => \@log,
324       action => 'reflog',
325   );
326 }
327
328 =head2 auto
329
330 Populate the header and footer. Perhaps not the best location.
331
332 =cut
333
334 sub auto : Private {
335     my($self, $c) = @_;
336
337     # Yes, this is hideous.
338     $self->header($c);
339     $self->footer($c);
340 }
341
342 # XXX This could probably be dropped altogether.
343 use Gitalist::Util qw(to_utf8);
344 # Formally git_header_html
345 sub header {
346   my($self, $c) = @_;
347
348   my $title = $c->config->{sitename};
349
350   my $project   = $c->req->param('project')  || $c->req->param('p');
351   my $action    = $c->req->param('action')   || $c->req->param('a');
352   my $file_name = $c->req->param('filename') || $c->req->param('f');
353   if(defined $project) {
354     $title .= " - " . to_utf8($project);
355     if (defined $action) {
356       $title .= "/$action";
357       if (defined $file_name) {
358         $title .= " - " . $file_name;
359         if ($action eq "tree" && $file_name !~ m|/$|) {
360           $title .= "/";
361         }
362       }
363     }
364   }
365
366   $c->stash->{version}     = $c->config->{version};
367   $c->stash->{git_version} = $c->model('Git')->run_cmd('--version');
368   $c->stash->{title}       = $title;
369
370   #$c->stash->{baseurl} = $ENV{PATH_INFO} && uri_escape($base_url);
371   $c->stash->{stylesheet} = $c->config->{stylesheet} || 'gitweb.css';
372
373   $c->stash->{project} = $project;
374   my @links;
375   if($project) {
376     my %href_params = $self->feed_info($c);
377     $href_params{'-title'} ||= 'log';
378
379     foreach my $format qw(RSS Atom) {
380       my $type = lc($format);
381       push @links, {
382         rel   => 'alternate',
383         title => "$project - $href_params{'-title'} - $format feed",
384
385         # XXX A bit hacky and could do with using gitweb::href() features
386         href  => "?a=$type;p=$project",
387         type  => "application/$type+xml"
388         }, {
389         rel   => 'alternate',
390
391         # XXX This duplication also feels a bit awkward
392         title => "$project - $href_params{'-title'} - $format feed (no merges)",
393         href  => "?a=$type;p=$project;opt=--no-merges",
394         type  => "application/$type+xml"
395         };
396     }
397   } else {
398     push @links, {
399       rel => "alternate",
400       title => $c->config->{sitename}." projects list",
401       href => '?a=project_index',
402       type => "text/plain; charset=utf-8"
403       }, {
404       rel => "alternate",
405       title => $c->config->{sitename}." projects feeds",
406       href => '?a=opml',
407       type => "text/plain; charset=utf-8"
408       };
409   }
410
411   $c->stash->{favicon} = $c->config->{favicon};
412
413   # </head><body>
414
415   $c->stash(
416     logo_url      => $c->config->{logo_url},
417     logo_label    => $c->config->{logo_label},
418     logo_img      => $c->config->{logo},
419     home_link     => $c->config->{home_link},
420     home_link_str => $c->config->{home_link_str},
421     );
422
423   if(defined $project) {
424     $c->stash(
425       search_text => ( $c->req->param('s') || $c->req->param('searchtext') ),
426       search_hash => ( $c->req->param('hb') || $c->req->param('hashbase')
427           || $c->req->param('h')  || $c->req->param('hash')
428           || 'HEAD' ),
429       );
430   }
431 }
432
433 # Formally git_footer_html
434 sub footer {
435   my($self, $c) = @_;
436
437   my $feed_class = 'rss_logo';
438
439   my @feeds;
440   my $project = $c->req->param('project')  || $c->req->param('p');
441   if(defined $project) {
442     (my $pstr = $project) =~ s[/?\.git$][];
443     my $descr = $c->model('Git')->project_info($project)->{description};
444     $c->stash->{project_description} = defined $descr
445       ? $descr
446       : '';
447
448     my %href_params = $self->feed_info($c);
449     if (!%href_params) {
450       $feed_class .= ' generic';
451     }
452     $href_params{'-title'} ||= 'log';
453
454     @feeds = [
455       map +{
456         class => $feed_class,
457         title => "$href_params{'-title'} $_ feed",
458         href  => "/?p=$project;a=\L$_",
459         name  => lc $_,
460         }, qw(RSS Atom)
461       ];
462   } else {
463     @feeds = [
464       map {
465         class => $feed_class,
466           title => '',
467           href  => "/?a=$_->[0]",
468           name  => $_->[1],
469         }, [opml=>'OPML'],[project_index=>'TXT'],
470       ];
471   }
472 }
473
474 # XXX This feels wrong here, should probably be refactored.
475 # returns hash to be passed to href to generate gitweb URL
476 # in -title key it returns description of link
477 sub feed_info {
478   my($self, $c) = @_;
479
480   my $format = shift || 'Atom';
481   my %res = (action => lc($format));
482
483   # feed links are possible only for project views
484   return unless $c->req->param('project');
485
486   # some views should link to OPML, or to generic project feed,
487   # or don't have specific feed yet (so they should use generic)
488   return if $c->req->param('action') =~ /^(?:tags|heads|forks|tag|search)$/x;
489
490   my $branch;
491   my $hash = $c->req->param('h')  || $c->req->param('hash');
492   my $hash_base = $c->req->param('hb') || $c->req->param('hashbase');
493
494   # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
495   # from tag links; this also makes possible to detect branch links
496   if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
497     (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
498     $branch = $1;
499   }
500
501   # find log type for feed description (title)
502   my $type = 'log';
503   my $file_name = $c->req->param('f') || $c->req->param('filename');
504   if (defined $file_name) {
505     $type  = "history of $file_name";
506     $type .= "/" if $c->req->param('action') eq 'tree';
507     $type .= " on '$branch'" if (defined $branch);
508   } else {
509     $type = "log of $branch" if (defined $branch);
510   }
511
512   $res{-title} = $type;
513   $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
514   $res{'file_name'} = $file_name;
515
516   return %res;
517 }
518 =head2 end
519
520 Attempt to render a view, if needed.
521
522 =cut
523
524 sub end : ActionClass('RenderView') {
525   # Give every view the current HEAD.
526   $_[1]->stash->{HEAD} = $_[1]->model('Git')->head_hash;
527 }
528
529 =head1 AUTHOR
530
531 Dan Brook,,,
532
533 =head1 LICENSE
534
535 This library is free software. You can redistribute it and/or modify
536 it under the same terms as Perl itself.
537
538 =cut
539
540 __PACKAGE__->meta->make_immutable;