1 package Gitalist::Controller::Root;
9 use XML::OPML::SimpleGen;
11 use Gitalist::Utils qw/ age_string /;
13 use namespace::autoclean;
15 BEGIN { extends 'Catalyst::Controller' }
17 __PACKAGE__->config->{namespace} = '';
19 sub root : Chained('/') PathPart('') CaptureArgs(0) {}
22 my($self, $c, $haveh) = @_;
24 my $h = $haveh || $c->req->param('h') || '';
25 my $f = $c->req->param('f');
27 my $m = $c->stash->{Repository};
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))
34 # XXX This could definitely use more context.
35 || Carp::croak("Couldn't find a hash for the commit object!");
37 my $obj = $m->get_object($hash)
38 or Carp::croak("Couldn't find a object for '$hash' in '$pd'!");
43 sub index : Chained('base') PathPart('') Args(0) {
44 my ( $self, $c ) = @_;
46 $c->detach($c->req->param('a'))
47 if $c->req->param('a');
49 my @list = @{ $c->model()->repositories };
50 die 'No repositories found in '. $c->model->repo_dir
53 my $search = $c->req->param('s') || '';
56 index($_->name, $search) > -1
57 or ( $_->description !~ /^Unnamed repository/ and index($_->description, $search) > -1 )
62 search_text => $search,
63 repositories => \@list,
70 A summary of what's happening in the repo.
74 sub summary : Chained('base') Args(0) {
75 my ( $self, $c ) = @_;
76 my $repository = $c->stash->{Repository};
77 $c->detach('error_404') unless $repository;
78 my $commit = $self->_get_object($c);
79 my @heads = @{$repository->heads};
80 my $maxitems = Gitalist->config->{paging}{summary} || 10;
83 log_lines => [$repository->list_revs(
84 sha1 => $commit->sha1,
87 refs => $repository->references,
88 heads => [ @heads[0 .. ($#heads < $maxitems ? $#heads : $maxitems)] ],
95 The current list of heads (aka branches) in the repo.
99 sub heads : Chained('base') Args(0) {
100 my ( $self, $c ) = @_;
101 my $repository = $c->stash->{Repository};
103 commit => $self->_get_object($c),
104 heads => $repository->heads,
111 The current list of tags in the repo.
115 sub tags : Chained('base') Args(0) {
116 my ( $self, $c ) = @_;
117 my $repository = $c->stash->{Repository};
119 commit => $self->_get_object($c),
120 tags => $repository->tags,
125 sub blame : Chained('base') Args(0) {
128 my $repository = $c->stash->{Repository};
129 my $h = $c->req->param('h')
130 || $repository->hash_by_path($c->req->param('hb'), $c->req->param('f'))
131 || die "No file or sha1 provided.";
132 my $hb = $c->req->param('hb')
133 || $repository->head_hash
134 || die "Couldn't discern the corresponding head.";
135 my $filename = $c->req->param('f') || '';
137 my $blame = $repository->get_object($hb)->blame($filename, $h);
140 head => $repository->get_object($hb),
141 filename => $filename,
143 # XXX Hack hack hack, see View::SyntaxHighlight
144 language => ($filename =~ /\.p[lm]$/i ? 'Perl' : ''),
145 blob => join("\n", map $_->{line}, @$blame),
148 $c->forward('View::SyntaxHighlight')
149 unless $c->stash->{no_wrapper};
153 my ( $self, $c ) = @_;
154 my $repository = $c->stash->{Repository};
155 my $h = $c->req->param('h')
156 || $repository->hash_by_path($c->req->param('hb'), $c->req->param('f'))
157 || die "No file or sha1 provided.";
158 my $hb = $c->req->param('hb')
159 || $repository->head_hash
160 || die "Couldn't discern the corresponding head.";
162 my $filename = $c->req->param('f') || '';
164 my $blob = $repository->get_object($h);
165 $blob = $repository->get_object(
166 $repository->hash_by_path($h || $hb, $filename)
167 ) if $blob->type ne 'blob';
169 return $blob, $repository->get_object($hb), $filename;
174 The blob action i.e the contents of a file.
178 sub blob : Chained('base') Args(0) {
179 my ( $self, $c ) = @_;
181 my($blob, $head, $filename) = $self->_blob_objs($c);
183 blob => $blob->content,
185 filename => $filename,
186 # XXX Hack hack hack, see View::SyntaxHighlight
187 language => ($filename =~ /\.p[lm]$/i ? 'Perl' : ''),
191 $c->forward('View::SyntaxHighlight')
192 unless $c->stash->{no_wrapper};
197 The plain text version of blob, where file is rendered as is.
201 sub blob_plain : Chained('base') Args(0) {
204 my($blob) = $self->_blob_objs($c);
205 $c->response->content_type('text/plain; charset=utf-8');
206 $c->response->body($blob->content);
207 $c->response->status(200);
210 =head2 blobdiff_plain
212 The plain text version of blobdiff.
216 sub blobdiff_plain : Chained('base') Args(0) {
219 $c->stash(no_wrapper => 1);
220 $c->response->content_type('text/plain; charset=utf-8');
222 $c->forward('blobdiff');
227 Exposes a given diff of a blob.
231 sub blobdiff : Chained('base') Args(0) {
232 my ( $self, $c ) = @_;
233 my $commit = $self->_get_object($c, $c->req->param('hb'));
234 my $filename = $c->req->param('f')
235 || croak("No file specified!");
236 my($tree, $patch) = $c->stash->{Repository}->diff(
239 parent => $c->req->param('hpb') || undef,
245 filename => $filename,
246 # XXX Hack hack hack, see View::SyntaxHighlight
247 blobs => [$patch->[0]->{diff}],
249 action => 'blobdiff',
252 $c->forward('View::SyntaxHighlight')
253 unless $c->stash->{no_wrapper};
258 Exposes a given commit.
262 sub commit : Chained('base') Args(0) {
263 my ( $self, $c ) = @_;
264 my $repository = $c->stash->{Repository};
265 my $commit = $self->_get_object($c);
268 diff_tree => ($repository->diff(commit => $commit))[0],
269 refs => $repository->references,
276 Exposes a given diff of a commit.
280 sub commitdiff : Chained('base') Args(0) {
281 my ( $self, $c ) = @_;
282 my $commit = $self->_get_object($c);
283 my($tree, $patch) = $c->stash->{Repository}->diff(
285 parent => $c->req->param('hp') || undef,
292 # XXX Hack hack hack, see View::SyntaxHighlight
293 blobs => [map $_->{diff}, @$patch],
295 action => 'commitdiff',
298 $c->forward('View::SyntaxHighlight')
299 unless $c->stash->{no_wrapper};
302 sub commitdiff_plain : Chained('base') Args(0) {
305 $c->stash(no_wrapper => 1);
306 $c->response->content_type('text/plain; charset=utf-8');
308 $c->forward('commitdiff');
313 Expose an abbreviated log of a given sha1.
317 sub shortlog : Chained('base') Args(0) {
318 my ( $self, $c ) = @_;
320 my $repository = $c->stash->{Repository};
321 my $commit = $self->_get_object($c, $c->req->param('hb'));
322 my $filename = $c->req->param('f') || '';
325 sha1 => $commit->sha1,
326 count => Gitalist->config->{paging}{log} || 25,
327 ($filename ? (file => $filename) : ())
330 my $page = $c->req->param('pg') || 0;
331 $logargs{skip} = $c->req->param('pg') * $logargs{count}
332 if $c->req->param('pg');
336 log_lines => [$repository->list_revs(%logargs)],
337 refs => $repository->references,
339 filename => $filename,
340 action => 'shortlog',
346 Calls shortlog internally. Perhaps that should be reversed ...
350 sub log : Chained('base') Args(0) {
351 $_[0]->shortlog($_[1]);
352 $_[1]->stash->{action} = 'log';
355 # For legacy support.
356 sub history : Chained('base') Args(0) {
357 my ( $self, $c ) = @_;
359 my $repository = $c->stash->{Repository};
360 my $file = $repository->get_object(
361 $repository->hash_by_path(
362 $repository->head_hash,
363 $c->stash->{filename}
366 $c->stash( action => 'history',
367 filetype => $file->type,
373 The tree of a given commit.
377 sub tree : Chained('base') Args(0) {
378 my ( $self, $c ) = @_;
379 my $repository = $c->stash->{Repository};
380 my $commit = $self->_get_object($c, $c->req->param('hb'));
381 my $filename = $c->req->param('f') || '';
383 ? $repository->get_object($repository->hash_by_path($commit->sha1, $filename))
384 : $repository->get_object($commit->tree_sha1)
389 tree_list => [$repository->list_tree($tree->sha1)],
390 path => $c->req->param('f') || '',
397 Expose the local reflog. This may go away.
401 sub reflog : Chained('base') Args(0) {
402 my ( $self, $c ) = @_;
403 my @log = $c->stash->{Repository}->reflog(
415 The action for the search form.
419 sub search : Chained('base') Args(0) {
421 my $repository = $c->stash->{Repository};
422 my $commit = $self->_get_object($c);
423 # Lifted from /shortlog.
425 sha1 => $commit->sha1,
426 count => Gitalist->config->{paging}{log},
427 ($c->req->param('f') ? (file => $c->req->param('f')) : ()),
429 type => $c->req->param('type'),
430 text => $c->req->param('text'),
431 regexp => $c->req->param('regexp') || 0,
437 results => [$repository->list_revs(%logargs)],
439 # This could be added - page => $page,
445 Provides some help for the search form.
449 sub search_help : Chained('base') Args(0) {
451 $c->stash(template => 'search_help.tt2');
456 Provides an atom feed for a given repository.
460 sub atom : Chained('base') Args(0) {
463 my $feed = XML::Atom::Feed->new;
465 my $host = lc Sys::Hostname::hostname();
466 $feed->title($host . ' - ' . Gitalist->config->{name});
467 $feed->updated(~~DateTime->now);
469 my $repository = $c->stash->{Repository};
471 sha1 => $repository->head_hash,
472 count => Gitalist->config->{paging}{log} || 25,
473 ($c->req->param('f') ? (file => $c->req->param('f')) : ())
476 my $mk_title = $c->stash->{short_cmt};
477 for my $commit ($repository->list_revs(%logargs)) {
478 my $entry = XML::Atom::Entry->new;
479 $entry->title( $mk_title->($commit->comment) );
480 $entry->id($c->uri_for('commit', {h=>$commit->sha1}));
482 $entry->content($commit->comment);
483 $feed->add_entry($entry);
486 $c->response->body($feed->as_xml);
487 $c->response->content_type('application/atom+xml');
488 $c->response->status(200);
493 Provides an RSS feed for a given repository.
497 sub rss : Chained('base') Args(0) {
500 my $repository = $c->stash->{Repository};
502 my $rss = XML::RSS->new(version => '2.0');
504 title => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name},
505 link => $c->uri_for('summary', {p=>$repository->name}),
507 description => $repository->description,
508 pubDate => DateTime->now,
509 lastBuildDate => DateTime->now,
513 sha1 => $repository->head_hash,
514 count => Gitalist->config->{paging}{log} || 25,
515 ($c->req->param('f') ? (file => $c->req->param('f')) : ())
517 my $mk_title = $c->stash->{short_cmt};
518 for my $commit ($repository->list_revs(%logargs)) {
519 # XXX Needs work ....
521 title => $mk_title->($commit->comment),
522 permaLink => $c->uri_for(commit => {h=>$commit->sha1}),
523 description => $commit->comment,
527 $c->response->body($rss->as_string);
528 $c->response->content_type('application/rss+xml');
529 $c->response->status(200);
532 sub opml : Chained('base') Args(0) {
535 my $opml = XML::OPML::SimpleGen->new();
537 $opml->head(title => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name});
539 my @list = @{ $c->model()->repositories };
540 die 'No repositories found in '. $c->model->repo_dir
543 for my $proj ( @list ) {
544 $opml->insert_outline(
545 text => $proj->name. ' - '. $proj->description,
546 xmlUrl => $c->uri_for(rss => {p => $proj->name}),
550 $c->response->body($opml->as_string);
551 $c->response->content_type('application/rss');
552 $c->response->status(200);
557 A raw patch for a given commit.
561 sub patch : Chained('base') Args(0) {
563 $c->detach('patches', [1]);
568 The patcheset for a given commit ???
572 sub patches : Chained('base') Args(0) {
573 my ($self, $c, $count) = @_;
574 $count ||= Gitalist->config->{patches}{max};
575 my $commit = $self->_get_object($c);
576 my $parent = $c->req->param('hp') || undef;
577 my $patch = $commit->get_patch( $parent, $count );
578 $c->response->body($patch);
579 $c->response->content_type('text/plain');
580 $c->response->status(200);
585 Provides a snapshot of a given commit.
589 sub snapshot : Chained('base') Args(0) {
591 my $format = $c->req->param('sf') || 'tgz';
593 my $sha1 = $c->req->param('h') || $self->_get_object($c)->sha1;
594 my @snap = $c->stash->{Repository}->snapshot(
598 $c->response->status(200);
599 $c->response->headers->header( 'Content-Disposition' =>
600 "attachment; filename=$snap[0]");
601 $c->response->body($snap[1]);
605 sub base : Chained('/root') PathPart('') CaptureArgs(0) {
608 my $repository = $c->req->param('p');
609 if (defined $repository) {
611 $c->stash(Repository => $c->model()->get_repository($repository));
614 $c->detach('/error_404');
618 my $a_repository = $c->stash->{Repository} || $c->model()->repositories->[0];
620 git_version => $a_repository->run_cmd('--version'),
621 version => $Gitalist::VERSION,
623 # XXX Move these to a plugin!
625 return 'never' unless $_[0];
626 return age_string(time - $_[0]->epoch);
630 my($line) = split /\n/, $cmt;
631 $line =~ s/^(.{70,80}\b).*/$1 \x{2026}/;
634 abridged_description => sub {
635 join(' ', grep { defined } (split / /, shift)[0..10]);
640 sub end : ActionClass('RenderView') {
642 # Give repository views the current HEAD.
643 if ($c->stash->{Repository}) {
644 $c->stash->{HEAD} = $c->stash->{Repository}->head_hash;
648 sub error_404 : Action {
650 $c->response->status(404);
651 $c->response->body('Page not found');
654 __PACKAGE__->meta->make_immutable;
660 Gitalist::Controller::Root - Root controller for the application
664 This controller handles all of the root level paths for the application
670 Root of chained actions
674 Populate the header and footer. Perhaps not the best location.
678 Provides the repository listing.
682 Attempt to render a view, if needed.
686 =head2 commitdiff_plain
694 =head2 repository_index
698 See L<Gitalist> for authors.
702 See L<Gitalist> for the license.