Hacked in syntax highlighting to the blame view.
[catagits/Gitalist.git] / lib / Gitalist / Controller / Root.pm
CommitLineData
89de6a33 1package Gitalist::Controller::Root;
42fe5d11 2use Moose;
3use namespace::autoclean;
89de6a33 4
42fe5d11 5BEGIN { extends 'Catalyst::Controller' }
89de6a33 6
89de6a33 7__PACKAGE__->config->{namespace} = '';
8
f796a861 9use Sys::Hostname ();
d17ce39c 10use XML::Atom::Feed;
11use XML::Atom::Entry;
f796a861 12use XML::RSS;
286cea09 13use XML::OPML::SimpleGen;
d17ce39c 14
89de6a33 15=head1 NAME
16
17Gitalist::Controller::Root - Root Controller for Gitalist
18
19=head1 DESCRIPTION
20
21[enter your description here]
22
23=head1 METHODS
24
25=cut
26
832cbc81 27sub _get_object {
b4b4d0fd 28 my($self, $c, $haveh) = @_;
9dc3b9a5 29
c1f608c8 30 my $h = $haveh || $c->req->param('h') || '';
0ee97fec 31 my $f = $c->req->param('f');
8dbe8024 32
1aad4e81 33 my $m = $c->stash->{Project};
34 my $pd = $m->path;
0ee97fec 35
9dc3b9a5 36 # Either use the provided h(ash) parameter, the f(ile) parameter or just use HEAD.
a7cc1ede 37 my $hash = ($h =~ /[^a-f0-9]/ ? $m->head_hash($h) : $h)
38 || ($f && $m->hash_by_path($f))
39 || $m->head_hash
9dc3b9a5 40 # XXX This could definitely use more context.
41 || Carp::croak("Couldn't find a hash for the commit object!");
42
c046a52f 43 my $obj = $m->get_object($hash)
44 or Carp::croak("Couldn't find a object for '$hash' in '$pd'!");
9dc3b9a5 45
c046a52f 46 return $obj;
9dc3b9a5 47}
48
1625cd5f 49=head2 index
50
51Provides the project listing.
52
53=cut
54
86382b95 55sub index :Path :Args(0) {
7bc165b3 56 my ( $self, $c ) = @_;
57
58 $c->detach($c->req->param('a'))
59 if $c->req->param('a');
60
61 my @list = @{ $c->model()->projects };
62 die 'No projects found in '. $c->model->repo_dir
63 unless @list;
64
65 my $search = $c->req->param('s') || '';
66 if($search) {
67 @list = grep {
68 index($_->name, $search) > -1
69 or ( $_->description !~ /^Unnamed repository/ and index($_->description, $search) > -1 )
70 } @list
71 }
72
73 $c->stash(
74 search_text => $search,
75 projects => \@list,
76 action => 'index',
77 );
04d1d917 78}
79
38a4951e 80sub project_index : Local {
81 my ( $self, $c ) = @_;
82
83 my @list = @{ $c->model()->projects };
84 die 'No projects found in '. $c->model->repo_dir
85 unless @list;
86
87 $c->response->content_type('text/plain');
88 $c->response->body(
89 join "\n", map $_->name, @list
90 );
91 $c->response->status(200);
92}
93
790ce598 94=head2 summary
95
96A summary of what's happening in the repo.
97
98=cut
99
100sub summary : Local {
101 my ( $self, $c ) = @_;
b47ae2c0 102 my $project = $c->stash->{Project};
3bbb1202 103 $c->detach('error_404') unless $project;
832cbc81 104 my $commit = $self->_get_object($c);
c65cccc2 105 my @heads = @{$project->heads};
06281e11 106 my $maxitems = Gitalist->config->{paging}{summary} || 10;
790ce598 107 $c->stash(
108 commit => $commit,
4111e151 109 log_lines => [$project->list_revs(
110 sha1 => $commit->sha1,
06281e11 111 count => $maxitems,
fde5091f 112 )],
4111e151 113 refs => $project->references,
0969b411 114 heads => [ @heads[0 .. ($#heads < $maxitems ? $#heads : $maxitems)] ],
790ce598 115 action => 'summary',
06281e11 116 );
790ce598 117}
118
119=head2 heads
120
121The current list of heads (aka branches) in the repo.
122
123=cut
124
125sub heads : Local {
126 my ( $self, $c ) = @_;
b47ae2c0 127 my $project = $c->stash->{Project};
790ce598 128 $c->stash(
832cbc81 129 commit => $self->_get_object($c),
c65cccc2 130 heads => $project->heads,
790ce598 131 action => 'heads',
132 );
133}
134
ea19a20c 135=head2 tags
136
137The current list of tags in the repo.
138
139=cut
140
141sub tags : Local {
142 my ( $self, $c ) = @_;
143 my $project = $c->stash->{Project};
144 $c->stash(
145 commit => $self->_get_object($c),
146 tags => $project->tags,
147 action => 'tags',
148 );
149}
150
18a8059a 151sub blame : Local {
152 my($self, $c) = @_;
153
154 my $project = $c->stash->{Project};
155 my $h = $c->req->param('h')
156 || $project->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 || $project->head_hash
160 || die "Couldn't discern the corresponding head.";
161 my $filename = $c->req->param('f') || '';
162
6bd8b34b 163 my $blame = $project->get_object($hb)->blame($filename);
18a8059a 164 $c->stash(
6bd8b34b 165 blame => $blame,
18a8059a 166 head => $project->get_object($hb),
167 filename => $filename,
6bd8b34b 168
169 # XXX Hack hack hack, see View::SyntaxHighlight
170 language => ($filename =~ /\.p[lm]$/i ? 'Perl' : ''),
171 blob => join("\n", map $_->{line}, @$blame),
18a8059a 172 );
6bd8b34b 173
174 $c->forward('View::SyntaxHighlight')
175 unless $c->stash->{no_wrapper};
18a8059a 176}
177
b5f3d3e7 178sub _blob_objs {
295c9703 179 my ( $self, $c ) = @_;
8bb7649b 180 my $project = $c->stash->{Project};
c8870bd3 181 my $h = $c->req->param('h')
8bb7649b 182 || $project->hash_by_path($c->req->param('hb'), $c->req->param('f'))
c8870bd3 183 || die "No file or sha1 provided.";
184 my $hb = $c->req->param('hb')
8bb7649b 185 || $project->head_hash
c8870bd3 186 || die "Couldn't discern the corresponding head.";
187
f5da8e7a 188 my $filename = $c->req->param('f') || '';
189
b5f3d3e7 190 my $blob = $project->get_object($h);
191 $blob = $project->get_object(
192 $project->hash_by_path($h || $hb, $filename)
193 ) if $blob->type ne 'blob';
194
195 return $blob, $project->get_object($hb), $filename;
196}
197
198=head2 blob
199
200The blob action i.e the contents of a file.
201
202=cut
203
204sub blob : Local {
205 my ( $self, $c ) = @_;
206
207 my($blob, $head, $filename) = $self->_blob_objs($c);
295c9703 208 $c->stash(
b5f3d3e7 209 blob => $blob->content,
210 head => $head,
f5da8e7a 211 filename => $filename,
212 # XXX Hack hack hack, see View::SyntaxHighlight
6bd8b34b 213 language => ($filename =~ /\.p[lm]$/i ? 'Perl' : ''),
c8870bd3 214 action => 'blob',
295c9703 215 );
7e54e579 216
c8a42dd5 217 $c->forward('View::SyntaxHighlight')
218 unless $c->stash->{no_wrapper};
219}
220
b5f3d3e7 221=head2 blob_plain
222
223The plain text version of blob, where file is rendered as is.
224
225=cut
226
c8a42dd5 227sub blob_plain : Local {
228 my($self, $c) = @_;
229
b5f3d3e7 230 my($blob) = $self->_blob_objs($c);
c8a42dd5 231 $c->response->content_type('text/plain; charset=utf-8');
b5f3d3e7 232 $c->response->body($blob->content);
233 $c->response->status(200);
c8a42dd5 234}
235
b5f3d3e7 236=head2 blobdiff_plain
237
238The plain text version of blobdiff.
239
240=cut
241
c8a42dd5 242sub blobdiff_plain : Local {
243 my($self, $c) = @_;
244
245 $c->stash(no_wrapper => 1);
246 $c->response->content_type('text/plain; charset=utf-8');
247
248 $c->forward('blobdiff');
295c9703 249}
250
6cf4366a 251=head2 blobdiff
252
253Exposes a given diff of a blob.
254
255=cut
256
257sub blobdiff : Local {
258 my ( $self, $c ) = @_;
832cbc81 259 my $commit = $self->_get_object($c, $c->req->param('hb'));
6cf4366a 260 my $filename = $c->req->param('f')
261 || croak("No file specified!");
6cfcd548 262 my($tree, $patch) = $c->stash->{Project}->diff(
ad8884fc 263 commit => $commit,
ad8884fc 264 patch => 1,
c8a42dd5 265 parent => $c->req->param('hpb') || undef,
266 file => $filename,
6cf4366a 267 );
268 $c->stash(
269 commit => $commit,
ad8884fc 270 diff => $patch,
592b68ef 271 filename => $filename,
6cf4366a 272 # XXX Hack hack hack, see View::SyntaxHighlight
ad8884fc 273 blobs => [$patch->[0]->{diff}],
6cf4366a 274 language => 'Diff',
275 action => 'blobdiff',
276 );
277
c8a42dd5 278 $c->forward('View::SyntaxHighlight')
279 unless $c->stash->{no_wrapper};
6cf4366a 280}
281
1625cd5f 282=head2 commit
283
47495599 284Exposes a given commit.
1625cd5f 285
286=cut
287
1feb3d6b 288sub commit : Local {
d7c9a32f 289 my ( $self, $c ) = @_;
77edf882 290 my $project = $c->stash->{Project};
832cbc81 291 my $commit = $self->_get_object($c);
b7aca93a 292 $c->stash(
790ce598 293 commit => $commit,
77edf882 294 diff_tree => ($project->diff(commit => $commit))[0],
295 refs => $project->references,
790ce598 296 action => 'commit',
b7aca93a 297 );
d7c9a32f 298}
299
2247133f 300=head2 commitdiff
301
302Exposes a given diff of a commit.
303
304=cut
305
306sub commitdiff : Local {
307 my ( $self, $c ) = @_;
832cbc81 308 my $commit = $self->_get_object($c);
fc948aee 309 my($tree, $patch) = $c->stash->{Project}->diff(
ad8884fc 310 commit => $commit,
fc948aee 311 parent => $c->req->param('hp') || undef,
ad8884fc 312 patch => 1,
313 );
2247133f 314 $c->stash(
f5da8e7a 315 commit => $commit,
ad8884fc 316 diff_tree => $tree,
317 diff => $patch,
f5da8e7a 318 # XXX Hack hack hack, see View::SyntaxHighlight
ad8884fc 319 blobs => [map $_->{diff}, @$patch],
f5da8e7a 320 language => 'Diff',
321 action => 'commitdiff',
2247133f 322 );
f5da8e7a 323
c8a42dd5 324 $c->forward('View::SyntaxHighlight')
325 unless $c->stash->{no_wrapper};
326}
327
328sub commitdiff_plain : Local {
329 my($self, $c) = @_;
330
331 $c->stash(no_wrapper => 1);
332 $c->response->content_type('text/plain; charset=utf-8');
333
334 $c->forward('commitdiff');
2247133f 335}
336
47495599 337=head2 shortlog
338
339Expose an abbreviated log of a given sha1.
340
341=cut
342
343sub shortlog : Local {
344 my ( $self, $c ) = @_;
592b68ef 345
346 my $project = $c->stash->{Project};
c046a52f 347 my $commit = $self->_get_object($c, $c->req->param('hb'));
592b68ef 348 my $filename = $c->req->param('f') || '';
349
fde5091f 350 my %logargs = (
351 sha1 => $commit->sha1,
f740f8a9 352 count => Gitalist->config->{paging}{log} || 25,
592b68ef 353 ($filename ? (file => $filename) : ())
63e220b9 354 );
fde5091f 355
356 my $page = $c->req->param('pg') || 0;
357 $logargs{skip} = $c->req->param('pg') * $logargs{count}
358 if $c->req->param('pg');
359
47495599 360 $c->stash(
790ce598 361 commit => $commit,
f740f8a9 362 log_lines => [$project->list_revs(%logargs)],
363 refs => $project->references,
b4b4d0fd 364 page => $page,
592b68ef 365 filename => $filename,
366 action => 'shortlog',
47495599 367 );
368}
369
790ce598 370=head2 log
371
372Calls shortlog internally. Perhaps that should be reversed ...
373
374=cut
375sub log : Local {
376 $_[0]->shortlog($_[1]);
377 $_[1]->stash->{action} = 'log';
378}
379
c8a42dd5 380# For legacy support.
381sub history : Local {
c046a52f 382 $_[1]->forward('shortlog');
c8a42dd5 383}
384
b3fa97cd 385=head2 tree
386
387The tree of a given commit.
388
389=cut
390
391sub tree : Local {
392 my ( $self, $c ) = @_;
74af77a3 393 my $project = $c->stash->{Project};
832cbc81 394 my $commit = $self->_get_object($c, $c->req->param('hb'));
c046a52f 395 my $filename = $c->req->param('f') || '';
396 my $tree = $filename
397 ? $project->get_object($project->hash_by_path($commit->sha1, $filename))
398 : $project->get_object($commit->tree_sha1)
399 ;
b3fa97cd 400 $c->stash(
790ce598 401 commit => $commit,
b4b4d0fd 402 tree => $tree,
74af77a3 403 tree_list => [$project->list_tree($tree->sha1)],
832cbc81 404 path => $c->req->param('f') || '',
790ce598 405 action => 'tree',
b3fa97cd 406 );
407}
408
9dc3b9a5 409=head2 reflog
410
411Expose the local reflog. This may go away.
412
413=cut
414
415sub reflog : Local {
416 my ( $self, $c ) = @_;
d8abdf1c 417 my @log = $c->stash->{Project}->reflog(
9dc3b9a5 418 '--since=yesterday'
419 );
420
421 $c->stash(
422 log => \@log,
423 action => 'reflog',
424 );
425}
426
ea19a20c 427=head2 search
428
429The action for the search form.
430
431=cut
432
14664e1c 433sub search : Local {
4df2f62f 434 my($self, $c) = @_;
d8abdf1c 435 $c->stash(current_action => 'GitRepos');
436 my $project = $c->stash->{Project};
832cbc81 437 my $commit = $self->_get_object($c);
4df2f62f 438 # Lifted from /shortlog.
439 my %logargs = (
440 sha1 => $commit->sha1,
441 count => Gitalist->config->{paging}{log},
442 ($c->req->param('f') ? (file => $c->req->param('f')) : ()),
72d72d4d 443 search => {
444 type => $c->req->param('type'),
445 text => $c->req->param('text'),
446 regexp => $c->req->param('regexp') || 0,
447 },
4df2f62f 448 );
449
450 $c->stash(
451 commit => $commit,
d8abdf1c 452 results => [$project->list_revs(%logargs)],
4df2f62f 453 action => 'search',
454 # This could be added - page => $page,
455 );
14664e1c 456}
457
ea19a20c 458=head2 search_help
459
460Provides some help for the search form.
461
462=cut
463
14664e1c 464sub search_help : Local {
2646511e 465 my ($self, $c) = @_;
466 $c->stash(template => 'search_help.tt2');
6cfcd548 467}
468
ea19a20c 469=head2 atom
470
471Provides an atom feed for a given project.
472
473=cut
474
6cfcd548 475sub atom : Local {
d17ce39c 476 my($self, $c) = @_;
6cfcd548 477
d17ce39c 478 my $feed = XML::Atom::Feed->new;
6cfcd548 479
d17ce39c 480 my $host = lc Sys::Hostname::hostname();
481 $feed->title($host . ' - ' . Gitalist->config->{name});
482 $feed->updated(~~DateTime->now);
483
484 my $project = $c->stash->{Project};
485 my %logargs = (
486 sha1 => $project->head_hash,
487 count => Gitalist->config->{paging}{log} || 25,
488 ($c->req->param('f') ? (file => $c->req->param('f')) : ())
489 );
490
491 my $mk_title = $c->stash->{short_cmt};
492 for my $commit ($project->list_revs(%logargs)) {
493 my $entry = XML::Atom::Entry->new;
494 $entry->title( $mk_title->($commit->comment) );
495 $entry->id($c->uri_for('commit', {h=>$commit->sha1}));
496 # XXX Needs work ...
497 $entry->content($commit->comment);
498 $feed->add_entry($entry);
499 }
500
f796a861 501 $c->response->body($feed->as_xml);
e75df318 502 $c->response->content_type('application/atom+xml');
f796a861 503 $c->response->status(200);
6cfcd548 504}
505
ea19a20c 506=head2 rss
507
508Provides an RSS feed for a given project.
509
510=cut
511
6cfcd548 512sub rss : Local {
f796a861 513 my ($self, $c) = @_;
514
515 my $project = $c->stash->{Project};
516
517 my $rss = XML::RSS->new(version => '2.0');
518 $rss->channel(
519 title => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name},
520 link => $c->uri_for('summary', {p=>$project->name}),
521 language => 'en',
522 description => $project->description,
523 pubDate => DateTime->now,
524 lastBuildDate => DateTime->now,
525 );
526
527 my %logargs = (
528 sha1 => $project->head_hash,
529 count => Gitalist->config->{paging}{log} || 25,
530 ($c->req->param('f') ? (file => $c->req->param('f')) : ())
531 );
532 my $mk_title = $c->stash->{short_cmt};
533 for my $commit ($project->list_revs(%logargs)) {
534 # XXX Needs work ....
535 $rss->add_item(
536 title => $mk_title->($commit->comment),
537 permaLink => $c->uri_for(commit => {h=>$commit->sha1}),
538 description => $commit->comment,
539 );
540 }
541
542 $c->response->body($rss->as_string);
543 $c->response->content_type('application/rss+xml');
544 $c->response->status(200);
14664e1c 545}
546
286cea09 547sub opml : Local {
548 my($self, $c) = @_;
549
550 my $opml = XML::OPML::SimpleGen->new();
551
552 $opml->head(title => lc(Sys::Hostname::hostname()) . ' - ' . Gitalist->config->{name});
553
554 my @list = @{ $c->model()->projects };
555 die 'No projects found in '. $c->model->repo_dir
556 unless @list;
557
558 for my $proj ( @list ) {
559 $opml->insert_outline(
560 text => $proj->name. ' - '. $proj->description,
561 xmlUrl => $c->uri_for(rss => {p => $proj->name}),
562 );
563 }
564
565 $c->response->body($opml->as_string);
566 $c->response->content_type('application/rss');
567 $c->response->status(200);
568}
569
ea19a20c 570=head2 patch
571
572A raw patch for a given commit.
573
574=cut
575
6cfcd548 576sub patch : Local {
377bf360 577 my ($self, $c) = @_;
61ba8635 578 $c->detach('patches', [1]);
579}
580
ea19a20c 581=head2 patches
582
583The patcheset for a given commit ???
584
585=cut
586
61ba8635 587sub patches : Local {
588 my ($self, $c, $count) = @_;
589 $count ||= Gitalist->config->{patches}{max};
377bf360 590 my $commit = $self->_get_object($c);
591 my $parent = $c->req->param('hp') || undef;
f707d264 592 my $patch = $commit->get_patch( $parent, $count );
377bf360 593 $c->response->body($patch);
594 $c->response->content_type('text/plain');
595 $c->response->status(200);
6cfcd548 596}
597
ea19a20c 598=head2 snapshot
599
600Provides a snapshot of a given commit.
601
602=cut
603
6cfcd548 604sub snapshot : Local {
30db8f5b 605 my ($self, $c) = @_;
63afe2db 606 my $format = $c->req->param('sf') || 'tgz';
30db8f5b 607 die unless $format;
2e79039a 608 my $sha1 = $c->req->param('h') || $self->_get_object($c)->sha1;
c0dbe239 609 my @snap = $c->stash->{Project}->snapshot(
610 sha1 => $sha1,
611 format => $format
612 );
30db8f5b 613 $c->response->status(200);
614 $c->response->headers->header( 'Content-Disposition' =>
c0dbe239 615 "attachment; filename=$snap[0]");
616 $c->response->body($snap[1]);
6cfcd548 617}
618
1625cd5f 619=head2 auto
620
621Populate the header and footer. Perhaps not the best location.
622
623=cut
624
04d1d917 625sub auto : Private {
4621ecf0 626 my($self, $c) = @_;
04d1d917 627
da8f4f82 628 my $project = $c->req->param('p');
629 if (defined $project) {
630 eval {
631 $c->stash(Project => $c->model('GitRepos')->project($project));
632 };
633 if ($@) {
634 $c->detach('error_404');
635 }
636 }
637
638 my $a_project = $c->stash->{Project} || $c->model()->projects->[0];
4621ecf0 639 $c->stash(
da8f4f82 640 git_version => $a_project->run_cmd('--version'),
641 version => $Gitalist::VERSION,
642
643 # XXX Move these to a plugin!
4621ecf0 644 time_since => sub {
ef11b09e 645 return 'never' unless $_[0];
4621ecf0 646 return age_string(time - $_[0]->epoch);
647 },
648 short_cmt => sub {
649 my $cmt = shift;
650 my($line) = split /\n/, $cmt;
741e110e 651 $line =~ s/^(.{70,80}\b).*/$1 \x{2026}/;
4621ecf0 652 return $line;
653 },
5cd9b9f9 654 abridged_description => sub {
655 join(' ', grep { defined } (split / /, shift)[0..10]);
656 },
4621ecf0 657 );
04d1d917 658}
d9a9b56b 659
89de6a33 660=head2 end
661
662Attempt to render a view, if needed.
663
664=cut
665
fde5091f 666sub end : ActionClass('RenderView') {
1aad4e81 667 my ($self, $c) = @_;
668 # Give project views the current HEAD.
669 if ($c->stash->{Project}) {
670 $c->stash->{HEAD} = $c->stash->{Project}->head_hash;
671 }
1fd8159c 672}
d9a9b56b 673
4111e151 674sub error_404 :Private {
675 my ($self, $c) = @_;
676 $c->response->status(404);
677 $c->stash(
678 title => 'Page not found',
679 content => 'Page not found',
1aad4e81 680 );
4111e151 681}
682
1fd8159c 683sub age_string {
da8f4f82 684 my $age = shift;
685 my $age_str;
686
687 if ( $age > 60 * 60 * 24 * 365 * 2 ) {
688 $age_str = ( int $age / 60 / 60 / 24 / 365 );
689 $age_str .= " years ago";
690 }
691 elsif ( $age > 60 * 60 * 24 * ( 365 / 12 ) * 2 ) {
692 $age_str = int $age / 60 / 60 / 24 / ( 365 / 12 );
693 $age_str .= " months ago";
694 }
695 elsif ( $age > 60 * 60 * 24 * 7 * 2 ) {
696 $age_str = int $age / 60 / 60 / 24 / 7;
697 $age_str .= " weeks ago";
698 }
699 elsif ( $age > 60 * 60 * 24 * 2 ) {
700 $age_str = int $age / 60 / 60 / 24;
701 $age_str .= " days ago";
702 }
703 elsif ( $age > 60 * 60 * 2 ) {
704 $age_str = int $age / 60 / 60;
705 $age_str .= " hours ago";
706 }
707 elsif ( $age > 60 * 2 ) {
708 $age_str = int $age / 60;
709 $age_str .= " min ago";
710 }
711 elsif ( $age > 2 ) {
712 $age_str = int $age;
713 $age_str .= " sec ago";
714 }
715 else {
716 $age_str .= " right now";
717 }
718 return $age_str;
7a88ffa4 719}
720
775e96e0 721__PACKAGE__->meta->make_immutable;
722
723__END__
724
d137f7d5 725=head1 NAME
726
727Gitalist::Controller::Root - Root controller for the application
728
729=head1 DESCRIPTION
730
731This controller handles all of the root level paths for the application
732
733=head1 METHODS
734
735=head2 age_string
736
737=head2 blame
738
739=head2 commitdiff_plain
740
741=head2 error_404
742
743=head2 history
744
745=head2 opml
746
747=head2 project_index
da8f4f82 748
775e96e0 749=head1 AUTHORS
89de6a33 750
775e96e0 751See L<Gitalist> for authors.
89de6a33 752
753=head1 LICENSE
754
775e96e0 755See L<Gitalist> for the license.
89de6a33 756
757=cut