Merge fixes early for next maint series.
[git.git] / git-annotate.perl
1 #!/usr/bin/perl
2 # Copyright 2006, Ryan Anderson <ryan@michonline.com>
3 #
4 # GPL v2 (See COPYING)
5 #
6 # This file is licensed under the GPL v2, or a later version
7 # at the discretion of Linus Torvalds.
8
9 use warnings;
10 use strict;
11 use Getopt::Std;
12 use POSIX qw(strftime gmtime);
13
14 sub usage() {
15         print STDERR 'Usage: ${\basename $0} [-s] [-S revs-file] file
16
17         -l              show long rev
18         -r              follow renames
19         -S commit       use revs from revs-file instead of calling git-rev-list
20 ';
21
22         exit(1);
23 }
24
25 our ($opt_h, $opt_l, $opt_r, $opt_S);
26 getopts("hlrS:") or usage();
27 $opt_h && usage();
28
29 my $filename = shift @ARGV;
30
31 my @stack = (
32         {
33                 'rev' => "HEAD",
34                 'filename' => $filename,
35         },
36 );
37
38 our (@lineoffsets, @pendinglineoffsets);
39 our @filelines = ();
40 open(F,"<",$filename)
41         or die "Failed to open filename: $!";
42
43 while(<F>) {
44         chomp;
45         push @filelines, $_;
46 }
47 close(F);
48 our $leftover_lines = @filelines;
49 our %revs;
50 our @revqueue;
51 our $head;
52
53 my $revsprocessed = 0;
54 while (my $bound = pop @stack) {
55         my @revisions = git_rev_list($bound->{'rev'}, $bound->{'filename'});
56         foreach my $revinst (@revisions) {
57                 my ($rev, @parents) = @$revinst;
58                 $head ||= $rev;
59
60                 if (!defined($rev)) {
61                         $rev = "";
62                 }
63                 $revs{$rev}{'filename'} = $bound->{'filename'};
64                 if (scalar @parents > 0) {
65                         $revs{$rev}{'parents'} = \@parents;
66                         next;
67                 }
68
69                 if (!$opt_r) {
70                         next;
71                 }
72
73                 my $newbound = find_parent_renames($rev, $bound->{'filename'});
74                 if ( exists $newbound->{'filename'} && $newbound->{'filename'} ne $bound->{'filename'}) {
75                         push @stack, $newbound;
76                         $revs{$rev}{'parents'} = [$newbound->{'rev'}];
77                 }
78         }
79 }
80 push @revqueue, $head;
81 init_claim($head);
82 $revs{$head}{'lineoffsets'} = {};
83 handle_rev();
84
85
86 my $i = 0;
87 foreach my $l (@filelines) {
88         my ($output, $rev, $committer, $date);
89         if (ref $l eq 'ARRAY') {
90                 ($output, $rev, $committer, $date) = @$l;
91                 if (!$opt_l && length($rev) > 8) {
92                         $rev = substr($rev,0,8);
93                 }
94         } else {
95                 $output = $l;
96                 ($rev, $committer, $date) = ('unknown', 'unknown', 'unknown');
97         }
98
99         printf("%s\t(%10s\t%10s\t%d)%s\n", $rev, $committer,
100                 format_date($date), $i++, $output);
101 }
102
103 sub init_claim {
104         my ($rev) = @_;
105         my %revinfo = git_commit_info($rev);
106         for (my $i = 0; $i < @filelines; $i++) {
107                 $filelines[$i] = [ $filelines[$i], '', '', '', 1];
108                         # line,
109                         # rev,
110                         # author,
111                         # date,
112                         # 1 <-- belongs to the original file.
113         }
114         $revs{$rev}{'lines'} = \@filelines;
115 }
116
117
118 sub handle_rev {
119         my $i = 0;
120         while (my $rev = shift @revqueue) {
121
122                 my %revinfo = git_commit_info($rev);
123
124                 foreach my $p (@{$revs{$rev}{'parents'}}) {
125
126                         git_diff_parse($p, $rev, %revinfo);
127                         push @revqueue, $p;
128                 }
129
130
131                 if (scalar @{$revs{$rev}{parents}} == 0) {
132                         # We must be at the initial rev here, so claim everything that is left.
133                         for (my $i = 0; $i < @{$revs{$rev}{lines}}; $i++) {
134                                 if (ref ${$revs{$rev}{lines}}[$i] eq '' || ${$revs{$rev}{lines}}[$i][1] eq '') {
135                                         claim_line($i, $rev, $revs{$rev}{lines}, %revinfo);
136                                 }
137                         }
138                 }
139         }
140 }
141
142
143 sub git_rev_list {
144         my ($rev, $file) = @_;
145
146         if ($opt_S) {
147                 open(P, '<' . $opt_S);
148         } else {
149                 open(P,"-|","git-rev-list","--parents","--remove-empty",$rev,"--",$file)
150                         or die "Failed to exec git-rev-list: $!";
151         }
152
153         my @revs;
154         while(my $line = <P>) {
155                 chomp $line;
156                 my ($rev, @parents) = split /\s+/, $line;
157                 push @revs, [ $rev, @parents ];
158         }
159         close(P);
160
161         printf("0 revs found for rev %s (%s)\n", $rev, $file) if (@revs == 0);
162         return @revs;
163 }
164
165 sub find_parent_renames {
166         my ($rev, $file) = @_;
167
168         open(P,"-|","git-diff-tree", "-M50", "-r","--name-status", "-z","$rev")
169                 or die "Failed to exec git-diff: $!";
170
171         local $/ = "\0";
172         my %bound;
173         my $junk = <P>;
174         while (my $change = <P>) {
175                 chomp $change;
176                 my $filename = <P>;
177                 chomp $filename;
178
179                 if ($change =~ m/^[AMD]$/ ) {
180                         next;
181                 } elsif ($change =~ m/^R/ ) {
182                         my $oldfilename = $filename;
183                         $filename = <P>;
184                         chomp $filename;
185                         if ( $file eq $filename ) {
186                                 my $parent = git_find_parent($rev, $oldfilename);
187                                 @bound{'rev','filename'} = ($parent, $oldfilename);
188                                 last;
189                         }
190                 }
191         }
192         close(P);
193
194         return \%bound;
195 }
196
197
198 sub git_find_parent {
199         my ($rev, $filename) = @_;
200
201         open(REVPARENT,"-|","git-rev-list","--remove-empty", "--parents","--max-count=1","$rev","--",$filename)
202                 or die "Failed to open git-rev-list to find a single parent: $!";
203
204         my $parentline = <REVPARENT>;
205         chomp $parentline;
206         my ($revfound,$parent) = split m/\s+/, $parentline;
207
208         close(REVPARENT);
209
210         return $parent;
211 }
212
213
214 # Get a diff between the current revision and a parent.
215 # Record the commit information that results.
216 sub git_diff_parse {
217         my ($parent, $rev, %revinfo) = @_;
218
219         my ($ri, $pi) = (0,0);
220         open(DIFF,"-|","git-diff-tree","-M","-p",$rev,$parent,"--",
221                         $revs{$rev}{'filename'}, $revs{$parent}{'filename'})
222                 or die "Failed to call git-diff for annotation: $!";
223
224         my $slines = $revs{$rev}{'lines'};
225         my @plines;
226
227         my $gotheader = 0;
228         my ($remstart, $remlength, $addstart, $addlength);
229         my ($hunk_start, $hunk_index, $hunk_adds);
230         while(<DIFF>) {
231                 chomp;
232                 if (m/^@@ -(\d+),(\d+) \+(\d+),(\d+)/) {
233                         ($remstart, $remlength, $addstart, $addlength) = ($1, $2, $3, $4);
234                         # Adjust for 0-based arrays
235                         $remstart--;
236                         $addstart--;
237                         # Reinit hunk tracking.
238                         $hunk_start = $remstart;
239                         $hunk_index = 0;
240                         $gotheader = 1;
241
242                         for (my $i = $ri; $i < $remstart; $i++) {
243                                 $plines[$pi++] = $slines->[$i];
244                                 $ri++;
245                         }
246                         next;
247                 } elsif (!$gotheader) {
248                         next;
249                 }
250
251                 if (m/^\+(.*)$/) {
252                         my $line = $1;
253                         $plines[$pi++] = [ $line, '', '', '', 0 ];
254                         next;
255
256                 } elsif (m/^-(.*)$/) {
257                         my $line = $1;
258                         if (get_line($slines, $ri) eq $line) {
259                                 # Found a match, claim
260                                 claim_line($ri, $rev, $slines, %revinfo);
261                         } else {
262                                 die sprintf("Sync error: %d/%d\n|%s\n|%s\n%s => %s\n",
263                                                 $ri, $hunk_start + $hunk_index,
264                                                 $line,
265                                                 get_line($slines, $ri),
266                                                 $rev, $parent);
267                         }
268                         $ri++;
269
270                 } else {
271                         if (substr($_,1) ne get_line($slines,$ri) ) {
272                                 die sprintf("Line %d (%d) does not match:\n|%s\n|%s\n%s => %s\n",
273                                                 $hunk_start + $hunk_index, $ri,
274                                                 substr($_,1),
275                                                 get_line($slines,$ri),
276                                                 $rev, $parent);
277                         }
278                         $plines[$pi++] = $slines->[$ri++];
279                 }
280                 $hunk_index++;
281         }
282         close(DIFF);
283         for (my $i = $ri; $i < @{$slines} ; $i++) {
284                 push @plines, $slines->[$ri++];
285         }
286
287         $revs{$parent}{lines} = \@plines;
288         return;
289 }
290
291 sub get_line {
292         my ($lines, $index) = @_;
293
294         return ref $lines->[$index] ne '' ? $lines->[$index][0] : $lines->[$index];
295 }
296
297 sub git_cat_file {
298         my ($parent, $filename) = @_;
299         return () unless defined $parent && defined $filename;
300         my $blobline = `git-ls-tree $parent $filename`;
301         my ($mode, $type, $blob, $tfilename) = split(/\s+/, $blobline, 4);
302
303         open(C,"-|","git-cat-file", "blob", $blob)
304                 or die "Failed to git-cat-file blob $blob (rev $parent, file $filename): " . $!;
305
306         my @lines;
307         while(<C>) {
308                 chomp;
309                 push @lines, $_;
310         }
311         close(C);
312
313         return @lines;
314 }
315
316
317 sub claim_line {
318         my ($floffset, $rev, $lines, %revinfo) = @_;
319         my $oline = get_line($lines, $floffset);
320         @{$lines->[$floffset]} = ( $oline, $rev,
321                 $revinfo{'author'}, $revinfo{'author_date'} );
322         #printf("Claiming line %d with rev %s: '%s'\n",
323         #               $floffset, $rev, $oline) if 1;
324 }
325
326 sub git_commit_info {
327         my ($rev) = @_;
328         open(COMMIT, "-|","git-cat-file", "commit", $rev)
329                 or die "Failed to call git-cat-file: $!";
330
331         my %info;
332         while(<COMMIT>) {
333                 chomp;
334                 last if (length $_ == 0);
335
336                 if (m/^author (.*) <(.*)> (.*)$/) {
337                         $info{'author'} = $1;
338                         $info{'author_email'} = $2;
339                         $info{'author_date'} = $3;
340                 } elsif (m/^committer (.*) <(.*)> (.*)$/) {
341                         $info{'committer'} = $1;
342                         $info{'committer_email'} = $2;
343                         $info{'committer_date'} = $3;
344                 }
345         }
346         close(COMMIT);
347
348         return %info;
349 }
350
351 sub format_date {
352         my ($timestamp, $timezone) = split(' ', $_[0]);
353
354         return strftime("%Y-%m-%d %H:%M:%S " . $timezone, gmtime($timestamp));
355 }
356