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