Trivial tidyups.
[catagits/Gitalist.git] / lib / Gitalist / Controller / Root.pm
1 package Gitalist::Controller::Root;
2
3 use Moose;
4 use Moose::Autobox;
5 use Sys::Hostname ();
6 use XML::Atom::Feed;
7 use XML::Atom::Entry;
8 use XML::RSS;
9 use XML::OPML::SimpleGen;
10
11 use Gitalist::Utils qw/ age_string /;
12
13 use namespace::autoclean;
14
15 BEGIN { extends 'Catalyst::Controller' }
16
17 __PACKAGE__->config->{namespace} = '';
18
19 sub root : Chained('/') PathPart('') CaptureArgs(0) {}
20
21 sub _get_object {
22   my($self, $c, $haveh) = @_;
23
24   my $h = $haveh || $c->req->param('h') || '';
25   my $f = $c->req->param('f');
26
27   my $m = $c->stash->{Repository};
28   my $pd = $m->path;
29
30   # Either use the provided h(ash) parameter, the f(ile) parameter or just use HEAD.
31   my $hash = ($h =~ /[^a-f0-9]/ ? $m->head_hash($h) : $h)
32           || ($f && $m->hash_by_path($f))
33           || $m->head_hash
34           # XXX This could definitely use more context.
35           || Carp::croak("Couldn't find a hash for the commit object!");
36
37   my $obj = $m->get_object($hash)
38     or Carp::croak("Couldn't find a object for '$hash' in '$pd'!");
39
40   return $obj;
41 }
42
43 sub index : Chained('base') PathPart('') Args(0) {
44   my ( $self, $c ) = @_;
45
46   $c->detach($c->req->param('a'))
47     if $c->req->param('a');
48
49   my $search = $c->req->param('s') || '';
50
51   $c->stash(
52     search_text => $search,
53   );
54 }
55
56 sub blame : Chained('base') Args(0) {
57   my($self, $c) = @_;
58
59   my $repository = $c->stash->{Repository};
60   my $h  = $c->req->param('h')
61        || $repository->hash_by_path($c->req->param('hb'), $c->req->param('f'))
62        || die "No file or sha1 provided.";
63   my $hb = $c->req->param('hb')
64        || $repository->head_hash
65        || die "Couldn't discern the corresponding head.";
66   my $filename = $c->req->param('f') || '';
67
68   my $blame = $repository->get_object($hb)->blame($filename, $h);
69   $c->stash(
70     blame    => $blame,
71     head     => $repository->get_object($hb),
72     filename => $filename,
73
74     # XXX Hack hack hack, see View::SyntaxHighlight
75     language => ($filename =~ /\.p[lm]$/i ? 'Perl' : ''),
76     blob     => join("\n", map $_->{line}, @$blame),
77   );
78
79   $c->forward('View::SyntaxHighlight')
80     unless $c->stash->{no_wrapper};
81 }
82
83 sub _blob_objs {
84   my ( $self, $c ) = @_;
85   my $repository = $c->stash->{Repository};
86   my $h  = $c->req->param('h')
87        || $repository->hash_by_path($c->req->param('hb'), $c->req->param('f'))
88        || die "No file or sha1 provided.";
89   my $hb = $c->req->param('hb')
90        || $repository->head_hash
91        || die "Couldn't discern the corresponding head.";
92
93   my $filename = $c->req->param('f') || '';
94
95   my $blob = $repository->get_object($h);
96   $blob = $repository->get_object(
97     $repository->hash_by_path($h || $hb, $filename)
98   ) if $blob->type ne 'blob';
99
100   return $blob, $repository->get_object($hb), $filename;
101 }
102
103 =head2 blob
104
105 The blob action i.e the contents of a file.
106
107 =cut
108
109 sub blob : Chained('base') Args(0) {
110   my ( $self, $c ) = @_;
111
112   my($blob, $head, $filename) = $self->_blob_objs($c);
113   $c->stash(
114     blob     => $blob->content,
115     head     => $head,
116     filename => $filename,
117     # XXX Hack hack hack, see View::SyntaxHighlight
118     language => ($filename =~ /\.p[lm]$/i ? 'Perl' : ''),
119   );
120
121   $c->forward('View::SyntaxHighlight')
122     unless $c->stash->{no_wrapper};
123 }
124
125 =head2 blob_plain
126
127 The plain text version of blob, where file is rendered as is.
128
129 =cut
130
131 sub blob_plain : Chained('base') Args(0) {
132   my($self, $c) = @_;
133
134   my($blob) = $self->_blob_objs($c);
135   $c->response->content_type('text/plain; charset=utf-8');
136   $c->response->body($blob->content);
137   $c->response->status(200);
138 }
139
140 =head2 blobdiff_plain
141
142 The plain text version of blobdiff.
143
144 =cut
145
146 sub blobdiff_plain : Chained('base') Args(0) {
147   my($self, $c) = @_;
148
149   $c->stash(no_wrapper => 1);
150   $c->response->content_type('text/plain; charset=utf-8');
151
152   $c->forward('blobdiff');
153 }
154
155 =head2 blobdiff
156
157 Exposes a given diff of a blob.
158
159 =cut
160
161 sub blobdiff : Chained('base') Args(0) {
162   my ( $self, $c ) = @_;
163   my $commit = $self->_get_object($c, $c->req->param('hb'));
164   my $filename = $c->req->param('f')
165               || croak("No file specified!");
166   my($tree, $patch) = $c->stash->{Repository}->diff(
167     commit => $commit,
168     patch  => 1,
169     parent => $c->req->param('hpb') || undef,
170     file   => $filename,
171   );
172   $c->stash(
173     commit    => $commit,
174     diff      => $patch,
175     filename  => $filename,
176     # XXX Hack hack hack, see View::SyntaxHighlight
177     blobs     => [$patch->[0]->{diff}],
178     language  => 'Diff',
179   );
180
181   $c->forward('View::SyntaxHighlight')
182     unless $c->stash->{no_wrapper};
183 }
184
185 =head2 commit
186
187 Exposes a given commit.
188
189 =cut
190
191 sub commit : Chained('base') Args(0) {
192   my ( $self, $c ) = @_;
193   my $repository = $c->stash->{Repository};
194   my $commit = $self->_get_object($c);
195   $c->stash(
196       commit      => $commit,
197       diff_tree   => ($repository->diff(commit => $commit))[0],
198       refs      => $repository->references,
199   );
200 }
201
202 # For legacy support.
203 sub history : Chained('base') Args(0) {
204     my ( $self, $c ) = @_;
205     $self->shortlog($c);
206     my $repository = $c->stash->{Repository};
207     my $file = $repository->get_object(
208         $repository->hash_by_path(
209             $repository->head_hash,
210             $c->stash->{filename}
211         )
212     );
213      $c->stash(
214                filetype => $file->type,
215            );
216 }
217
218 =head2 tree
219
220 The tree of a given commit.
221
222 =cut
223
224 sub tree : Chained('base') Args(0) {
225   my ( $self, $c ) = @_;
226   my $repository = $c->stash->{Repository};
227   my $commit  = $self->_get_object($c, $c->req->param('hb'));
228   my $filename = $c->req->param('f') || '';
229   my $tree    = $filename
230     ? $repository->get_object($repository->hash_by_path($commit->sha1, $filename))
231     : $repository->get_object($commit->tree_sha1)
232   ;
233   $c->stash(
234       commit    => $commit,
235       tree      => $tree,
236       tree_list => [$repository->list_tree($tree->sha1)],
237       path      => $c->req->param('f') || '',
238   );
239 }
240
241 =head2 reflog
242
243 Expose the local reflog. This may go away.
244
245 =cut
246
247 sub reflog : Chained('base') Args(0) {
248   my ( $self, $c ) = @_;
249   my @log = $c->stash->{Repository}->reflog(
250       '--since=yesterday'
251   );
252
253   $c->stash(
254       log    => \@log,
255   );
256 }
257
258 =head2 search
259
260 The action for the search form.
261
262 =cut
263
264 sub search : Chained('base') Args(0) {
265   my($self, $c) = @_;
266   my $repository = $c->stash->{Repository};
267   my $commit  = $self->_get_object($c);
268   # Lifted from /shortlog.
269   my %logargs = (
270     sha1   => $commit->sha1,
271     count  => Gitalist->config->{paging}{log},
272     ($c->req->param('f') ? (file => $c->req->param('f')) : ()),
273     search => {
274       type   => $c->req->param('type'),
275       text   => $c->req->param('text'),
276       regexp => $c->req->param('regexp') || 0,
277     },
278   );
279
280   $c->stash(
281       commit  => $commit,
282       results => [$repository->list_revs(%logargs)],
283           # This could be added - page      => $page,
284   );
285 }
286
287 =head2 search_help
288
289 Provides some help for the search form.
290
291 =cut
292
293 sub search_help : Chained('base') Args(0) {
294     my ($self, $c) = @_;
295     $c->stash(template => 'search_help.tt2');
296 }
297
298 =head2 atom
299
300 Provides an atom feed for a given repository.
301
302 =cut
303
304 sub atom : Chained('base') Args(0) {
305   my($self, $c) = @_;
306
307   my $feed = XML::Atom::Feed->new;
308
309   my $host = lc Sys::Hostname::hostname();
310   $feed->title($host . ' - ' . Gitalist->config->{name});
311   $feed->updated(~~DateTime->now);
312
313   my $repository = $c->stash->{Repository};
314   my %logargs = (
315       sha1   => $repository->head_hash,
316       count  => Gitalist->config->{paging}{log} || 25,
317       ($c->req->param('f') ? (file => $c->req->param('f')) : ())
318   );
319
320   my $mk_title = $c->stash->{short_cmt};
321   for my $commit ($repository->list_revs(%logargs)) {
322     my $entry = XML::Atom::Entry->new;
323     $entry->title( $mk_title->($commit->comment) );
324     $entry->id($c->uri_for('commit', {h=>$commit->sha1}));
325     # XXX Needs work ...
326     $entry->content($commit->comment);
327     $feed->add_entry($entry);
328   }
329
330   $c->response->body($feed->as_xml);
331   $c->response->content_type('application/atom+xml');
332   $c->response->status(200);
333 }
334
335 =head2 rss
336
337 Provides an RSS feed for a given repository.
338
339 =cut
340
341 sub rss : Chained('base') Args(0) {
342   my ($self, $c) = @_;
343
344   my $repository = $c->stash->{Repository};
345
346   my $rss = XML::RSS->new(version => '2.0');
347   $rss->channel(
348     title          => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name},
349     link           => $c->uri_for('summary', {p=>$repository->name}),
350     language       => 'en',
351     description    => $repository->description,
352     pubDate        => DateTime->now,
353     lastBuildDate  => DateTime->now,
354   );
355
356   my %logargs = (
357       sha1   => $repository->head_hash,
358       count  => Gitalist->config->{paging}{log} || 25,
359       ($c->req->param('f') ? (file => $c->req->param('f')) : ())
360   );
361   my $mk_title = $c->stash->{short_cmt};
362   for my $commit ($repository->list_revs(%logargs)) {
363     # XXX Needs work ....
364     $rss->add_item(
365         title       => $mk_title->($commit->comment),
366         permaLink   => $c->uri_for(commit => {h=>$commit->sha1}),
367         description => $commit->comment,
368     );
369   }
370
371   $c->response->body($rss->as_string);
372   $c->response->content_type('application/rss+xml');
373   $c->response->status(200);
374 }
375
376 sub opml : Chained('base') Args(0) {
377   my($self, $c) = @_;
378
379   my $opml = XML::OPML::SimpleGen->new();
380
381   $opml->head(title => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name});
382
383   my @list = @{ $c->model()->repositories };
384   die 'No repositories found in '. $c->model->repo_dir
385     unless @list;
386
387   for my $proj ( @list ) {
388     $opml->insert_outline(
389       text   => $proj->name. ' - '. $proj->description,
390       xmlUrl => $c->uri_for(rss => {p => $proj->name}),
391     );
392   }
393
394   $c->response->body($opml->as_string);
395   $c->response->content_type('application/rss');
396   $c->response->status(200);
397 }
398
399 =head2 patch
400
401 A raw patch for a given commit.
402
403 =cut
404
405 sub patch : Chained('base') Args(0) {
406     my ($self, $c) = @_;
407     $c->detach('patches', [1]);
408 }
409
410 =head2 patches
411
412 The patcheset for a given commit ???
413
414 =cut
415
416 sub patches : Chained('base') Args(0) {
417     my ($self, $c, $count) = @_;
418     $count ||= Gitalist->config->{patches}{max};
419     my $commit = $self->_get_object($c);
420     my $parent = $c->req->param('hp') || undef;
421     my $patch = $commit->get_patch( $parent, $count );
422     $c->response->body($patch);
423     $c->response->content_type('text/plain');
424     $c->response->status(200);
425 }
426
427 =head2 snapshot
428
429 Provides a snapshot of a given commit.
430
431 =cut
432
433 sub snapshot : Chained('base') Args(0) {
434     my ($self, $c) = @_;
435     my $format = $c->req->param('sf') || 'tgz';
436     die unless $format;
437     my $sha1 = $c->req->param('h') || $self->_get_object($c)->sha1;
438     my @snap = $c->stash->{Repository}->snapshot(
439         sha1 => $sha1,
440         format => $format
441     );
442     $c->response->status(200);
443     $c->response->headers->header( 'Content-Disposition' =>
444                                        "attachment; filename=$snap[0]");
445     $c->response->body($snap[1]);
446 }
447
448
449 sub base : Chained('/root') PathPart('') CaptureArgs(0) {
450   my($self, $c) = @_;
451
452   my $git_version = `git --version`;
453   chomp($git_version);
454   $c->stash(
455     git_version => $git_version,
456     version     => $Gitalist::VERSION,
457
458     # XXX Move these to a plugin!
459     time_since => sub {
460       return 'never' unless $_[0];
461       return age_string(time - $_[0]->epoch);
462     },
463     short_cmt => sub {
464       my $cmt = shift;
465       my($line) = split /\n/, $cmt;
466       $line =~ s/^(.{70,80}\b).*/$1 \x{2026}/;
467       return $line;
468     },
469     abridged_description => sub {
470         join(' ', grep { defined } (split / /, shift)[0..10]);
471     },
472   );
473 }
474
475 sub end : ActionClass('RenderView') {
476     my ($self, $c) = @_;
477     # Give repository views the current HEAD.
478     if ($c->stash->{Repository}) {
479         $c->stash->{HEAD} = $c->stash->{Repository}->head_hash;
480     }
481 }
482
483 sub error_404 : Action {
484     my ($self, $c) = @_;
485     $c->response->status(404);
486     $c->response->body('Page not found');
487 }
488
489 __PACKAGE__->meta->make_immutable;
490
491 __END__
492
493 =head1 NAME
494
495 Gitalist::Controller::Root - Root controller for the application
496
497 =head1 DESCRIPTION
498
499 This controller handles all of the root level paths for the application
500
501 =head1 METHODS
502
503 =head2 root
504
505 Root of chained actions
506
507 =head2 base
508
509 Populate the header and footer. Perhaps not the best location.
510
511 =head2 index
512
513 Provides the repository listing.
514
515 =head2 end
516
517 Attempt to render a view, if needed.
518
519 =head2 blame
520
521 =head2 error_404
522
523 =head2 history
524
525 =head2 opml
526
527 =head2 repository_index
528
529 =head1 AUTHORS
530
531 See L<Gitalist> for authors.
532
533 =head1 LICENSE
534
535 See L<Gitalist> for the license.
536
537 =cut