Made SyntaxHighlight more generic so /commitdiff now gets highlighting too.
[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(sha1 => $commit->sha1, count => 16)],
121     refs      => $c->model('Git')->references,
122     heads     => [$c->model('Git')->heads],
123     HEAD      => $c->model('Git')->head_hash,
124     action    => 'summary',
125   );
126 }
127
128 =head2 heads
129
130 The current list of heads (aka branches) in the repo.
131
132 =cut
133
134 sub heads : Local {
135   my ( $self, $c ) = @_;
136
137   $c->stash(
138     commit => $self->_get_commit($c),
139     heads  => [$c->model('Git')->heads],
140     HEAD   => $c->model('Git')->head_hash,
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 commit
176
177 Exposes a given commit.
178
179 =cut
180
181 sub commit : Local {
182   my ( $self, $c ) = @_;
183
184   my $commit = $self->_get_commit($c);
185   $c->stash(
186       commit      => $commit,
187       diff_tree   => [$c->model('Git')->diff_tree($commit)],
188       branches_on => [$c->model('Git')->refs_for($commit->sha1)],
189       action      => 'commit',
190   );
191 }
192
193 =head2 commitdiff
194
195 Exposes a given diff of a commit.
196
197 =cut
198
199 sub commitdiff : Local {
200   my ( $self, $c ) = @_;
201
202   my $commit = $self->_get_commit($c);
203   my @difflist = $c->model('Git')->diff($commit->parent_sha1, $commit->sha1);
204   $c->stash(
205     commit    => $commit,
206     diff_tree => [$c->model('Git')->diff_tree($commit)],
207     diff      => \@difflist,
208     # XXX Hack hack hack, see View::SyntaxHighlight
209     blobs     => [map $_->{diff}, @difflist],
210     language  => 'Diff',
211     action    => 'commitdiff',
212   );
213
214   $c->forward('View::SyntaxHighlight');
215 }
216
217 =head2 shortlog
218
219 Expose an abbreviated log of a given sha1.
220
221 =cut
222
223 sub shortlog : Local {
224   my ( $self, $c ) = @_;
225
226   my $commit = $self->_get_commit($c);
227   # XXX Needs paging.
228   $c->stash(
229       commit    => $commit,
230       log_lines => [$c->model('Git')->list_revs(sha1 => $commit->sha1)],
231       refs      => $c->model('Git')->references,
232       action    => 'shortlog',
233   );
234 }
235
236 =head2 log
237
238 Calls shortlog internally. Perhaps that should be reversed ...
239
240 =cut
241 sub log : Local {
242     $_[0]->shortlog($_[1]);
243     $_[1]->stash->{action} = 'log';
244 }
245
246 =head2 tree
247
248 The tree of a given commit.
249
250 =cut
251
252 sub tree : Local {
253   my ( $self, $c ) = @_;
254
255   my $commit = $self->_get_commit($c);
256   $c->stash(
257       # XXX Useful defaults needed ...
258       commit    => $commit,
259       tree      => $c->model('Git')->get_object($c->req->param('hb')),
260       tree_list => [$c->model('Git')->list_tree($commit->sha1)],
261       action    => 'tree',
262   );
263 }
264
265 =head2 reflog
266
267 Expose the local reflog. This may go away.
268
269 =cut
270
271 sub reflog : Local {
272   my ( $self, $c ) = @_;
273
274   my @log = $c->model('Git')->reflog(
275       '--since=yesterday'
276   );
277
278   $c->stash(
279       log    => \@log,
280       action => 'reflog',
281   );
282 }
283
284 =head2 auto
285
286 Populate the header and footer. Perhaps not the best location.
287
288 =cut
289
290 sub auto : Private {
291     my($self, $c) = @_;
292
293     # XXX Temp hack until a decent solution is invented.
294     $c->model('Git')->project($c->req->param('p'))
295       if $c->req->param('p');
296
297     # Yes, this is hideous.
298     $self->header($c);
299     $self->footer($c);
300 }
301
302 # XXX This could probably be dropped altogether.
303 use Gitalist::Util qw(to_utf8);
304 # Formally git_header_html
305 sub header {
306   my($self, $c) = @_;
307
308   my $title = $c->config->{sitename};
309
310   my $project   = $c->req->param('project')  || $c->req->param('p');
311   my $action    = $c->req->param('action')   || $c->req->param('a');
312   my $file_name = $c->req->param('filename') || $c->req->param('f');
313   if(defined $project) {
314     $title .= " - " . to_utf8($project);
315     if (defined $action) {
316       $title .= "/$action";
317       if (defined $file_name) {
318         $title .= " - " . $file_name;
319         if ($action eq "tree" && $file_name !~ m|/$|) {
320           $title .= "/";
321         }
322       }
323     }
324   }
325
326   $c->stash->{version}     = $c->config->{version};
327   $c->stash->{git_version} = $c->model('Git')->run_cmd('--version');
328   $c->stash->{title}       = $title;
329
330   #$c->stash->{baseurl} = $ENV{PATH_INFO} && uri_escape($base_url);
331   $c->stash->{stylesheet} = $c->config->{stylesheet} || 'gitweb.css';
332
333   $c->stash->{project} = $project;
334   my @links;
335   if($project) {
336     my %href_params = $self->feed_info($c);
337     $href_params{'-title'} ||= 'log';
338
339     foreach my $format qw(RSS Atom) {
340       my $type = lc($format);
341       push @links, {
342         rel   => 'alternate',
343         title => "$project - $href_params{'-title'} - $format feed",
344
345         # XXX A bit hacky and could do with using gitweb::href() features
346         href  => "?a=$type;p=$project",
347         type  => "application/$type+xml"
348         }, {
349         rel   => 'alternate',
350
351         # XXX This duplication also feels a bit awkward
352         title => "$project - $href_params{'-title'} - $format feed (no merges)",
353         href  => "?a=$type;p=$project;opt=--no-merges",
354         type  => "application/$type+xml"
355         };
356     }
357   } else {
358     push @links, {
359       rel => "alternate",
360       title => $c->config->{sitename}." projects list",
361       href => '?a=project_index',
362       type => "text/plain; charset=utf-8"
363       }, {
364       rel => "alternate",
365       title => $c->config->{sitename}." projects feeds",
366       href => '?a=opml',
367       type => "text/plain; charset=utf-8"
368       };
369   }
370
371   $c->stash->{favicon} = $c->config->{favicon};
372
373   # </head><body>
374
375   $c->stash(
376     logo_url      => $c->config->{logo_url},
377     logo_label    => $c->config->{logo_label},
378     logo_img      => $c->config->{logo},
379     home_link     => $c->config->{home_link},
380     home_link_str => $c->config->{home_link_str},
381     );
382
383   if(defined $project) {
384     $c->stash(
385       search_text => ( $c->req->param('s') || $c->req->param('searchtext') ),
386       search_hash => ( $c->req->param('hb') || $c->req->param('hashbase')
387           || $c->req->param('h')  || $c->req->param('hash')
388           || 'HEAD' ),
389       );
390   }
391 }
392
393 # Formally git_footer_html
394 sub footer {
395   my($self, $c) = @_;
396
397   my $feed_class = 'rss_logo';
398
399   my @feeds;
400   my $project = $c->req->param('project')  || $c->req->param('p');
401   if(defined $project) {
402     (my $pstr = $project) =~ s[/?\.git$][];
403     my $descr = $c->model('Git')->project_info($project)->{description};
404     $c->stash->{project_description} = defined $descr
405       ? $descr
406       : '';
407
408     my %href_params = $self->feed_info($c);
409     if (!%href_params) {
410       $feed_class .= ' generic';
411     }
412     $href_params{'-title'} ||= 'log';
413
414     @feeds = [
415       map +{
416         class => $feed_class,
417         title => "$href_params{'-title'} $_ feed",
418         href  => "/?p=$project;a=\L$_",
419         name  => lc $_,
420         }, qw(RSS Atom)
421       ];
422   } else {
423     @feeds = [
424       map {
425         class => $feed_class,
426           title => '',
427           href  => "/?a=$_->[0]",
428           name  => $_->[1],
429         }, [opml=>'OPML'],[project_index=>'TXT'],
430       ];
431   }
432 }
433
434 # XXX This feels wrong here, should probably be refactored.
435 # returns hash to be passed to href to generate gitweb URL
436 # in -title key it returns description of link
437 sub feed_info {
438   my($self, $c) = @_;
439
440   my $format = shift || 'Atom';
441   my %res = (action => lc($format));
442
443   # feed links are possible only for project views
444   return unless $c->req->param('project');
445
446   # some views should link to OPML, or to generic project feed,
447   # or don't have specific feed yet (so they should use generic)
448   return if $c->req->param('action') =~ /^(?:tags|heads|forks|tag|search)$/x;
449
450   my $branch;
451   my $hash = $c->req->param('h')  || $c->req->param('hash');
452   my $hash_base = $c->req->param('hb') || $c->req->param('hashbase');
453
454   # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
455   # from tag links; this also makes possible to detect branch links
456   if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
457     (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
458     $branch = $1;
459   }
460
461   # find log type for feed description (title)
462   my $type = 'log';
463   my $file_name = $c->req->param('f') || $c->req->param('filename');
464   if (defined $file_name) {
465     $type  = "history of $file_name";
466     $type .= "/" if $c->req->param('action') eq 'tree';
467     $type .= " on '$branch'" if (defined $branch);
468   } else {
469     $type = "log of $branch" if (defined $branch);
470   }
471
472   $res{-title} = $type;
473   $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
474   $res{'file_name'} = $file_name;
475
476   return %res;
477 }
478 =head2 end
479
480 Attempt to render a view, if needed.
481
482 =cut
483
484 sub end : ActionClass('RenderView') {}
485
486 =head1 AUTHOR
487
488 Dan Brook,,,
489
490 =head1 LICENSE
491
492 This library is free software. You can redistribute it and/or modify
493 it under the same terms as Perl itself.
494
495 =cut
496
497 __PACKAGE__->meta->make_immutable;