Started the switch from hand-written URLs to uri_for.
[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, $haveh) = @_;
62
63   my $h = $haveh || $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 in ".Gitalist->config->{repodir};
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,
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, $c->req->param('hb'));
300   my $tree   = $c->model('Git')->get_object($c->req->param('h') || $commit->tree_sha1);
301   $c->stash(
302       # XXX Useful defaults needed ...
303       commit    => $commit,
304       tree      => $tree,
305       tree_list => [$c->model('Git')->list_tree($tree->sha1)],
306           path      => $c->req->param('f') || '',
307       action    => 'tree',
308   );
309 }
310
311 =head2 reflog
312
313 Expose the local reflog. This may go away.
314
315 =cut
316
317 sub reflog : Local {
318   my ( $self, $c ) = @_;
319
320   my @log = $c->model('Git')->reflog(
321       '--since=yesterday'
322   );
323
324   $c->stash(
325       log    => \@log,
326       action => 'reflog',
327   );
328 }
329
330 sub search : Local {
331     Carp::croak "Not implemented.";
332 }
333
334 sub search_help : Local {
335     Carp::croak "Not implemented.";
336 }
337
338 =head2 auto
339
340 Populate the header and footer. Perhaps not the best location.
341
342 =cut
343
344 sub auto : Private {
345     my($self, $c) = @_;
346
347     # Yes, this is hideous.
348     $self->header($c);
349     $self->footer($c);
350 }
351
352 # XXX This could probably be dropped altogether.
353 use Gitalist::Util qw(to_utf8);
354 # Formally git_header_html
355 sub header {
356   my($self, $c) = @_;
357
358   my $title = $c->config->{sitename};
359
360   my $project   = $c->req->param('project')  || $c->req->param('p');
361   my $action    = $c->req->param('action')   || $c->req->param('a');
362   my $file_name = $c->req->param('filename') || $c->req->param('f');
363   if(defined $project) {
364     $title .= " - " . to_utf8($project);
365     if (defined $action) {
366       $title .= "/$action";
367       if (defined $file_name) {
368         $title .= " - " . $file_name;
369         if ($action eq "tree" && $file_name !~ m|/$|) {
370           $title .= "/";
371         }
372       }
373     }
374   }
375
376   $c->stash->{version}     = $c->config->{version};
377   $c->stash->{git_version} = $c->model('Git')->run_cmd('--version');
378   $c->stash->{title}       = $title;
379
380   #$c->stash->{baseurl} = $ENV{PATH_INFO} && uri_escape($base_url);
381   $c->stash->{stylesheet} = $c->config->{stylesheet} || 'gitweb.css';
382
383   $c->stash->{project} = $project;
384   my @links;
385   if($project) {
386     my %href_params = $self->feed_info($c);
387     $href_params{'-title'} ||= 'log';
388
389     foreach my $format qw(RSS Atom) {
390       my $type = lc($format);
391       push @links, {
392         rel   => 'alternate',
393         title => "$project - $href_params{'-title'} - $format feed",
394
395         # XXX A bit hacky and could do with using gitweb::href() features
396         href  => "?a=$type;p=$project",
397         type  => "application/$type+xml"
398         }, {
399         rel   => 'alternate',
400
401         # XXX This duplication also feels a bit awkward
402         title => "$project - $href_params{'-title'} - $format feed (no merges)",
403         href  => "?a=$type;p=$project;opt=--no-merges",
404         type  => "application/$type+xml"
405         };
406     }
407   } else {
408     push @links, {
409       rel => "alternate",
410       title => $c->config->{sitename}." projects list",
411       href => '?a=project_index',
412       type => "text/plain; charset=utf-8"
413       }, {
414       rel => "alternate",
415       title => $c->config->{sitename}." projects feeds",
416       href => '?a=opml',
417       type => "text/plain; charset=utf-8"
418       };
419   }
420
421   $c->stash->{favicon} = $c->config->{favicon};
422
423   # </head><body>
424
425   $c->stash(
426     logo_url      => $c->config->{logo_url},
427     logo_label    => $c->config->{logo_label},
428     logo_img      => $c->config->{logo},
429     home_link     => $c->config->{home_link},
430     home_link_str => $c->config->{home_link_str},
431     );
432
433   if(defined $project) {
434     $c->stash(
435       search_text => ( $c->req->param('s') || $c->req->param('searchtext') || ''),
436       search_hash => ( $c->req->param('hb') || $c->req->param('hashbase')
437           || $c->req->param('h')  || $c->req->param('hash')
438           || 'HEAD' ),
439       );
440   }
441 }
442
443 # Formally git_footer_html
444 sub footer {
445   my($self, $c) = @_;
446
447   my $feed_class = 'rss_logo';
448
449   my @feeds;
450   my $project = $c->req->param('project')  || $c->req->param('p');
451   if(defined $project) {
452     (my $pstr = $project) =~ s[/?\.git$][];
453     my $descr = $c->model('Git')->project_info($project)->{description};
454     $c->stash->{project_description} = defined $descr
455       ? $descr
456       : '';
457
458     my %href_params = $self->feed_info($c);
459     if (!%href_params) {
460       $feed_class .= ' generic';
461     }
462     $href_params{'-title'} ||= 'log';
463
464     @feeds = [
465       map +{
466         class => $feed_class,
467         title => "$href_params{'-title'} $_ feed",
468         href  => "/?p=$project;a=\L$_",
469         name  => lc $_,
470         }, qw(RSS Atom)
471       ];
472   } else {
473     @feeds = [
474       map {
475         class => $feed_class,
476           title => '',
477           href  => "/?a=$_->[0]",
478           name  => $_->[1],
479         }, [opml=>'OPML'],[project_index=>'TXT'],
480       ];
481   }
482 }
483
484 # XXX This feels wrong here, should probably be refactored.
485 # returns hash to be passed to href to generate gitweb URL
486 # in -title key it returns description of link
487 sub feed_info {
488   my($self, $c) = @_;
489
490   my $format = shift || 'Atom';
491   my %res = (action => lc($format));
492
493   # feed links are possible only for project views
494   return unless $c->req->param('project');
495
496   # some views should link to OPML, or to generic project feed,
497   # or don't have specific feed yet (so they should use generic)
498   return if $c->req->param('action') =~ /^(?:tags|heads|forks|tag|search)$/x;
499
500   my $branch;
501   my $hash = $c->req->param('h')  || $c->req->param('hash');
502   my $hash_base = $c->req->param('hb') || $c->req->param('hashbase');
503
504   # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
505   # from tag links; this also makes possible to detect branch links
506   if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
507     (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
508     $branch = $1;
509   }
510
511   # find log type for feed description (title)
512   my $type = 'log';
513   my $file_name = $c->req->param('f') || $c->req->param('filename');
514   if (defined $file_name) {
515     $type  = "history of $file_name";
516     $type .= "/" if $c->req->param('action') eq 'tree';
517     $type .= " on '$branch'" if (defined $branch);
518   } else {
519     $type = "log of $branch" if (defined $branch);
520   }
521
522   $res{-title} = $type;
523   $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
524   $res{'file_name'} = $file_name;
525
526   return %res;
527 }
528 =head2 end
529
530 Attempt to render a view, if needed.
531
532 =cut
533
534 sub end : ActionClass('RenderView') {
535   # Give every view the current HEAD.
536   $_[1]->stash->{HEAD} = $_[1]->model('Git')->head_hash;
537 }
538
539 =head1 AUTHOR
540
541 Dan Brook,,,
542
543 =head1 LICENSE
544
545 This library is free software. You can redistribute it and/or modify
546 it under the same terms as Perl itself.
547
548 =cut
549
550 __PACKAGE__->meta->make_immutable;