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