Fix cat_file. (trivial)
[catagits/Gitalist.git] / lib / Gitalist / Model / Git.pm
CommitLineData
fbf3eb7e 1package Gitalist::Model::Git;
2
3use Moose;
4use namespace::autoclean;
7268d0ae 5use MooseX::Types::Common::String qw/NonEmptySimpleStr/;
a3694730 6use Moose::Autobox;
fbf3eb7e 7
37997f11 8extends 'Catalyst::Model';
9with 'Catalyst::Component::InstancePerContext';
86382b95 10
7268d0ae 11has repo_dir => ( is => 'ro', required => 1, isa => NonEmptySimpleStr );
a7cc1ede 12
27e05d7b 13=head1 NAME
14
15Gitalist::Model::Git - the model for git interactions
16
17=head1 DESCRIPTION
18
19[enter your description here]
20
21=head1 METHODS
22
23=cut
24
a3694730 25use Git::PurePerl;
45420449 26use Path::Class qw/dir/;
a3694730 27sub build_per_context_instance {
28 my ( $self, $c ) = @_;
29
06e141d1 30 my $app = blessed($c) || $c;
a3694730 31 my $model = Git::Repos->new(
32 project => ([$c->req->parameters->{p} || '/']->flatten)[0],
7268d0ae 33 repo_dir => $self->repo_dir,
a3694730 34 );
35
36 # This is fugly as fuck. Move Git::PurePerl construction into attribute builders..
45420449 37 my ($pd, $gd) = $model->project_dir( $model->project )->resolve =~ m{((.+?)(:?/\/\.git)?$)};
38 $gd .= '/.git' if ($gd !~ /\.git$/ and -d "$gd/.git");
39 $model->gpp( Git::PurePerl->new(gitdir => $gd, directory => $pd) );
a3694730 40
41 return $model;
42}
43
21336a02 44__PACKAGE__->meta->make_immutable;
45
a3694730 46package Git::Repos; # Better name? Split out into own file once we have a sane name.
47use Moose;
48use namespace::autoclean;
49use DateTime;
50use Path::Class;
51use File::Which;
52use Carp qw/croak/;
53use File::Find::Rule;
54use DateTime::Format::Mail;
55use File::Stat::ModeString;
56use List::MoreUtils qw/any zip/;
57use MooseX::Types::Common::String qw/NonEmptySimpleStr/; # FIXME, use Types::Path::Class and coerce
58
59use Git::PurePerl;
60
b3ad9e63 61# Should these live in a separate module? Or perhaps extended Regexp::Common?
a3694730 62# No, should be a MooseX::Types module!!
b3ad9e63 63our $SHA1RE = qr/[0-9a-fA-F]{40}/;
64
a7cc1ede 65# These are static and only need to be setup on app start.
0caa4dd2 66has repo_dir => ( isa => NonEmptySimpleStr, is => 'ro', required => 1 ); # Fixme - path::class
8c032474 67has git => ( isa => NonEmptySimpleStr, is => 'ro', lazy_build => 1 );
a7cc1ede 68# These are dynamic and can be different from one request to the next.
69has project => ( isa => NonEmptySimpleStr, is => 'rw');
70has gpp => ( isa => 'Git::PurePerl', is => 'rw', lazy_build => 1 );
71
a7cc1ede 72
a7cc1ede 73
27e05d7b 74=head2 BUILD
75
76=cut
77
8c032474 78sub BUILD {
79 my ($self) = @_;
80 $self->git; # Cause lazy value build.
1feb3d6b 81 $self->repo_dir;
8c032474 82}
83
84sub _build_git {
1feb3d6b 85 my $git = File::Which::which('git');
d7c9a32f 86
1feb3d6b 87 if (!$git) {
88 die <<EOR;
fbf3eb7e 89Could not find a git executable.
90Please specify the which git executable to use in gitweb.yml
91EOR
1feb3d6b 92 }
fbf3eb7e 93
1feb3d6b 94 return $git;
04d1d917 95}
1ef8dc7d 96
27e05d7b 97=head2 get_object
98
99A wrapper for the equivalent L<Git::PurePerl> method.
100
101=cut
102
1ef8dc7d 103sub get_object {
a7cc1ede 104 my($self, $sha1) = @_;
105
9dc3b9a5 106 # We either want an object or undef, *not* an empty list.
a7cc1ede 107 return $self->gpp->get_object($sha1) || undef;
8c032474 108}
d7c9a32f 109
27e05d7b 110=head2 is_git_repo
111
112Determine whether a given directory (as a L<Path::Class::Dir> object) is a
113C<git> repo.
114
115=cut
116
fbf3eb7e 117sub is_git_repo {
1feb3d6b 118 my ($self, $dir) = @_;
fbf3eb7e 119
1feb3d6b 120 return -f $dir->file('HEAD') || -f $dir->file('.git/HEAD');
fbf3eb7e 121}
122
27e05d7b 123=head2 run_cmd
124
125Call out to the C<git> binary and return a string consisting of the output.
126
127=cut
128
1ef8dc7d 129sub run_cmd {
130 my ($self, @args) = @_;
131
132 print STDERR 'RUNNING: ', $self->git, qq[ @args], $/;
133
134 open my $fh, '-|', $self->git, @args
135 or die "failed to run git command";
136 binmode $fh, ':encoding(UTF-8)';
137
138 my $output = do { local $/ = undef; <$fh> };
139 close $fh;
140
141 return $output;
142}
143
27e05d7b 144=head2 project_dir
145
146The directory under which the given project will reside i.e C<.git/..>
147
148=cut
149
1ef8dc7d 150sub project_dir {
151 my($self, $project) = @_;
152
153 my $dir = blessed($project) && $project->isa('Path::Class::Dir')
154 ? $project->stringify
27e05d7b 155 : $self->dir_from_project_name($project);
1ef8dc7d 156
27e05d7b 157 $dir .= '/.git'
e679ab75 158 if -f dir($dir)->file('.git/HEAD');
1ef8dc7d 159
160 return $dir;
161}
162
27e05d7b 163=head2 run_cmd_in
164
165Run a C<git> command in a given project and return the output as a string.
166
167=cut
168
1ef8dc7d 169sub run_cmd_in {
170 my ($self, $project, @args) = @_;
171
27e05d7b 172 return $self->run_cmd('--git-dir' => $self->project_dir($project), @args);
1ef8dc7d 173}
174
27e05d7b 175=head2 command
176
177Run a C<git> command for the project specified in the C<p> parameter and
178return the output as a list of strings corresponding to the lines of output.
179
180=cut
181
b3ad9e63 182sub command {
183 my($self, @args) = @_;
184
27e05d7b 185 my $output = $self->run_cmd('--git-dir' => $self->project_dir($self->project), @args);
b3ad9e63 186
187 return $output ? split(/\n/, $output) : ();
188}
189
27e05d7b 190=head2 project_info
191
192Returns a hash corresponding to a given project's properties. The keys will
193be:
194
e679ab75 195 name
196 description (empty if .git/description is empty/unnamed)
197 owner
198 last_change
27e05d7b 199
200=cut
201
fbf3eb7e 202sub project_info {
1feb3d6b 203 my ($self, $project) = @_;
fbf3eb7e 204
1feb3d6b 205 return {
206 name => $project,
207 $self->get_project_properties(
27e05d7b 208 $self->dir_from_project_name($project),
209 ),
210 };
fbf3eb7e 211}
212
27e05d7b 213=head2 get_project_properties
214
215Called by C<project_info> to get a project's properties.
216
217=cut
218
fbf3eb7e 219sub get_project_properties {
1feb3d6b 220 my ($self, $dir) = @_;
221 my %props;
fbf3eb7e 222
1feb3d6b 223 eval {
224 $props{description} = $dir->file('description')->slurp;
225 chomp $props{description};
fbf3eb7e 226 };
227
1feb3d6b 228 if ($props{description} && $props{description} =~ /^Unnamed repository;/) {
229 delete $props{description};
230 }
fbf3eb7e 231
1feb3d6b 232 ($props{owner} = (getpwuid $dir->stat->uid)[6]) =~ s/,+$//;
fbf3eb7e 233
1feb3d6b 234 my $output = $self->run_cmd_in($dir, qw{
235 for-each-ref --format=%(committer)
236 --sort=-committerdate --count=1 refs/heads
237 });
fbf3eb7e 238
1feb3d6b 239 if (my ($epoch, $tz) = $output =~ /\s(\d+)\s+([+-]\d+)$/) {
240 my $dt = DateTime->from_epoch(epoch => $epoch);
241 $dt->set_time_zone($tz);
242 $props{last_change} = $dt;
243 }
fbf3eb7e 244
1feb3d6b 245 return %props;
fbf3eb7e 246}
247
27e05d7b 248=head2 list_projects
249
250For the C<repo_dir> specified in the config return an array of projects where
251each item will contain the contents of L</project_info>.
252
253=cut
254
fbf3eb7e 255sub list_projects {
29b8fbb9 256 my ($self, $dir) = @_;
fbf3eb7e 257
29b8fbb9 258 my $base = dir($dir || $self->repo_dir);
fbf3eb7e 259
260 my @ret;
6ccff25e 261 my $dh = $base->open or die("Cannot open dir $base");
fbf3eb7e 262 while (my $file = $dh->read) {
263 next if $file =~ /^.{1,2}$/;
264
265 my $obj = $base->subdir($file);
266 next unless -d $obj;
267 next unless $self->is_git_repo($obj);
d5cc37a4 268 # XXX Leaky abstraction alert!
e66db0fb 269 my $is_bare = !-d $obj->subdir('.git');
fbf3eb7e 270
e66db0fb 271 my $name = (File::Spec->splitdir($obj))[-1];
fbf3eb7e 272 push @ret, {
c1087225 273 name => ($name . ( $is_bare ? '' : '/.git' )),
e66db0fb 274 $self->get_project_properties(
275 $is_bare ? $obj : $obj->subdir('.git')
276 ),
fbf3eb7e 277 };
1feb3d6b 278 }
279
280 return [sort { $a->{name} cmp $b->{name} } @ret];
fbf3eb7e 281}
282
27e05d7b 283=head2 dir_from_project_name
284
285Get the corresponding directory of a given project.
286
287=cut
288
289sub dir_from_project_name {
1feb3d6b 290 my ($self, $project) = @_;
fbf3eb7e 291
1feb3d6b 292 return dir($self->repo_dir)->subdir($project);
fbf3eb7e 293}
294
27e05d7b 295=head2 head_hash
296
0ee97fec 297Find the hash of a given head (defaults to HEAD) of given (or current) project.
27e05d7b 298
299=cut
300
c8870bd3 301sub head_hash {
e679ab75 302 my ($self, $head, $project) = @_;
fbf3eb7e 303
e679ab75 304 my $output = $self->run_cmd_in($project || $self->project, qw/rev-parse --verify/, $head || 'HEAD' );
305 return unless defined $output;
fbf3eb7e 306
a7cc1ede 307 my($sha1) = $output =~ /^($SHA1RE)$/;
308 return $sha1;
fbf3eb7e 309}
310
27e05d7b 311=head2 list_tree
312
313For a given tree sha1 return an array describing the tree's contents. Where
314the keys for each item will be:
315
e679ab75 316 mode
317 type
318 object
319 file
27e05d7b 320
321=cut
322
fbf3eb7e 323sub list_tree {
e679ab75 324 my ($self, $rev, $project) = @_;
fbf3eb7e 325
e679ab75 326 $project ||= $self->project;
327 $rev ||= $self->head_hash($project);
fbf3eb7e 328
e679ab75 329 my $output = $self->run_cmd_in($project, qw/ls-tree -z/, $rev);
330 return unless defined $output;
fbf3eb7e 331
1feb3d6b 332 my @ret;
333 for my $line (split /\0/, $output) {
334 my ($mode, $type, $object, $file) = split /\s+/, $line, 4;
d7c9a32f 335
1feb3d6b 336 push @ret, {
b3fa97cd 337 mode => oct $mode,
e679ab75 338 # XXX I wonder why directories always turn up as 040000 ...
b3fa97cd 339 modestr => $self->get_object_mode_string({mode=>oct $mode}),
340 type => $type,
341 object => $object,
342 file => $file,
27e05d7b 343 };
1feb3d6b 344 }
345
346 return @ret;
fbf3eb7e 347}
348
27e05d7b 349=head2 get_object_mode_string
350
351Provide a string equivalent of an octal mode e.g 0644 eq '-rw-r--r--'.
352
353=cut
354
fbf3eb7e 355sub get_object_mode_string {
1feb3d6b 356 my ($self, $object) = @_;
fbf3eb7e 357
1feb3d6b 358 return unless $object && $object->{mode};
359 return mode_to_string($object->{mode});
fbf3eb7e 360}
361
27e05d7b 362=head2 get_object_type
363
364=cut
365
fbf3eb7e 366sub get_object_type {
e679ab75 367 my ($self, $object, $project) = @_;
1feb3d6b 368
e679ab75 369 chomp(my $output = $self->run_cmd_in($project || $self->project, qw/cat-file -t/, $object));
370 return unless $output;
1feb3d6b 371
1feb3d6b 372 return $output;
373}
374
27e05d7b 375=head2 cat_file
376
377Return the contents of a given file.
378
379=cut
380
381sub cat_file {
e679ab75 382 my ($self, $object, $project) = @_;
27e05d7b 383
6f788a83 384 my $type = $self->get_object_type($object, $project || $self->project);
27e05d7b 385 die "object `$object' is not a file\n"
386 if (!defined $type || $type ne 'blob');
387
e679ab75 388 my $output = $self->run_cmd_in($project || $self->project, qw/cat-file -p/, $object);
389 return unless $output;
27e05d7b 390
391 return $output;
392}
393
394=head2 hash_by_path
395
396For a given sha1 and path find the corresponding hash. Useful for find blobs.
397
398=cut
399
c8870bd3 400sub hash_by_path {
1feb3d6b 401 my($self, $base, $path, $type) = @_;
fbf3eb7e 402
1feb3d6b 403 $path =~ s{/+$}();
fbf3eb7e 404
c8870bd3 405 my($line) = $self->command('ls-tree', $base, '--', $path)
1feb3d6b 406 or return;
407
e679ab75 408 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
b3ad9e63 409 $line =~ m/^([0-9]+) (.+) ($SHA1RE)\t/;
1feb3d6b 410 return defined $type && $type ne $2
411 ? ()
c8870bd3 412 : $3;
295c9703 413}
414
27e05d7b 415=head2 valid_rev
fbf3eb7e 416
27e05d7b 417Check whether a given rev is valid i.e looks like a sha1.
fbf3eb7e 418
27e05d7b 419=cut
fbf3eb7e 420
421sub valid_rev {
1feb3d6b 422 my ($self, $rev) = @_;
fbf3eb7e 423
1feb3d6b 424 return unless $rev;
b3ad9e63 425 return ($rev =~ /^($SHA1RE)$/);
fbf3eb7e 426}
427
9c0984d1 428=head2 raw_diff
27e05d7b 429
9c0984d1 430Provides the raw output of a diff.
27e05d7b 431
432=cut
433
ad8884fc 434# gitweb uses the following sort of command for diffing merges:
435# /home/dbrook/apps/bin/git --git-dir=/home/dbrook/dev/app/.git diff-tree -r -M --no-commit-id --patch-with-raw --full-index --cc 316cf158df3f6207afbae7270bcc5ba0 --
436# and for regular diffs
437# /home/dbrook/apps/bin/git --git-dir=/home/dbrook/dev/app/.git diff-tree -r -M --no-commit-id --patch-with-raw --full-index 2e3454ca0749641b42f063730b0090e1 316cf158df3f6207afbae7270bcc5ba0 --
438
9c0984d1 439sub raw_diff {
6cf4366a 440 my ($self, @args) = @_;
fbf3eb7e 441
ad8884fc 442 return $self->command(
e679ab75 443 qw(diff-tree -r -M --no-commit-id --full-index),
444 @args
ad8884fc 445 );
9c0984d1 446}
fbf3eb7e 447
ad8884fc 448=pod
9c0984d1 449diff --git a/TODO b/TODO
450index 6a05e77..2071fd0 100644
451--- a/TODO
452+++ b/TODO
453@@ -2,4 +2,3 @@
454 * An action to find what branches have been merged, either as a list or through a search mechanism.
455 * An action to find which branches a given commit is on.
456 * Fix any not text/html bits e.g the patch action.
457-* Simplify the creation of links.
458diff --git a/lib/Gitalist/Controller/Root.pm b/lib/Gitalist/Controller/Root.pm
459index 706d024..7fac165 100644
460--- a/lib/Gitalist/Controller/Root.pm
461+++ b/lib/Gitalist/Controller/Root.pm
462@@ -157,23 +157,6 @@ sub shortlog : Local {
463 );
464 }
465
466-=head2 tree
467-
468-The tree of a given commit.
469=cut
470
471=head2 diff
472
473Returns a list of diff chunks corresponding to the files contained in the diff
474and some associated metadata.
475
476=cut
477
ad8884fc 478# XXX Ideally this would return a wee object instead of ad hoc structures.
9c0984d1 479sub diff {
ad8884fc 480 my($self, %args) = @_;
481
482 # So either a parent is specifed, or we use the commit's parent if there's
483 # only one, otherwise it was a merge commit.
484 my $parent = $args{parent}
1e805b5e 485 ? $args{parent}
0b9ac993 486 : $args{commit}->parents <= 1
1e805b5e 487 ? $args{commit}->parent_sha1
488 : '-c';
ad8884fc 489 my @etc = (
490 ( $args{file} ? ('--', $args{file}) : () ),
491 );
492
493 my @out = $self->raw_diff(
e679ab75 494 ( $args{patch} ? '--patch-with-raw' : () ),
495 $parent, $args{commit}->sha1, @etc
ad8884fc 496 );
9c0984d1 497
ad8884fc 498 # XXX Yes, there is much wrongness having parse_diff_tree be destructive.
499 my @difftree = $self->parse_diff_tree(\@out);
500
501 return \@difftree
e679ab75 502 unless $args{patch};
ad8884fc 503
504 # The blank line between the tree and the patch.
505 shift @out;
506
507 # XXX And no I'm not happy about having diff return tree + patch.
508 return \@difftree, [$self->parse_diff(@out)];
6cf4366a 509}
510
511sub parse_diff {
512 my($self, @diff) = @_;
9c0984d1 513
514 my @ret;
9dc3b9a5 515 for (@diff) {
5a2f0948 516 # This regex is a little pathological.
517 if(m{^diff --git (a/(.*?)) (b/\2)}) {
9c0984d1 518 push @ret, {
5a2f0948 519 head => $_,
520 a => $1,
521 b => $3,
522 file => $2,
523 diff => '',
9c0984d1 524 };
5a2f0948 525 next;
9c0984d1 526 }
5a2f0948 527
528 if(/^index (\w+)\.\.(\w+) (\d+)$/) {
529 @{$ret[-1]}{qw(index src dst mode)} = ($_, $1, $2, $3);
530 next
531 }
532
533 # XXX Somewhat hacky. Ahem.
534 $ret[@ret ? -1 : 0]{diff} .= "$_\n";
9c0984d1 535 }
536
537 return @ret;
fbf3eb7e 538}
539
ad8884fc 540# $ git diff-tree -r --no-commit-id -M b222ff0a7260cc1777c7e455dfcaf22551a512fc 7e54e579e196c6c545fee1030175f65a111039d4
541# :100644 100644 6a85d6c6315b55a99071974eb6ce643aeb2799d6 44c03ed6c328fa6de4b1d9b3f19a3de96b250370 M templates/blob.tt2
542
543=head2 parse_diff_tree
544
545Given a L<Git::PurePerl> commit object return a list of hashes corresponding
546to the C<diff-tree> output.
547
548=cut
549
550sub parse_diff_tree {
551 my($self, $diff) = @_;
552
553 my @keys = qw(modesrc modedst sha1src sha1dst status src dst);
554 my @ret;
b4b4d0fd 555 while(@$diff and $diff->[0] =~ /^:\d+/) {
e679ab75 556 my $line = shift @$diff;
ad8884fc 557 # see. man git-diff-tree for more info
558 # mode src, mode dst, sha1 src, sha1 dst, status, src[, dst]
b4b4d0fd 559 my @vals = $line =~ /^:(\d+) (\d+) ($SHA1RE) ($SHA1RE) ([ACDMRTUX]\d*)\t([^\t]+)(?:\t([^\n]+))?$/;
ad8884fc 560 my %line = zip @keys, @vals;
561 # Some convenience keys
562 $line{file} = $line{src};
563 $line{sha1} = $line{sha1dst};
b4b4d0fd 564 $line{is_new} = $line{sha1src} =~ /^0+$/
e679ab75 565 if $line{sha1src};
566 @line{qw/status sim/} = $line{status} =~ /(R)(\d+)/
b4b4d0fd 567 if $line{status} =~ /^R/;
ad8884fc 568 push @ret, \%line;
569 }
570
571 return @ret;
572}
573
27e05d7b 574=head2 parse_rev_list
575
576Given the output of the C<rev-list> command return a list of hashes.
577
578=cut
579
47495599 580sub parse_rev_list {
581 my ($self, $output) = @_;
47495599 582
30e4d46d 583 return
584 map $self->get_object($_),
585 grep $self->valid_rev($_),
586 map split(/\n/, $_, 6), split /\0/, $output;
fbf3eb7e 587}
588
27e05d7b 589=head2 list_revs
590
591Calls the C<rev-list> command (a low-level from of C<log>) and returns an
592array of hashes.
593
594=cut
595
fbf3eb7e 596sub list_revs {
27e05d7b 597 my ($self, %args) = @_;
fbf3eb7e 598
30e4d46d 599 $args{sha1} = $self->head_hash($args{sha1})
600 if !$args{sha1} || $args{sha1} !~ $SHA1RE;
601
602 my @search_opts;
603 if($args{search}) {
604 my $sargs = $args{search};
605 $sargs->{type} = 'grep'
606 if $sargs->{type} eq 'commit';
607 @search_opts = (
608 # This seems a little fragile ...
609 qq[--$sargs->{type}=$sargs->{text}],
610 '--regexp-ignore-case',
611 $sargs->{regexp} ? '--extended-regexp' : '--fixed-strings'
612 );
613 }
fbf3eb7e 614
27e05d7b 615 my $output = $self->run_cmd_in($args{project} || $self->project, 'rev-list',
1feb3d6b 616 '--header',
30e4d46d 617 (defined $args{ count } ? "--max-count=$args{count}" : ()),
618 (defined $args{ skip } ? "--skip=$args{skip}" : ()),
619 @search_opts,
0ee97fec 620 $args{sha1},
1feb3d6b 621 '--',
27e05d7b 622 ($args{file} ? $args{file} : ()),
623 );
1feb3d6b 624 return unless $output;
fbf3eb7e 625
1feb3d6b 626 my @revs = $self->parse_rev_list($output);
fbf3eb7e 627
790ce598 628 return @revs;
fbf3eb7e 629}
630
27e05d7b 631=head2 rev_info
632
633Get a single piece of revision information for a given sha1.
634
635=cut
636
fbf3eb7e 637sub rev_info {
27e05d7b 638 my($self, $rev, $project) = @_;
fbf3eb7e 639
1feb3d6b 640 return unless $self->valid_rev($rev);
c5065c66 641
27e05d7b 642 return $self->list_revs(
e679ab75 643 rev => $rev, count => 1,
644 ( $project ? (project => $project) : () )
27e05d7b 645 );
1feb3d6b 646}
647
27e05d7b 648=head2 reflog
649
650Calls the C<reflog> command and returns a list of hashes.
651
652=cut
653
1feb3d6b 654sub reflog {
655 my ($self, @logargs) = @_;
656
657 my @entries
658 = $self->run_cmd_in($self->project, qw(log -g), @logargs)
659 =~ /(^commit.+?(?:(?=^commit)|(?=\z)))/msg;
660
ad8884fc 661=pod
1feb3d6b 662 commit 02526fc15beddf2c64798a947fecdd8d11bf993d
663 Reflog: HEAD@{14} (The Git Server <git@git.dev.venda.com>)
664 Reflog message: push
1ef8dc7d 665 Author: Foo Barsby <fbarsby@example.com>
1feb3d6b 666 Date: Thu Sep 17 12:26:05 2009 +0100
667
1ef8dc7d 668 Merge branch 'abc123'
1feb3d6b 669=cut
670
671 return map {
672
673 # XXX Stuff like this makes me want to switch to Git::PurePerl
674 my($sha1, $type, $author, $date)
675 = m{
b3ad9e63 676 ^ commit \s+ ($SHA1RE)$
1feb3d6b 677 .*?
678 Reflog[ ]message: \s+ (.+?)$ \s+
679 Author: \s+ ([^<]+) <.*?$ \s+
680 Date: \s+ (.+?)$
27e05d7b 681 }xms;
1feb3d6b 682
683 pos($_) = index($_, $date) + length $date;
684
685 # Yeah, I just did that.
686
687 my($msg) = /\G\s+(\S.*)/sg;
688
689 {
690 hash => $sha1,
691 type => $type,
692 author => $author,
693
694 # XXX Add DateTime goodness.
695 date => $date,
696 message => $msg,
697 };
1ef8dc7d 698 } @entries;
c5065c66 699}
700
790ce598 701=head2 heads
27e05d7b 702
703Returns an array of hashes representing the heads (aka branches) for the
704given, or current, project.
705
706=cut
707
790ce598 708sub heads {
1feb3d6b 709 my ($self, $project) = @_;
fbf3eb7e 710
790ce598 711 my @output = $self->command(qw/for-each-ref --sort=-committerdate /, '--format=%(objectname)%00%(refname)%00%(committer)', 'refs/heads');
fbf3eb7e 712
1feb3d6b 713 my @ret;
790ce598 714 for my $line (@output) {
1feb3d6b 715 my ($rev, $head, $commiter) = split /\0/, $line, 3;
716 $head =~ s!^refs/heads/!!;
fbf3eb7e 717
790ce598 718 push @ret, { sha1 => $rev, name => $head };
fbf3eb7e 719
1feb3d6b 720 #FIXME: That isn't the time I'm looking for..
2247133f 721 if (my ($epoch, $tz) = $line =~ /\s(\d+)\s+([+-]\d+)$/) {
1feb3d6b 722 my $dt = DateTime->from_epoch(epoch => $epoch);
723 $dt->set_time_zone($tz);
724 $ret[-1]->{last_change} = $dt;
fbf3eb7e 725 }
1feb3d6b 726 }
fbf3eb7e 727
790ce598 728 return @ret;
fbf3eb7e 729}
730
1ef8dc7d 731=head2 refs_for
732
27e05d7b 733For a given sha1 check which branches currently point at it.
1ef8dc7d 734
735=cut
736
737sub refs_for {
e679ab75 738 my($self, $sha1) = @_;
1ef8dc7d 739
e679ab75 740 my $refs = $self->references->{$sha1};
1ef8dc7d 741
e679ab75 742 return $refs ? @$refs : ();
1ef8dc7d 743}
744
27e05d7b 745=head2 references
1ef8dc7d 746
747A wrapper for C<git show-ref --dereference>. Based on gitweb's
748C<git_get_references>.
749
750=cut
751
752sub references {
e679ab75 753 my($self) = @_;
1ef8dc7d 754
e679ab75 755 return $self->{references}
756 if $self->{references};
1ef8dc7d 757
e679ab75 758 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
759 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
760 my @reflist = $self->command(qw(show-ref --dereference))
761 or return;
1ef8dc7d 762
e679ab75 763 my %refs;
764 for(@reflist) {
765 push @{$refs{$1}}, $2
766 if m!^($SHA1RE)\srefs/(.*)$!;
767 }
1ef8dc7d 768
e679ab75 769 return $self->{references} = \%refs;
1ef8dc7d 770}
771
fbf3eb7e 7721;
773
774__PACKAGE__->meta->make_immutable;