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