• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1eval '(exit $?0)' && eval 'exec perl -wS "$0" ${1+"$@"}'
2  & eval 'exec perl -wS "$0" $argv:q'
3    if 0;
4# Convert git log output to ChangeLog format.
5
6my $VERSION = '2014-11-20 17:25'; # UTC
7# The definition above must lie within the first 8 lines in order
8# for the Emacs time-stamp write hook (at end) to update it.
9# If you change this file with Emacs, please let the write hook
10# do its job.  Otherwise, update this string manually.
11
12# Copyright (C) 2008-2014 Free Software Foundation, Inc.
13
14# This program is free software: you can redistribute it and/or modify
15# it under the terms of the GNU General Public License as published by
16# the Free Software Foundation, either version 3 of the License, or
17# (at your option) any later version.
18
19# This program is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22# GNU General Public License for more details.
23
24# You should have received a copy of the GNU General Public License
25# along with this program.  If not, see <http://www.gnu.org/licenses/>.
26
27# Written by Jim Meyering
28
29use strict;
30use warnings;
31use Getopt::Long;
32use POSIX qw(strftime);
33
34(my $ME = $0) =~ s|.*/||;
35
36# use File::Coda; # http://meyering.net/code/Coda/
37END {
38  defined fileno STDOUT or return;
39  close STDOUT and return;
40  warn "$ME: failed to close standard output: $!\n";
41  $? ||= 1;
42}
43
44sub usage ($)
45{
46  my ($exit_code) = @_;
47  my $STREAM = ($exit_code == 0 ? *STDOUT : *STDERR);
48  if ($exit_code != 0)
49    {
50      print $STREAM "Try '$ME --help' for more information.\n";
51    }
52  else
53    {
54      print $STREAM <<EOF;
55Usage: $ME [OPTIONS] [ARGS]
56
57Convert git log output to ChangeLog format.  If present, any ARGS
58are passed to "git log".  To avoid ARGS being parsed as options to
59$ME, they may be preceded by '--'.
60
61OPTIONS:
62
63   --amend=FILE FILE maps from an SHA1 to perl code (i.e., s/old/new/) that
64                  makes a change to SHA1's commit log text or metadata.
65   --append-dot append a dot to the first line of each commit message if
66                  there is no other punctuation or blank at the end.
67   --no-cluster never cluster commit messages under the same date/author
68                  header; the default is to cluster adjacent commit messages
69                  if their headers are the same and neither commit message
70                  contains multiple paragraphs.
71   --srcdir=DIR the root of the source tree, from which the .git/
72                  directory can be derived.
73   --since=DATE convert only the logs since DATE;
74                  the default is to convert all log entries.
75   --until=DATE convert only the logs older than DATE.
76   --format=FMT set format string for commit subject and body;
77                  see 'man git-log' for the list of format metacharacters;
78                  the default is '%s%n%b%n'
79   --strip-tab  remove one additional leading TAB from commit message lines.
80   --strip-cherry-pick  remove data inserted by "git cherry-pick";
81                  this includes the "cherry picked from commit ..." line,
82                  and the possible final "Conflicts:" paragraph.
83   --help       display this help and exit
84   --version    output version information and exit
85
86EXAMPLE:
87
88  $ME --since=2008-01-01 > ChangeLog
89  $ME -- -n 5 foo > last-5-commits-to-branch-foo
90
91SPECIAL SYNTAX:
92
93The following types of strings are interpreted specially when they appear
94at the beginning of a log message line.  They are not copied to the output.
95
96  Copyright-paperwork-exempt: Yes
97    Append the "(tiny change)" notation to the usual "date name email"
98    ChangeLog header to mark a change that does not require a copyright
99    assignment.
100  Co-authored-by: Joe User <user\@example.com>
101    List the specified name and email address on a second
102    ChangeLog header, denoting a co-author.
103  Signed-off-by: Joe User <user\@example.com>
104    These lines are simply elided.
105
106In a FILE specified via --amend, comment lines (starting with "#") are ignored.
107FILE must consist of <SHA,CODE+> pairs where SHA is a 40-byte SHA1 (alone on
108a line) referring to a commit in the current project, and CODE refers to one
109or more consecutive lines of Perl code.  Pairs must be separated by one or
110more blank line.
111
112Here is sample input for use with --amend=FILE, from coreutils:
113
1143a169f4c5d9159283548178668d2fae6fced3030
115# fix typo in title:
116s/all tile types/all file types/
117
1181379ed974f1fa39b12e2ffab18b3f7a607082202
119# Due to a bug in vc-dwim, I mis-attributed a patch by Paul to myself.
120# Change the author to be Paul.  Note the escaped "@":
121s,Jim .*>,Paul Eggert <eggert\\\@cs.ucla.edu>,
122
123EOF
124    }
125  exit $exit_code;
126}
127
128# If the string $S is a well-behaved file name, simply return it.
129# If it contains white space, quotes, etc., quote it, and return the new string.
130sub shell_quote($)
131{
132  my ($s) = @_;
133  if ($s =~ m![^\w+/.,-]!)
134    {
135      # Convert each single quote to '\''
136      $s =~ s/\'/\'\\\'\'/g;
137      # Then single quote the string.
138      $s = "'$s'";
139    }
140  return $s;
141}
142
143sub quoted_cmd(@)
144{
145  return join (' ', map {shell_quote $_} @_);
146}
147
148# Parse file F.
149# Comment lines (starting with "#") are ignored.
150# F must consist of <SHA,CODE+> pairs where SHA is a 40-byte SHA1
151# (alone on a line) referring to a commit in the current project, and
152# CODE refers to one or more consecutive lines of Perl code.
153# Pairs must be separated by one or more blank line.
154sub parse_amend_file($)
155{
156  my ($f) = @_;
157
158  open F, '<', $f
159    or die "$ME: $f: failed to open for reading: $!\n";
160
161  my $fail;
162  my $h = {};
163  my $in_code = 0;
164  my $sha;
165  while (defined (my $line = <F>))
166    {
167      $line =~ /^\#/
168        and next;
169      chomp $line;
170      $line eq ''
171        and $in_code = 0, next;
172
173      if (!$in_code)
174        {
175          $line =~ /^([0-9a-fA-F]{40})$/
176            or (warn "$ME: $f:$.: invalid line; expected an SHA1\n"),
177              $fail = 1, next;
178          $sha = lc $1;
179          $in_code = 1;
180          exists $h->{$sha}
181            and (warn "$ME: $f:$.: duplicate SHA1\n"),
182              $fail = 1, next;
183        }
184      else
185        {
186          $h->{$sha} ||= '';
187          $h->{$sha} .= "$line\n";
188        }
189    }
190  close F;
191
192  $fail
193    and exit 1;
194
195  return $h;
196}
197
198# git_dir_option $SRCDIR
199#
200# From $SRCDIR, the --git-dir option to pass to git (none if $SRCDIR
201# is undef).  Return as a list (0 or 1 element).
202sub git_dir_option($)
203{
204  my ($srcdir) = @_;
205  my @res = ();
206  if (defined $srcdir)
207    {
208      my $qdir = shell_quote $srcdir;
209      my $cmd = "cd $qdir && git rev-parse --show-toplevel";
210      my $qcmd = shell_quote $cmd;
211      my $git_dir = qx($cmd);
212      defined $git_dir
213        or die "$ME: cannot run $qcmd: $!\n";
214      $? == 0
215        or die "$ME: $qcmd had unexpected exit code or signal ($?)\n";
216      chomp $git_dir;
217      push @res, "--git-dir=$git_dir/.git";
218    }
219  @res;
220}
221
222{
223  my $since_date;
224  my $until_date;
225  my $format_string = '%s%n%b%n';
226  my $amend_file;
227  my $append_dot = 0;
228  my $cluster = 1;
229  my $strip_tab = 0;
230  my $strip_cherry_pick = 0;
231  my $srcdir;
232  GetOptions
233    (
234     help => sub { usage 0 },
235     version => sub { print "$ME version $VERSION\n"; exit },
236     'since=s' => \$since_date,
237     'until=s' => \$until_date,
238     'format=s' => \$format_string,
239     'amend=s' => \$amend_file,
240     'append-dot' => \$append_dot,
241     'cluster!' => \$cluster,
242     'strip-tab' => \$strip_tab,
243     'strip-cherry-pick' => \$strip_cherry_pick,
244     'srcdir=s' => \$srcdir,
245    ) or usage 1;
246
247  defined $since_date
248    and unshift @ARGV, "--since=$since_date";
249  defined $until_date
250    and unshift @ARGV, "--until=$until_date";
251
252  # This is a hash that maps an SHA1 to perl code (i.e., s/old/new/)
253  # that makes a correction in the log or attribution of that commit.
254  my $amend_code = defined $amend_file ? parse_amend_file $amend_file : {};
255
256  my @cmd = ('git',
257             git_dir_option $srcdir,
258             qw(log --log-size),
259             '--pretty=format:%H:%ct  %an  <%ae>%n%n'.$format_string, @ARGV);
260  open PIPE, '-|', @cmd
261    or die ("$ME: failed to run '". quoted_cmd (@cmd) ."': $!\n"
262            . "(Is your Git too old?  Version 1.5.1 or later is required.)\n");
263
264  my $prev_multi_paragraph;
265  my $prev_date_line = '';
266  my @prev_coauthors = ();
267  while (1)
268    {
269      defined (my $in = <PIPE>)
270        or last;
271      $in =~ /^log size (\d+)$/
272        or die "$ME:$.: Invalid line (expected log size):\n$in";
273      my $log_nbytes = $1;
274
275      my $log;
276      my $n_read = read PIPE, $log, $log_nbytes;
277      $n_read == $log_nbytes
278        or die "$ME:$.: unexpected EOF\n";
279
280      # Extract leading hash.
281      my ($sha, $rest) = split ':', $log, 2;
282      defined $sha
283        or die "$ME:$.: malformed log entry\n";
284      $sha =~ /^[0-9a-fA-F]{40}$/
285        or die "$ME:$.: invalid SHA1: $sha\n";
286
287      # If this commit's log requires any transformation, do it now.
288      my $code = $amend_code->{$sha};
289      if (defined $code)
290        {
291          eval 'use Safe';
292          my $s = new Safe;
293          # Put the unpreprocessed entry into "$_".
294          $_ = $rest;
295
296          # Let $code operate on it, safely.
297          my $r = $s->reval("$code")
298            or die "$ME:$.:$sha: failed to eval \"$code\":\n$@\n";
299
300          # Note that we've used this entry.
301          delete $amend_code->{$sha};
302
303          # Update $rest upon success.
304          $rest = $_;
305        }
306
307      # Remove lines inserted by "git cherry-pick".
308      if ($strip_cherry_pick)
309        {
310          $rest =~ s/^\s*Conflicts:\n.*//sm;
311          $rest =~ s/^\s*\(cherry picked from commit [\da-f]+\)\n//m;
312        }
313
314      my @line = split "\n", $rest;
315      my $author_line = shift @line;
316      defined $author_line
317        or die "$ME:$.: unexpected EOF\n";
318      $author_line =~ /^(\d+)  (.*>)$/
319        or die "$ME:$.: Invalid line "
320          . "(expected date/author/email):\n$author_line\n";
321
322      # Format 'Copyright-paperwork-exempt: Yes' as a standard ChangeLog
323      # `(tiny change)' annotation.
324      my $tiny = (grep (/^Copyright-paperwork-exempt:\s+[Yy]es$/, @line)
325                  ? '  (tiny change)' : '');
326
327      my $date_line = sprintf "%s  %s$tiny\n",
328        strftime ("%F", localtime ($1)), $2;
329
330      my @coauthors = grep /^Co-authored-by:.*$/, @line;
331      # Omit meta-data lines we've already interpreted.
332      @line = grep !/^(?:Signed-off-by:[ ].*>$
333                       |Co-authored-by:[ ]
334                       |Copyright-paperwork-exempt:[ ]
335                       )/x, @line;
336
337      # Remove leading and trailing blank lines.
338      if (@line)
339        {
340          while ($line[0] =~ /^\s*$/) { shift @line; }
341          while ($line[$#line] =~ /^\s*$/) { pop @line; }
342        }
343
344      # Record whether there are two or more paragraphs.
345      my $multi_paragraph = grep /^\s*$/, @line;
346
347      # Format 'Co-authored-by: A U Thor <email@example.com>' lines in
348      # standard multi-author ChangeLog format.
349      for (@coauthors)
350        {
351          s/^Co-authored-by:\s*/\t    /;
352          s/\s*</  </;
353
354          /<.*?@.*\..*>/
355            or warn "$ME: warning: missing email address for "
356              . substr ($_, 5) . "\n";
357        }
358
359      # If clustering of commit messages has been disabled, if this header
360      # would be different from the previous date/name/email/coauthors header,
361      # or if this or the previous entry consists of two or more paragraphs,
362      # then print the header.
363      if ( ! $cluster
364          || $date_line ne $prev_date_line
365          || "@coauthors" ne "@prev_coauthors"
366          || $multi_paragraph
367          || $prev_multi_paragraph)
368        {
369          $prev_date_line eq ''
370            or print "\n";
371          print $date_line;
372          @coauthors
373            and print join ("\n", @coauthors), "\n";
374        }
375      $prev_date_line = $date_line;
376      @prev_coauthors = @coauthors;
377      $prev_multi_paragraph = $multi_paragraph;
378
379      # If there were any lines
380      if (@line == 0)
381        {
382          warn "$ME: warning: empty commit message:\n  $date_line\n";
383        }
384      else
385        {
386          if ($append_dot)
387            {
388              # If the first line of the message has enough room, then
389              if (length $line[0] < 72)
390                {
391                  # append a dot if there is no other punctuation or blank
392                  # at the end.
393                  $line[0] =~ /[[:punct:]\s]$/
394                    or $line[0] .= '.';
395                }
396            }
397
398          # Remove one additional leading TAB from each line.
399          $strip_tab
400            and map { s/^\t// } @line;
401
402          # Prefix each non-empty line with a TAB.
403          @line = map { length $_ ? "\t$_" : '' } @line;
404
405          print "\n", join ("\n", @line), "\n";
406        }
407
408      defined ($in = <PIPE>)
409        or last;
410      $in ne "\n"
411        and die "$ME:$.: unexpected line:\n$in";
412    }
413
414  close PIPE
415    or die "$ME: error closing pipe from " . quoted_cmd (@cmd) . "\n";
416  # FIXME-someday: include $PROCESS_STATUS in the diagnostic
417
418  # Complain about any unused entry in the --amend=F specified file.
419  my $fail = 0;
420  foreach my $sha (keys %$amend_code)
421    {
422      warn "$ME:$amend_file: unused entry: $sha\n";
423      $fail = 1;
424    }
425
426  exit $fail;
427}
428
429# Local Variables:
430# mode: perl
431# indent-tabs-mode: nil
432# eval: (add-hook 'write-file-hooks 'time-stamp)
433# time-stamp-start: "my $VERSION = '"
434# time-stamp-format: "%:y-%02m-%02d %02H:%02M"
435# time-stamp-time-zone: "UTC"
436# time-stamp-end: "'; # UTC"
437# End:
438