Merge branch 'maint'
[git] / perl / Git / SVN / Log.pm
1 package Git::SVN::Log;
2 use strict;
3 use warnings;
4 use Git::SVN::Utils qw(fatal);
5 use Git qw(command
6            command_oneline
7            command_output_pipe
8            command_close_pipe
9            get_tz_offset);
10 use POSIX qw/strftime/;
11 use constant commit_log_separator => ('-' x 72) . "\n";
12 use vars qw/$TZ $limit $color $pager $non_recursive $verbose $oneline
13             %rusers $show_commit $incremental/;
14
15 # Option set in git-svn
16 our $_git_format;
17
18 sub cmt_showable {
19         my ($c) = @_;
20         return 1 if defined $c->{r};
21
22         # big commit message got truncated by the 16k pretty buffer in rev-list
23         if ($c->{l} && $c->{l}->[-1] eq "...\n" &&
24                                 $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) {
25                 @{$c->{l}} = ();
26                 my @log = command(qw/cat-file commit/, $c->{c});
27
28                 # shift off the headers
29                 shift @log while ($log[0] ne '');
30                 shift @log;
31
32                 # TODO: make $c->{l} not have a trailing newline in the future
33                 @{$c->{l}} = map { "$_\n" } grep !/^git-svn-id: /, @log;
34
35                 (undef, $c->{r}, undef) = ::extract_metadata(
36                                 (grep(/^git-svn-id: /, @log))[-1]);
37         }
38         return defined $c->{r};
39 }
40
41 sub log_use_color {
42         return $color || Git->repository->get_colorbool('color.diff');
43 }
44
45 sub git_svn_log_cmd {
46         my ($r_min, $r_max, @args) = @_;
47         my $head = 'HEAD';
48         my (@files, @log_opts);
49         foreach my $x (@args) {
50                 if ($x eq '--' || @files) {
51                         push @files, $x;
52                 } else {
53                         if (::verify_ref("$x^0")) {
54                                 $head = $x;
55                         } else {
56                                 push @log_opts, $x;
57                         }
58                 }
59         }
60
61         my ($url, $rev, $uuid, $gs) = ::working_head_info($head);
62
63         require Git::SVN;
64         $gs ||= Git::SVN->_new;
65         my @cmd = (qw/log --abbrev-commit --pretty=raw --default/,
66                    $gs->refname);
67         push @cmd, '-r' unless $non_recursive;
68         push @cmd, qw/--raw --name-status/ if $verbose;
69         push @cmd, '--color' if log_use_color();
70         push @cmd, @log_opts;
71         if (defined $r_max && $r_max == $r_min) {
72                 push @cmd, '--max-count=1';
73                 if (my $c = $gs->rev_map_get($r_max)) {
74                         push @cmd, $c;
75                 }
76         } elsif (defined $r_max) {
77                 if ($r_max < $r_min) {
78                         ($r_min, $r_max) = ($r_max, $r_min);
79                 }
80                 my (undef, $c_max) = $gs->find_rev_before($r_max, 1, $r_min);
81                 my (undef, $c_min) = $gs->find_rev_after($r_min, 1, $r_max);
82                 # If there are no commits in the range, both $c_max and $c_min
83                 # will be undefined.  If there is at least 1 commit in the
84                 # range, both will be defined.
85                 return () if !defined $c_min || !defined $c_max;
86                 if ($c_min eq $c_max) {
87                         push @cmd, '--max-count=1', $c_min;
88                 } else {
89                         push @cmd, '--boundary', "$c_min..$c_max";
90                 }
91         }
92         return (@cmd, @files);
93 }
94
95 # adapted from pager.c
96 sub config_pager {
97         if (! -t *STDOUT) {
98                 $ENV{GIT_PAGER_IN_USE} = 'false';
99                 $pager = undef;
100                 return;
101         }
102         chomp($pager = command_oneline(qw(var GIT_PAGER)));
103         if ($pager eq 'cat') {
104                 $pager = undef;
105         }
106         $ENV{GIT_PAGER_IN_USE} = defined($pager);
107 }
108
109 sub run_pager {
110         return unless defined $pager;
111         pipe my ($rfd, $wfd) or return;
112         defined(my $pid = fork) or fatal "Can't fork: $!";
113         if (!$pid) {
114                 open STDOUT, '>&', $wfd or
115                                      fatal "Can't redirect to stdout: $!";
116                 return;
117         }
118         open STDIN, '<&', $rfd or fatal "Can't redirect stdin: $!";
119         $ENV{LESS} ||= 'FRSX';
120         exec $pager or fatal "Can't run pager: $! ($pager)";
121 }
122
123 sub format_svn_date {
124         my $t = shift || time;
125         require Git::SVN;
126         my $gmoff = get_tz_offset($t);
127         return strftime("%Y-%m-%d %H:%M:%S $gmoff (%a, %d %b %Y)", localtime($t));
128 }
129
130 sub parse_git_date {
131         my ($t, $tz) = @_;
132         # Date::Parse isn't in the standard Perl distro :(
133         if ($tz =~ s/^\+//) {
134                 $t += tz_to_s_offset($tz);
135         } elsif ($tz =~ s/^\-//) {
136                 $t -= tz_to_s_offset($tz);
137         }
138         return $t;
139 }
140
141 sub set_local_timezone {
142         if (defined $TZ) {
143                 $ENV{TZ} = $TZ;
144         } else {
145                 delete $ENV{TZ};
146         }
147 }
148
149 sub tz_to_s_offset {
150         my ($tz) = @_;
151         $tz =~ s/(\d\d)$//;
152         return ($1 * 60) + ($tz * 3600);
153 }
154
155 sub get_author_info {
156         my ($dest, $author, $t, $tz) = @_;
157         $author =~ s/(?:^\s*|\s*$)//g;
158         $dest->{a_raw} = $author;
159         my $au;
160         if ($::_authors) {
161                 $au = $rusers{$author} || undef;
162         }
163         if (!$au) {
164                 ($au) = ($author =~ /<([^>]+)\@[^>]+>$/);
165         }
166         $dest->{t} = $t;
167         $dest->{tz} = $tz;
168         $dest->{a} = $au;
169         $dest->{t_utc} = parse_git_date($t, $tz);
170 }
171
172 sub process_commit {
173         my ($c, $r_min, $r_max, $defer) = @_;
174         if (defined $r_min && defined $r_max) {
175                 if ($r_min == $c->{r} && $r_min == $r_max) {
176                         show_commit($c);
177                         return 0;
178                 }
179                 return 1 if $r_min == $r_max;
180                 if ($r_min < $r_max) {
181                         # we need to reverse the print order
182                         return 0 if (defined $limit && --$limit < 0);
183                         push @$defer, $c;
184                         return 1;
185                 }
186                 if ($r_min != $r_max) {
187                         return 1 if ($r_min < $c->{r});
188                         return 1 if ($r_max > $c->{r});
189                 }
190         }
191         return 0 if (defined $limit && --$limit < 0);
192         show_commit($c);
193         return 1;
194 }
195
196 my $l_fmt;
197 sub show_commit {
198         my $c = shift;
199         if ($oneline) {
200                 my $x = "\n";
201                 if (my $l = $c->{l}) {
202                         while ($l->[0] =~ /^\s*$/) { shift @$l }
203                         $x = $l->[0];
204                 }
205                 $l_fmt ||= 'A' . length($c->{r});
206                 print 'r',pack($l_fmt, $c->{r}),' | ';
207                 print "$c->{c} | " if $show_commit;
208                 print $x;
209         } else {
210                 show_commit_normal($c);
211         }
212 }
213
214 sub show_commit_changed_paths {
215         my ($c) = @_;
216         return unless $c->{changed};
217         print "Changed paths:\n", @{$c->{changed}};
218 }
219
220 sub show_commit_normal {
221         my ($c) = @_;
222         print commit_log_separator, "r$c->{r} | ";
223         print "$c->{c} | " if $show_commit;
224         print "$c->{a} | ", format_svn_date($c->{t_utc}), ' | ';
225         my $nr_line = 0;
226
227         if (my $l = $c->{l}) {
228                 while ($l->[$#$l] eq "\n" && $#$l > 0
229                                           && $l->[($#$l - 1)] eq "\n") {
230                         pop @$l;
231                 }
232                 $nr_line = scalar @$l;
233                 if (!$nr_line) {
234                         print "1 line\n\n\n";
235                 } else {
236                         if ($nr_line == 1) {
237                                 $nr_line = '1 line';
238                         } else {
239                                 $nr_line .= ' lines';
240                         }
241                         print $nr_line, "\n";
242                         show_commit_changed_paths($c);
243                         print "\n";
244                         print $_ foreach @$l;
245                 }
246         } else {
247                 print "1 line\n";
248                 show_commit_changed_paths($c);
249                 print "\n";
250
251         }
252         foreach my $x (qw/raw stat diff/) {
253                 if ($c->{$x}) {
254                         print "\n";
255                         print $_ foreach @{$c->{$x}}
256                 }
257         }
258 }
259
260 sub cmd_show_log {
261         my (@args) = @_;
262         my ($r_min, $r_max);
263         my $r_last = -1; # prevent dupes
264         set_local_timezone();
265         if (defined $::_revision) {
266                 if ($::_revision =~ /^(\d+):(\d+)$/) {
267                         ($r_min, $r_max) = ($1, $2);
268                 } elsif ($::_revision =~ /^\d+$/) {
269                         $r_min = $r_max = $::_revision;
270                 } else {
271                         fatal "-r$::_revision is not supported, use ",
272                                 "standard 'git log' arguments instead";
273                 }
274         }
275
276         config_pager();
277         @args = git_svn_log_cmd($r_min, $r_max, @args);
278         if (!@args) {
279                 print commit_log_separator unless $incremental || $oneline;
280                 return;
281         }
282         my $log = command_output_pipe(@args);
283         run_pager();
284         my (@k, $c, $d, $stat);
285         my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
286         while (<$log>) {
287                 if (/^${esc_color}commit (?:- )?($::sha1_short)/o) {
288                         my $cmt = $1;
289                         if ($c && cmt_showable($c) && $c->{r} != $r_last) {
290                                 $r_last = $c->{r};
291                                 process_commit($c, $r_min, $r_max, \@k) or
292                                                                 goto out;
293                         }
294                         $d = undef;
295                         $c = { c => $cmt };
296                 } elsif (/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {
297                         get_author_info($c, $1, $2, $3);
298                 } elsif (/^${esc_color}(?:tree|parent|committer) /o) {
299                         # ignore
300                 } elsif (/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {
301                         push @{$c->{raw}}, $_;
302                 } elsif (/^${esc_color}[ACRMDT]\t/) {
303                         # we could add $SVN->{svn_path} here, but that requires
304                         # remote access at the moment (repo_path_split)...
305                         s#^(${esc_color})([ACRMDT])\t#$1   $2 #o;
306                         push @{$c->{changed}}, $_;
307                 } elsif (/^${esc_color}diff /o) {
308                         $d = 1;
309                         push @{$c->{diff}}, $_;
310                 } elsif ($d) {
311                         push @{$c->{diff}}, $_;
312                 } elsif (/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*
313                           $esc_color*[\+\-]*$esc_color$/x) {
314                         $stat = 1;
315                         push @{$c->{stat}}, $_;
316                 } elsif ($stat && /^ \d+ files changed, \d+ insertions/) {
317                         push @{$c->{stat}}, $_;
318                         $stat = undef;
319                 } elsif (/^${esc_color}    (git-svn-id:.+)$/o) {
320                         ($c->{url}, $c->{r}, undef) = ::extract_metadata($1);
321                 } elsif (s/^${esc_color}    //o) {
322                         push @{$c->{l}}, $_;
323                 }
324         }
325         if ($c && defined $c->{r} && $c->{r} != $r_last) {
326                 $r_last = $c->{r};
327                 process_commit($c, $r_min, $r_max, \@k);
328         }
329         if (@k) {
330                 ($r_min, $r_max) = ($r_max, $r_min);
331                 process_commit($_, $r_min, $r_max) foreach reverse @k;
332         }
333 out:
334         close $log;
335         print commit_log_separator unless $incremental || $oneline;
336 }
337
338 sub cmd_blame {
339         my $path = pop;
340
341         config_pager();
342         run_pager();
343
344         my ($fh, $ctx, $rev);
345
346         if ($_git_format) {
347                 ($fh, $ctx) = command_output_pipe('blame', @_, $path);
348                 while (my $line = <$fh>) {
349                         if ($line =~ /^\^?([[:xdigit:]]+)\s/) {
350                                 # Uncommitted edits show up as a rev ID of
351                                 # all zeros, which we can't look up with
352                                 # cmt_metadata
353                                 if ($1 !~ /^0+$/) {
354                                         (undef, $rev, undef) =
355                                                 ::cmt_metadata($1);
356                                         $rev = '0' if (!$rev);
357                                 } else {
358                                         $rev = '0';
359                                 }
360                                 $rev = sprintf('%-10s', $rev);
361                                 $line =~ s/^\^?[[:xdigit:]]+(\s)/$rev$1/;
362                         }
363                         print $line;
364                 }
365         } else {
366                 ($fh, $ctx) = command_output_pipe('blame', '-p', @_, 'HEAD',
367                                                   '--', $path);
368                 my ($sha1);
369                 my %authors;
370                 my @buffer;
371                 my %dsha; #distinct sha keys
372
373                 while (my $line = <$fh>) {
374                         push @buffer, $line;
375                         if ($line =~ /^([[:xdigit:]]{40})\s\d+\s\d+/) {
376                                 $dsha{$1} = 1;
377                         }
378                 }
379
380                 my $s2r = ::cmt_sha2rev_batch([keys %dsha]);
381
382                 foreach my $line (@buffer) {
383                         if ($line =~ /^([[:xdigit:]]{40})\s\d+\s\d+/) {
384                                 $rev = $s2r->{$1};
385                                 $rev = '0' if (!$rev)
386                         }
387                         elsif ($line =~ /^author (.*)/) {
388                                 $authors{$rev} = $1;
389                                 $authors{$rev} =~ s/\s/_/g;
390                         }
391                         elsif ($line =~ /^\t(.*)$/) {
392                                 printf("%6s %10s %s\n", $rev, $authors{$rev}, $1);
393                         }
394                 }
395         }
396         command_close_pipe($fh, $ctx);
397 }
398
399 1;