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