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