3 # git-rev-list | git-diff-tree --stdin following renames
4 # Copyright (c) Petr Baudis, 2006
5 # Uses bits of git-annotate.perl by Ryan Anderson.
7 # This script will efficiently show output as of the
9 # git-rev-list --remove-empty ARGS -- FILE... |
10 # git-diff-tree -M -r -m --stdin --pretty=raw ARGS
12 # pipeline, except that it follows renames of individual files listed
17 # cg-Xfollowrenames revlistargs -- difftreeargs -- revs -- files
21 # TODO: BROKEN - if a file is removed (that is, added) on one branch,
22 # we will stop watching for the renames on all the other branches. We need
23 # to separate the heads and pipes.
25 # TODO: Does not work on multiple files properly yet - most probably
26 # (I didn't test it!). We want git-rev-list to stop traversing the history
27 # when _any_ file disappears while now it probably stops traversing when
28 # _all_ files disappear.
35 our (@revlist_args, @difftree_args, @revs, @files);
38 my @argp = (\
@revlist_args, \
@difftree_args, \
@revs, \
@files);
41 if ($arg eq '--' and $argi < $#argp) {
45 push(@
{$argp[$argi]}, $arg);
50 # The heads we watch (sorted by commit time)
53 # # Persistent for the whole line of development:
55 # files => \@files, # to watch for
57 # id => $sha1, # useful actually only for debugging
59 # str => $prettyoutput,
62 # # When the commit is processed, spawn these extra heads:
63 # recurse => {$sha1id => \@files, ...},
66 # To avoid printing duplicate commits
67 # FIXME: Currently, we will not handle merge commits properly since
68 # we hit them multiple times.
73 my ($stdin, @execlist) = @_;
75 my $pid = open my $kid, "-|";
76 defined $pid or die "Cannot fork: $!";
80 open STDIN
, "<&", $stdin or die "Cannot dup(): $!";
83 die "Cannot exec @execlist: $!";
90 my ($rev, @files) = @_;
91 open_pipe
(undef, "git-rev-list", "--remove-empty",
92 @revlist_args, $rev, "--", @files)
93 or die "Failed to exec git-rev-list: $!";
98 open_pipe
($revlist, "git-diff-tree", "-r", "-m", "--stdin", "-M",
99 "--pretty=raw", @difftree_args)
100 or die "Failed to exec git-diff-tree: $!";
103 sub revdiffpipe
($@
) {
104 my ($rev, @files) = @_;
105 my $pipe = difftree
(revlist
($rev, @files));
109 sub read_commit
($$) {
110 my ($head, $tolerant) = @_;
111 my $pipe = $head->{'pipe'};
113 my @oldset = @
{$head->{'files'}};
118 while (my $line = <$pipe>) {
119 $head->{'str'} .= $line;
121 $line eq '' and goto header_loaded
;
123 if ($line =~ /^diff-tree (\S+) \(from (root|\S+)\)/) {
125 if (not $tolerant and $commits{$1}++) {
129 # The 'root' case is harmless since there'll be no renames.
131 } elsif ($line =~ /^parent (\S+)/) {
132 push (@
{$head->{'parents'}}, $1);
133 } elsif ($line =~ /^committer .*?> (\d+)/) {
134 $head->{'time'} = $1;
141 while (my $line = <$pipe>) {
142 $head->{'str'} .= $line;
144 $line eq '' and goto message_loaded
;
150 # Note that we must interpret the patch we are seeing _reverse_.
151 while (my $line = <$pipe>) {
152 $head->{'str'} .= $line;
154 $line eq '' and goto delta_loaded
;
156 $line =~ /^:/ or return undef;
157 my ($info, $newfile, $oldfile) = split("\t", $line);
158 if ($info =~ /[RC]\d*$/) {
160 # (Or a copy, it's all the same for us.)
162 for ($i = 0; $i <= $#oldset; $i++) {
163 $oldfile eq $oldset[$i] or next;
165 splice(@oldset, $i, 1);
166 push(@newset, $newfile);
169 # In case of multiple candidates, follow
171 # (TODO: This might be a policy decision
172 # best left on the user.)
173 if ($i > $#oldset and grep { $oldfile eq $_ } @newset) {
175 push(@newset, $newfile);
177 } elsif ($info =~ /A$/) {
178 # Not weeding out deleted files (the patch is reversed
179 # so they appear as added to us) might cause bizarre
180 # results when following multiple files since
181 # git-rev-list weeds them out too (probably?).
182 #print STDERR "grepping - @oldset, @{$head->{files}} > MINUS $newfile <\n";
183 @oldset = grep { $newfile ne $_ } @oldset;
184 @
{$head->{'files'}} = grep { $newfile ne $_ } @
{$head->{'files'}};
185 #print STDERR "post-grepping - @oldset, @{$head->{files}} <\n";
188 $head->{'str'} .= "\n";
192 $head->{'recurse'}->{$against} = [@newset, @oldset];
199 $head->{'time'} = undef;
201 $head->{'parents'} = ();
203 read_commit
($head, 0) or return undef;
205 # In case there was a merge, the commit will be multiple times
206 # here, each time with a different delta section. Read them all.
207 for (1 .. $#{$head->{'parents'}}) { # stupid vim syntax highlighting
208 read_commit
($head, 1) or return undef;
211 # Cut the last \n. We don't want it for the last commit.
212 substr($head->{'str'}, -1, 1, '');
218 # Add head at the proper position
222 for ($i = 0; $i <= $#heads; $i++) {
223 last if ($head->{'time'} > $heads[$i]->{'time'})
225 splice(@heads, $i, 0, $head);
230 my ($rev, @files) = @_;
231 my $head = { files
=> \
@files, 'pipe' => revdiffpipe
($rev, @files) };
232 load_commit
($head) or return;
238 { # Seed the heads list
239 for my $rev (@revs) {
240 init_head
($rev, @files);
248 my $head = shift(@heads);
250 print "\n" unless $first; $first = 0;
251 print $head->{'str'};
253 foreach my $parent (keys %{$head->{'recurse'}}) {
254 init_head
($parent, @
{$head->{'recurse'}->{$parent}});
256 $head->{'recurse'} = undef;
258 load_commit
($head) or next;