Merge branch 'jk/misc-fixes-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} ||= 'FRX';
120         $ENV{LV} ||= '-c';
121         exec $pager or fatal "Can't run pager: $! ($pager)";
122 }
123
124 sub format_svn_date {
125         my $t = shift || time;
126         require Git::SVN;
127         my $gmoff = get_tz_offset($t);
128         return strftime("%Y-%m-%d %H:%M:%S $gmoff (%a, %d %b %Y)", localtime($t));
129 }
130
131 sub parse_git_date {
132         my ($t, $tz) = @_;
133         # Date::Parse isn't in the standard Perl distro :(
134         if ($tz =~ s/^\+//) {
135                 $t += tz_to_s_offset($tz);
136         } elsif ($tz =~ s/^\-//) {
137                 $t -= tz_to_s_offset($tz);
138         }
139         return $t;
140 }
141
142 sub set_local_timezone {
143         if (defined $TZ) {
144                 $ENV{TZ} = $TZ;
145         } else {
146                 delete $ENV{TZ};
147         }
148 }
149
150 sub tz_to_s_offset {
151         my ($tz) = @_;
152         $tz =~ s/(\d\d)$//;
153         return ($1 * 60) + ($tz * 3600);
154 }
155
156 sub get_author_info {
157         my ($dest, $author, $t, $tz) = @_;
158         $author =~ s/(?:^\s*|\s*$)//g;
159         $dest->{a_raw} = $author;
160         my $au;
161         if ($::_authors) {
162                 $au = $rusers{$author} || undef;
163         }
164         if (!$au) {
165                 ($au) = ($author =~ /<([^>]+)\@[^>]+>$/);
166         }
167         $dest->{t} = $t;
168         $dest->{tz} = $tz;
169         $dest->{a} = $au;
170         $dest->{t_utc} = parse_git_date($t, $tz);
171 }
172
173 sub process_commit {
174         my ($c, $r_min, $r_max, $defer) = @_;
175         if (defined $r_min && defined $r_max) {
176                 if ($r_min == $c->{r} && $r_min == $r_max) {
177                         show_commit($c);
178                         return 0;
179                 }
180                 return 1 if $r_min == $r_max;
181                 if ($r_min < $r_max) {
182                         # we need to reverse the print order
183                         return 0 if (defined $limit && --$limit < 0);
184                         push @$defer, $c;
185                         return 1;
186                 }
187                 if ($r_min != $r_max) {
188                         return 1 if ($r_min < $c->{r});
189                         return 1 if ($r_max > $c->{r});
190                 }
191         }
192         return 0 if (defined $limit && --$limit < 0);
193         show_commit($c);
194         return 1;
195 }
196
197 my $l_fmt;
198 sub show_commit {
199         my $c = shift;
200         if ($oneline) {
201                 my $x = "\n";
202                 if (my $l = $c->{l}) {
203                         while ($l->[0] =~ /^\s*$/) { shift @$l }
204                         $x = $l->[0];
205                 }
206                 $l_fmt ||= 'A' . length($c->{r});
207                 print 'r',pack($l_fmt, $c->{r}),' | ';
208                 print "$c->{c} | " if $show_commit;
209                 print $x;
210         } else {
211                 show_commit_normal($c);
212         }
213 }
214
215 sub show_commit_changed_paths {
216         my ($c) = @_;
217         return unless $c->{changed};
218         print "Changed paths:\n", @{$c->{changed}};
219 }
220
221 sub show_commit_normal {
222         my ($c) = @_;
223         print commit_log_separator, "r$c->{r} | ";
224         print "$c->{c} | " if $show_commit;
225         print "$c->{a} | ", format_svn_date($c->{t_utc}), ' | ';
226         my $nr_line = 0;
227
228         if (my $l = $c->{l}) {
229                 while ($l->[$#$l] eq "\n" && $#$l > 0
230                                           && $l->[($#$l - 1)] eq "\n") {
231                         pop @$l;
232                 }
233                 $nr_line = scalar @$l;
234                 if (!$nr_line) {
235                         print "1 line\n\n\n";
236                 } else {
237                         if ($nr_line == 1) {
238                                 $nr_line = '1 line';
239                         } else {
240                                 $nr_line .= ' lines';
241                         }
242                         print $nr_line, "\n";
243                         show_commit_changed_paths($c);
244                         print "\n";
245                         print $_ foreach @$l;
246                 }
247         } else {
248                 print "1 line\n";
249                 show_commit_changed_paths($c);
250                 print "\n";
251
252         }
253         foreach my $x (qw/raw stat diff/) {
254                 if ($c->{$x}) {
255                         print "\n";
256                         print $_ foreach @{$c->{$x}}
257                 }
258         }
259 }
260
261 sub cmd_show_log {
262         my (@args) = @_;
263         my ($r_min, $r_max);
264         my $r_last = -1; # prevent dupes
265         set_local_timezone();
266         if (defined $::_revision) {
267                 if ($::_revision =~ /^(\d+):(\d+)$/) {
268                         ($r_min, $r_max) = ($1, $2);
269                 } elsif ($::_revision =~ /^\d+$/) {
270                         $r_min = $r_max = $::_revision;
271                 } else {
272                         fatal "-r$::_revision is not supported, use ",
273                                 "standard 'git log' arguments instead";
274                 }
275         }
276
277         config_pager();
278         @args = git_svn_log_cmd($r_min, $r_max, @args);
279         if (!@args) {
280                 print commit_log_separator unless $incremental || $oneline;
281                 return;
282         }
283         my $log = command_output_pipe(@args);
284         run_pager();
285         my (@k, $c, $d, $stat);
286         my $esc_color = qr/(?:\033\[(?:(?:\d+;)*\d*)?m)*/;
287         while (<$log>) {
288                 if (/^${esc_color}commit (?:- )?($::sha1_short)/o) {
289                         my $cmt = $1;
290                         if ($c && cmt_showable($c) && $c->{r} != $r_last) {
291                                 $r_last = $c->{r};
292                                 process_commit($c, $r_min, $r_max, \@k) or
293                                                                 goto out;
294                         }
295                         $d = undef;
296                         $c = { c => $cmt };
297                 } elsif (/^${esc_color}author (.+) (\d+) ([\-\+]?\d+)$/o) {
298                         get_author_info($c, $1, $2, $3);
299                 } elsif (/^${esc_color}(?:tree|parent|committer) /o) {
300                         # ignore
301                 } elsif (/^${esc_color}:\d{6} \d{6} $::sha1_short/o) {
302                         push @{$c->{raw}}, $_;
303                 } elsif (/^${esc_color}[ACRMDT]\t/) {
304                         # we could add $SVN->{svn_path} here, but that requires
305                         # remote access at the moment (repo_path_split)...
306                         s#^(${esc_color})([ACRMDT])\t#$1   $2 #o;
307                         push @{$c->{changed}}, $_;
308                 } elsif (/^${esc_color}diff /o) {
309                         $d = 1;
310                         push @{$c->{diff}}, $_;
311                 } elsif ($d) {
312                         push @{$c->{diff}}, $_;
313                 } elsif (/^\ .+\ \|\s*\d+\ $esc_color[\+\-]*
314                           $esc_color*[\+\-]*$esc_color$/x) {
315                         $stat = 1;
316                         push @{$c->{stat}}, $_;
317                 } elsif ($stat && /^ \d+ files changed, \d+ insertions/) {
318                         push @{$c->{stat}}, $_;
319                         $stat = undef;
320                 } elsif (/^${esc_color}    (git-svn-id:.+)$/o) {
321                         ($c->{url}, $c->{r}, undef) = ::extract_metadata($1);
322                 } elsif (s/^${esc_color}    //o) {
323                         push @{$c->{l}}, $_;
324                 }
325         }
326         if ($c && defined $c->{r} && $c->{r} != $r_last) {
327                 $r_last = $c->{r};
328                 process_commit($c, $r_min, $r_max, \@k);
329         }
330         if (@k) {
331                 ($r_min, $r_max) = ($r_max, $r_min);
332                 process_commit($_, $r_min, $r_max) foreach reverse @k;
333         }
334 out:
335         close $log;
336         print commit_log_separator unless $incremental || $oneline;
337 }
338
339 sub cmd_blame {
340         my $path = pop;
341
342         config_pager();
343         run_pager();
344
345         my ($fh, $ctx, $rev);
346
347         if ($_git_format) {
348                 ($fh, $ctx) = command_output_pipe('blame', @_, $path);
349                 while (my $line = <$fh>) {
350                         if ($line =~ /^\^?([[:xdigit:]]+)\s/) {
351                                 # Uncommitted edits show up as a rev ID of
352                                 # all zeros, which we can't look up with
353                                 # cmt_metadata
354                                 if ($1 !~ /^0+$/) {
355                                         (undef, $rev, undef) =
356                                                 ::cmt_metadata($1);
357                                         $rev = '0' if (!$rev);
358                                 } else {
359                                         $rev = '0';
360                                 }
361                                 $rev = sprintf('%-10s', $rev);
362                                 $line =~ s/^\^?[[:xdigit:]]+(\s)/$rev$1/;
363                         }
364                         print $line;
365                 }
366         } else {
367                 ($fh, $ctx) = command_output_pipe('blame', '-p', @_, 'HEAD',
368                                                   '--', $path);
369                 my ($sha1);
370                 my %authors;
371                 my @buffer;
372                 my %dsha; #distinct sha keys
373
374                 while (my $line = <$fh>) {
375                         push @buffer, $line;
376                         if ($line =~ /^([[:xdigit:]]{40})\s\d+\s\d+/) {
377                                 $dsha{$1} = 1;
378                         }
379                 }
380
381                 my $s2r = ::cmt_sha2rev_batch([keys %dsha]);
382
383                 foreach my $line (@buffer) {
384                         if ($line =~ /^([[:xdigit:]]{40})\s\d+\s\d+/) {
385                                 $rev = $s2r->{$1};
386                                 $rev = '0' if (!$rev)
387                         }
388                         elsif ($line =~ /^author (.*)/) {
389                                 $authors{$rev} = $1;
390                                 $authors{$rev} =~ s/\s/_/g;
391                         }
392                         elsif ($line =~ /^\t(.*)$/) {
393                                 printf("%6s %10s %s\n", $rev, $authors{$rev}, $1);
394                         }
395                 }
396         }
397         command_close_pipe($fh, $ctx);
398 }
399
400 1;