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