Aaaaaand the logic (such as it is) for syntax stuff is pretty much is where I wanted it.
[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 @list = @{ $c->model()->repositories };
50   die 'No repositories found in '. $c->model->repo_dir
51     unless @list;
52
53   my $search = $c->req->param('s') || '';
54   if($search) {
55     @list = grep {
56          index($_->name, $search) > -1
57       or ( $_->description !~ /^Unnamed repository/ and index($_->description, $search) > -1 )
58     } @list
59   }
60
61   $c->stash(
62     search_text => $search,
63     repositories    => \@list,
64     action      => 'index',
65   );
66 }
67
68 # FIXME - WTF is this for?
69 sub repository_index : Chained('base') Args(0) {
70   my ( $self, $c ) = @_;
71
72   my @list = @{ $c->model()->repositories };
73   die 'No repositories found in '. $c->model->repo_dir
74     unless @list;
75
76   $c->response->content_type('text/plain');
77   $c->response->body(
78     join "\n", map $_->name, @list
79   );
80   $c->response->status(200);
81 }
82 # FIXME - maintain compatibility with previous URI
83 sub project_index : Chained('base') Args(0) {
84     my ( $self, $c) = @_;
85     $c->detach('repository_index');
86 }
87
88 =head2 summary
89
90 A summary of what's happening in the repo.
91
92 =cut
93
94 sub summary : Chained('base') Args(0) {
95   my ( $self, $c ) = @_;
96   my $repository = $c->stash->{Repository};
97   $c->detach('error_404') unless $repository;
98   my $commit = $self->_get_object($c);
99   my @heads  = @{$repository->heads};
100   my $maxitems = Gitalist->config->{paging}{summary} || 10;
101   $c->stash(
102     commit    => $commit,
103     log_lines => [$repository->list_revs(
104         sha1 => $commit->sha1,
105         count => $maxitems,
106     )],
107     refs      => $repository->references,
108     heads     => [ @heads[0 .. ($#heads < $maxitems ? $#heads : $maxitems)] ],
109     action    => 'summary',
110   );
111 }
112
113 =head2 heads
114
115 The current list of heads (aka branches) in the repo.
116
117 =cut
118
119 sub heads : Chained('base') Args(0) {
120   my ( $self, $c ) = @_;
121   my $repository = $c->stash->{Repository};
122   $c->stash(
123     commit => $self->_get_object($c),
124     heads  => $repository->heads,
125     action => 'heads',
126   );
127 }
128
129 =head2 tags
130
131 The current list of tags in the repo.
132
133 =cut
134
135 sub tags : Chained('base') Args(0) {
136   my ( $self, $c ) = @_;
137   my $repository = $c->stash->{Repository};
138   $c->stash(
139     commit => $self->_get_object($c),
140     tags   => $repository->tags,
141     action => 'tags',
142   );
143 }
144
145 sub blame : Chained('base') Args(0) {
146   my($self, $c) = @_;
147
148   my $repository = $c->stash->{Repository};
149   my $h  = $c->req->param('h')
150        || $repository->hash_by_path($c->req->param('hb'), $c->req->param('f'))
151        || die "No file or sha1 provided.";
152   my $hb = $c->req->param('hb')
153        || $repository->head_hash
154        || die "Couldn't discern the corresponding head.";
155   my $filename = $c->req->param('f') || '';
156
157   my $blame = $repository->get_object($hb)->blame($filename, $h);
158   $c->stash(
159     blame    => $blame,
160     head     => $repository->get_object($hb),
161     filename => $filename,
162     blob     => join("\n", map $_->{line}, @$blame),
163   );
164
165   $c->forward('Model::ContentMangler')
166     unless $c->stash->{no_wrapper};
167 }
168
169 sub _blob_objs {
170   my ( $self, $c ) = @_;
171   my $repository = $c->stash->{Repository};
172   my $h  = $c->req->param('h')
173        || $repository->hash_by_path($c->req->param('hb'), $c->req->param('f'))
174        || die "No file or sha1 provided.";
175   my $hb = $c->req->param('hb')
176        || $repository->head_hash
177        || die "Couldn't discern the corresponding head.";
178
179   my $filename = $c->req->param('f') || '';
180
181   my $blob = $repository->get_object($h);
182   $blob = $repository->get_object(
183     $repository->hash_by_path($h || $hb, $filename)
184   ) if $blob->type ne 'blob';
185
186   return $blob, $repository->get_object($hb), $filename;
187 }
188
189 =head2 blob
190
191 The blob action i.e the contents of a file.
192
193 =cut
194
195 sub blob : Chained('base') Args(0) {
196   my ( $self, $c ) = @_;
197
198   my($blob, $head, $filename) = $self->_blob_objs($c);
199   $c->stash(
200     blob     => $blob->content,
201     head     => $head,
202     filename => $filename,
203     action   => 'blob',
204   );
205
206   $c->forward('Model::ContentMangler')
207     unless $c->stash->{no_wrapper};
208 }
209
210 =head2 blob_plain
211
212 The plain text version of blob, where file is rendered as is.
213
214 =cut
215
216 sub blob_plain : Chained('base') Args(0) {
217   my($self, $c) = @_;
218
219   my($blob) = $self->_blob_objs($c);
220   $c->response->content_type('text/plain; charset=utf-8');
221   $c->response->body($blob->content);
222   $c->response->status(200);
223 }
224
225 =head2 blobdiff_plain
226
227 The plain text version of blobdiff.
228
229 =cut
230
231 sub blobdiff_plain : Chained('base') Args(0) {
232   my($self, $c) = @_;
233
234   $c->stash(no_wrapper => 1);
235   $c->response->content_type('text/plain; charset=utf-8');
236
237   $c->forward('blobdiff');
238 }
239
240 =head2 blobdiff
241
242 Exposes a given diff of a blob.
243
244 =cut
245
246 sub blobdiff : Chained('base') Args(0) {
247   my ( $self, $c ) = @_;
248   my $commit = $self->_get_object($c, $c->req->param('hb'));
249   my $filename = $c->req->param('f')
250               || croak("No file specified!");
251   my($tree, $patch) = $c->stash->{Repository}->diff(
252     commit => $commit,
253     patch  => 1,
254     parent => $c->req->param('hpb') || undef,
255     file   => $filename,
256   );
257   $c->stash(
258     commit    => $commit,
259     diff      => $patch,
260     filename  => $filename,
261     # XXX Hack hack hack, see View::SyntaxHighlight
262     blobs     => [$patch->[0]->{diff}],
263     language  => 'Diff',
264     action    => 'blobdiff',
265   );
266
267   $c->forward('Model::ContentMangler')
268     unless $c->stash->{no_wrapper};
269 }
270
271 =head2 commit
272
273 Exposes a given commit.
274
275 =cut
276
277 sub commit : Chained('base') Args(0) {
278   my ( $self, $c ) = @_;
279   my $repository = $c->stash->{Repository};
280   my $commit = $self->_get_object($c);
281   $c->stash(
282       commit      => $commit,
283       diff_tree   => ($repository->diff(commit => $commit))[0],
284       refs      => $repository->references,
285       action      => 'commit',
286   );
287 }
288
289 =head2 commitdiff
290
291 Exposes a given diff of a commit.
292
293 =cut
294
295 sub commitdiff : Chained('base') Args(0) {
296   my ( $self, $c ) = @_;
297   my $commit = $self->_get_object($c);
298   my($tree, $patch) = $c->stash->{Repository}->diff(
299       commit => $commit,
300       parent => $c->req->param('hp') || undef,
301       patch  => 1,
302   );
303   $c->stash(
304     commit    => $commit,
305     diff_tree => $tree,
306     diff      => $patch,
307     # XXX Hack hack hack, see View::SyntaxHighlight
308     blobs     => [map $_->{diff}, @$patch],
309     language  => 'Diff',
310     action    => 'commitdiff',
311   );
312
313   $c->forward('Model::ContentMangler')
314     unless $c->stash->{no_wrapper};
315 }
316
317 sub commitdiff_plain : Chained('base') Args(0) {
318   my($self, $c) = @_;
319
320   $c->stash(no_wrapper => 1);
321   $c->response->content_type('text/plain; charset=utf-8');
322
323   $c->forward('commitdiff');
324 }
325
326 =head2 shortlog
327
328 Expose an abbreviated log of a given sha1.
329
330 =cut
331
332 sub shortlog : Chained('base') Args(0) {
333   my ( $self, $c ) = @_;
334
335   my $repository  = $c->stash->{Repository};
336   my $commit   = $self->_get_object($c, $c->req->param('hb'));
337   my $filename = $c->req->param('f') || '';
338
339   my %logargs = (
340       sha1   => $commit->sha1,
341       count  => Gitalist->config->{paging}{log} || 25,
342       ($filename ? (file => $filename) : ())
343   );
344
345   my $page = $c->req->param('pg') || 0;
346   $logargs{skip} = $c->req->param('pg') * $logargs{count}
347     if $c->req->param('pg');
348
349   $c->stash(
350       commit    => $commit,
351       log_lines => [$repository->list_revs(%logargs)],
352       refs      => $repository->references,
353       page      => $page,
354       filename  => $filename,
355       action    => 'shortlog',
356   );
357 }
358
359 =head2 log
360
361 Calls shortlog internally. Perhaps that should be reversed ...
362
363 =cut
364
365 sub log : Chained('base') Args(0) {
366     $_[0]->shortlog($_[1]);
367     $_[1]->stash->{action} = 'log';
368 }
369
370 # For legacy support.
371 sub history : Chained('base') Args(0) {
372     my ( $self, $c ) = @_;
373     $self->shortlog($c);
374     my $repository = $c->stash->{Repository};
375     my $file = $repository->get_object(
376         $repository->hash_by_path(
377             $repository->head_hash,
378             $c->stash->{filename}
379         )
380     );
381      $c->stash( action => 'history',
382                filetype => $file->type,
383            );
384 }
385
386 =head2 tree
387
388 The tree of a given commit.
389
390 =cut
391
392 sub tree : Chained('base') Args(0) {
393   my ( $self, $c ) = @_;
394   my $repository = $c->stash->{Repository};
395   my $commit  = $self->_get_object($c, $c->req->param('hb'));
396   my $filename = $c->req->param('f') || '';
397   my $tree    = $filename
398     ? $repository->get_object($repository->hash_by_path($commit->sha1, $filename))
399     : $repository->get_object($commit->tree_sha1)
400   ;
401   $c->stash(
402       commit    => $commit,
403       tree      => $tree,
404       tree_list => [$repository->list_tree($tree->sha1)],
405       path      => $c->req->param('f') || '',
406       action    => 'tree',
407   );
408 }
409
410 =head2 reflog
411
412 Expose the local reflog. This may go away.
413
414 =cut
415
416 sub reflog : Chained('base') Args(0) {
417   my ( $self, $c ) = @_;
418   my @log = $c->stash->{Repository}->reflog(
419       '--since=yesterday'
420   );
421
422   $c->stash(
423       log    => \@log,
424       action => 'reflog',
425   );
426 }
427
428 =head2 search
429
430 The action for the search form.
431
432 =cut
433
434 sub search : Chained('base') Args(0) {
435   my($self, $c) = @_;
436   $c->stash(current_action => 'GitRepos');
437   my $repository = $c->stash->{Repository};
438   my $commit  = $self->_get_object($c);
439   # Lifted from /shortlog.
440   my %logargs = (
441     sha1   => $commit->sha1,
442     count  => Gitalist->config->{paging}{log},
443     ($c->req->param('f') ? (file => $c->req->param('f')) : ()),
444     search => {
445       type   => $c->req->param('type'),
446       text   => $c->req->param('text'),
447       regexp => $c->req->param('regexp') || 0,
448     },
449   );
450
451   $c->stash(
452       commit  => $commit,
453       results => [$repository->list_revs(%logargs)],
454       action  => 'search',
455           # This could be added - page      => $page,
456   );
457 }
458
459 =head2 search_help
460
461 Provides some help for the search form.
462
463 =cut
464
465 sub search_help : Chained('base') Args(0) {
466     my ($self, $c) = @_;
467     $c->stash(template => 'search_help.tt2');
468 }
469
470 =head2 atom
471
472 Provides an atom feed for a given repository.
473
474 =cut
475
476 sub atom : Chained('base') Args(0) {
477   my($self, $c) = @_;
478
479   my $feed = XML::Atom::Feed->new;
480
481   my $host = lc Sys::Hostname::hostname();
482   $feed->title($host . ' - ' . Gitalist->config->{name});
483   $feed->updated(~~DateTime->now);
484
485   my $repository = $c->stash->{Repository};
486   my %logargs = (
487       sha1   => $repository->head_hash,
488       count  => Gitalist->config->{paging}{log} || 25,
489       ($c->req->param('f') ? (file => $c->req->param('f')) : ())
490   );
491
492   my $mk_title = $c->stash->{short_cmt};
493   for my $commit ($repository->list_revs(%logargs)) {
494     my $entry = XML::Atom::Entry->new;
495     $entry->title( $mk_title->($commit->comment) );
496     $entry->id($c->uri_for('commit', {h=>$commit->sha1}));
497     # XXX Needs work ...
498     $entry->content($commit->comment);
499     $feed->add_entry($entry);
500   }
501
502   $c->response->body($feed->as_xml);
503   $c->response->content_type('application/atom+xml');
504   $c->response->status(200);
505 }
506
507 =head2 rss
508
509 Provides an RSS feed for a given repository.
510
511 =cut
512
513 sub rss : Chained('base') Args(0) {
514   my ($self, $c) = @_;
515
516   my $repository = $c->stash->{Repository};
517
518   my $rss = XML::RSS->new(version => '2.0');
519   $rss->channel(
520     title          => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name},
521     link           => $c->uri_for('summary', {p=>$repository->name}),
522     language       => 'en',
523     description    => $repository->description,
524     pubDate        => DateTime->now,
525     lastBuildDate  => DateTime->now,
526   );
527
528   my %logargs = (
529       sha1   => $repository->head_hash,
530       count  => Gitalist->config->{paging}{log} || 25,
531       ($c->req->param('f') ? (file => $c->req->param('f')) : ())
532   );
533   my $mk_title = $c->stash->{short_cmt};
534   for my $commit ($repository->list_revs(%logargs)) {
535     # XXX Needs work ....
536     $rss->add_item(
537         title       => $mk_title->($commit->comment),
538         permaLink   => $c->uri_for(commit => {h=>$commit->sha1}),
539         description => $commit->comment,
540     );
541   }
542
543   $c->response->body($rss->as_string);
544   $c->response->content_type('application/rss+xml');
545   $c->response->status(200);
546 }
547
548 sub opml : Chained('base') Args(0) {
549   my($self, $c) = @_;
550
551   my $opml = XML::OPML::SimpleGen->new();
552
553   $opml->head(title => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name});
554
555   my @list = @{ $c->model()->repositories };
556   die 'No repositories found in '. $c->model->repo_dir
557     unless @list;
558
559   for my $proj ( @list ) {
560     $opml->insert_outline(
561       text   => $proj->name. ' - '. $proj->description,
562       xmlUrl => $c->uri_for(rss => {p => $proj->name}),
563     );
564   }
565
566   $c->response->body($opml->as_string);
567   $c->response->content_type('application/rss');
568   $c->response->status(200);
569 }
570
571 =head2 patch
572
573 A raw patch for a given commit.
574
575 =cut
576
577 sub patch : Chained('base') Args(0) {
578     my ($self, $c) = @_;
579     $c->detach('patches', [1]);
580 }
581
582 =head2 patches
583
584 The patcheset for a given commit ???
585
586 =cut
587
588 sub patches : Chained('base') Args(0) {
589     my ($self, $c, $count) = @_;
590     $count ||= Gitalist->config->{patches}{max};
591     my $commit = $self->_get_object($c);
592     my $parent = $c->req->param('hp') || undef;
593     my $patch = $commit->get_patch( $parent, $count );
594     $c->response->body($patch);
595     $c->response->content_type('text/plain');
596     $c->response->status(200);
597 }
598
599 =head2 snapshot
600
601 Provides a snapshot of a given commit.
602
603 =cut
604
605 sub snapshot : Chained('base') Args(0) {
606     my ($self, $c) = @_;
607     my $format = $c->req->param('sf') || 'tgz';
608     die unless $format;
609     my $sha1 = $c->req->param('h') || $self->_get_object($c)->sha1;
610     my @snap = $c->stash->{Repository}->snapshot(
611         sha1 => $sha1,
612         format => $format
613     );
614     $c->response->status(200);
615     $c->response->headers->header( 'Content-Disposition' =>
616                                        "attachment; filename=$snap[0]");
617     $c->response->body($snap[1]);
618 }
619
620
621 sub base : Chained('/root') PathPart('') CaptureArgs(0) {
622   my($self, $c) = @_;
623
624   my $repository = $c->req->param('p');
625   if (defined $repository) {
626     eval {
627       $c->stash(Repository => $c->model('GitRepos')->get_repository($repository));
628     };
629     if ($@) {
630       $c->detach('/error_404');
631     }
632   }
633
634   my $a_repository = $c->stash->{Repository} || $c->model()->repositories->[0];
635   $c->stash(
636     git_version => $a_repository->run_cmd('--version'),
637     version     => $Gitalist::VERSION,
638
639     # XXX Move these to a plugin!
640     time_since => sub {
641       return 'never' unless $_[0];
642       return age_string(time - $_[0]->epoch);
643     },
644     short_cmt => sub {
645       my $cmt = shift;
646       my($line) = split /\n/, $cmt;
647       $line =~ s/^(.{70,80}\b).*/$1 \x{2026}/;
648       return $line;
649     },
650     abridged_description => sub {
651         join(' ', grep { defined } (split / /, shift)[0..10]);
652     },
653   );
654 }
655
656 sub end : ActionClass('RenderView') {
657     my ($self, $c) = @_;
658     # Give repository views the current HEAD.
659     if ($c->stash->{Repository}) {
660         $c->stash->{HEAD} = $c->stash->{Repository}->head_hash;
661     }
662     $c->stash(syntax_css => [$c->model('ContentMangler')->css]);
663 }
664
665 sub error_404 : Action {
666     my ($self, $c) = @_;
667     $c->response->status(404);
668     $c->response->body('Page not found');
669 }
670
671 __PACKAGE__->meta->make_immutable;
672
673 __END__
674
675 =head1 NAME
676
677 Gitalist::Controller::Root - Root controller for the application
678
679 =head1 DESCRIPTION
680
681 This controller handles all of the root level paths for the application
682
683 =head1 METHODS
684
685 =head2 root
686
687 Root of chained actions
688
689 =head2 base
690
691 Populate the header and footer. Perhaps not the best location.
692
693 =head2 index
694
695 Provides the repository listing.
696
697 =head2 end
698
699 Attempt to render a view, if needed.
700
701 =head2 blame
702
703 =head2 commitdiff_plain
704
705 =head2 error_404
706
707 =head2 history
708
709 =head2 opml
710
711 =head2 repository_index
712
713 =head1 AUTHORS
714
715 See L<Gitalist> for authors.
716
717 =head1 LICENSE
718
719 See L<Gitalist> for the license.
720
721 =cut