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 = '2012-01-24 15:58 (wk)'; # 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-2012 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 <https://www.gnu.org/licenses/>.
26
27# Written by Jim Meyering
28# Custom bugs bred by Werner Koch
29
30use strict;
31use warnings;
32use Getopt::Long;
33use POSIX qw(strftime);
34
35(my $ME = $0) =~ s|.*/||;
36
37# use File::Coda; # http://meyering.net/code/Coda/
38END {
39  defined fileno STDOUT or return;
40  close STDOUT and return;
41  warn "$ME: failed to close standard output: $!\n";
42  $? ||= 1;
43}
44
45sub usage ($)
46{
47  my ($exit_code) = @_;
48  my $STREAM = ($exit_code == 0 ? *STDOUT : *STDERR);
49  if ($exit_code != 0)
50    {
51      print $STREAM "Try `$ME --help' for more information.\n";
52    }
53  else
54    {
55      print $STREAM <<EOF;
56Usage: $ME [OPTIONS] [ARGS]
57
58Convert git log output to ChangeLog format.  If present, any ARGS
59are passed to "git log".  To avoid ARGS being parsed as options to
60$ME, they may be preceded by '--'.
61
62OPTIONS:
63
64   --amend=FILE FILE maps from an SHA1 to perl code (i.e., s/old/new/) that
65                  makes a change to SHA1's commit log text or metadata.
66   --append-dot append a dot to the first line of each commit message if
67                  there is no other punctuation or blank at the end.
68   --tear-off   tear off all commit log lines after a '--' line and
69                skip log entry with the first body line being '--'.
70   --since=DATE convert only the logs since DATE;
71                  the default is to convert all log entries.
72   --format=FMT set format string for commit subject and body;
73                  see 'man git-log' for the list of format metacharacters;
74                  the default is '%s%n%b%n'
75
76   --help       display this help and exit
77   --version    output version information and exit
78
79EXAMPLE:
80
81  $ME --since=2008-01-01 > ChangeLog
82  $ME -- -n 5 foo > last-5-commits-to-branch-foo
83
84In a FILE specified via --amend, comment lines (starting with "#") are ignored.
85FILE must consist of <SHA,CODE+> pairs where SHA is a 40-byte SHA1 (alone on
86a line) referring to a commit in the current project, and CODE refers to one
87or more consecutive lines of Perl code.  Pairs must be separated by one or
88more blank line.
89
90Here is sample input for use with --amend=FILE, from coreutils:
91
923a169f4c5d9159283548178668d2fae6fced3030
93# fix typo in title:
94s/all tile types/all file types/
95
961379ed974f1fa39b12e2ffab18b3f7a607082202
97# Due to a bug in vc-dwim, I mis-attributed a patch by Paul to myself.
98# Change the author to be Paul.  Note the escaped "@":
99s,Jim .*>,Paul Eggert <eggert\@cs.ucla.edu>,
100
101EOF
102    }
103  exit $exit_code;
104}
105
106# If the string $S is a well-behaved file name, simply return it.
107# If it contains white space, quotes, etc., quote it, and return the new string.
108sub shell_quote($)
109{
110  my ($s) = @_;
111  if ($s =~ m![^\w+/.,-]!)
112    {
113      # Convert each single quote to '\''
114      $s =~ s/\'/\'\\\'\'/g;
115      # Then single quote the string.
116      $s = "'$s'";
117    }
118  return $s;
119}
120
121sub quoted_cmd(@)
122{
123  return join (' ', map {shell_quote $_} @_);
124}
125
126# Parse file F.
127# Comment lines (starting with "#") are ignored.
128# F must consist of <SHA,CODE+> pairs where SHA is a 40-byte SHA1
129# (alone on a line) referring to a commit in the current project, and
130# CODE refers to one or more consecutive lines of Perl code.
131# Pairs must be separated by one or more blank line.
132sub parse_amend_file($)
133{
134  my ($f) = @_;
135
136  open F, '<', $f
137    or die "$ME: $f: failed to open for reading: $!\n";
138
139  my $fail;
140  my $h = {};
141  my $in_code = 0;
142  my $sha;
143  while (defined (my $line = <F>))
144    {
145      $line =~ /^\#/
146        and next;
147      chomp $line;
148      $line eq ''
149        and $in_code = 0, next;
150
151      if (!$in_code)
152        {
153          $line =~ /^([0-9a-fA-F]{40})$/
154            or (warn "$ME: $f:$.: invalid line; expected an SHA1\n"),
155              $fail = 1, next;
156          $sha = lc $1;
157          $in_code = 1;
158          exists $h->{$sha}
159            and (warn "$ME: $f:$.: duplicate SHA1\n"),
160              $fail = 1, next;
161        }
162      else
163        {
164          $h->{$sha} ||= '';
165          $h->{$sha} .= "$line\n";
166        }
167    }
168  close F;
169
170  $fail
171    and exit 1;
172
173  return $h;
174}
175
176{
177  my $since_date;
178  my $format_string = '%s%n%b%n';
179  my $amend_file;
180  my $append_dot = 0;
181  my $tear_off = 0;
182  GetOptions
183    (
184     help => sub { usage 0 },
185     version => sub { print "$ME version $VERSION\n"; exit },
186     'since=s' => \$since_date,
187     'format=s' => \$format_string,
188     'amend=s' => \$amend_file,
189     'append-dot' => \$append_dot,
190     'tear-off' => \$tear_off,
191    ) or usage 1;
192
193
194  defined $since_date
195    and unshift @ARGV, "--since=$since_date";
196
197  # This is a hash that maps an SHA1 to perl code (i.e., s/old/new/)
198  # that makes a correction in the log or attribution of that commit.
199  my $amend_code = defined $amend_file ? parse_amend_file $amend_file : {};
200
201  my @cmd = (qw (git log --log-size),
202             '--pretty=format:%H:%ct  %an  <%ae>%n%n'.$format_string, @ARGV);
203  open PIPE, '-|', @cmd
204    or die ("$ME: failed to run `". quoted_cmd (@cmd) ."': $!\n"
205            . "(Is your Git too old?  Version 1.5.1 or later is required.)\n");
206
207  my $prev_date_line = '';
208  my @prev_coauthors = ();
209
210  while (1)
211    {
212      defined (my $in = <PIPE>)
213        or last;
214      $in =~ /^log size (\d+)$/
215        or die "$ME:$.: Invalid line (expected log size):\n$in";
216      my $log_nbytes = $1;
217
218      my $log;
219      my $n_read = read PIPE, $log, $log_nbytes;
220      $n_read == $log_nbytes
221        or die "$ME:$.: unexpected EOF\n";
222
223      # Skip log entries with the default merge commit message.
224      $log =~ /^.*\n\nMerge branch '.*\n\s*/
225        and goto SKIPCOMMIT;
226
227      # Skip log entries if the body starts with a tear off marker.
228      if ($tear_off)
229        {
230          $log =~ /^.*\n\n.*\n--\s*/
231            and goto SKIPCOMMIT;
232        }
233
234      # Extract leading hash.
235      my ($sha, $rest) = split ':', $log, 2;
236      defined $sha
237        or die "$ME:$.: malformed log entry\n";
238      $sha =~ /^[0-9a-fA-F]{40}$/
239        or die "$ME:$.: invalid SHA1: $sha\n";
240
241      # If this commit's log requires any transformation, do it now.
242      my $code = $amend_code->{$sha};
243      if (defined $code)
244        {
245          eval 'use Safe';
246          my $s = new Safe;
247          # Put the unpreprocessed entry into "$_".
248          $_ = $rest;
249
250          # Let $code operate on it, safely.
251          my $r = $s->reval("$code")
252            or die "$ME:$.:$sha: failed to eval \"$code\":\n$@\n";
253
254          # Note that we've used this entry.
255          delete $amend_code->{$sha};
256
257          # Update $rest upon success.
258          $rest = $_;
259        }
260
261      my @line = split "\n", $rest;
262      my $author_line = shift @line;
263      defined $author_line
264        or die "$ME:$.: unexpected EOF\n";
265      $author_line =~ /^(\d+)  (.*>)$/
266        or die "$ME:$.: Invalid line "
267          . "(expected date/author/email):\n$author_line\n";
268
269      my $date_line = sprintf "%s  $2\n", strftime ("%F", localtime ($1));
270
271      # Format 'Co-authored-by: A U Thor <email@example.com>' lines in
272      # standard multi-author ChangeLog format.
273      my @coauthors = grep /^Co-authored-by:.*$/, @line;
274      for (@coauthors)
275        {
276          s/^Co-authored-by:\s*/\t    /;
277          s/\s*</  </;
278
279          /<.*?@.*\..*>/
280            or warn "$ME: warning: missing email address for "
281              . substr ($_, 5) . "\n";
282        }
283
284      # If this header would be the same as the previous date/name/email/
285      # coauthors header, then arrange not to print it.
286      if ($date_line ne $prev_date_line or "@coauthors" ne "@prev_coauthors")
287        {
288          $prev_date_line eq ''
289            or print "\n";
290          print $date_line;
291          @coauthors
292            and print join ("\n", @coauthors), "\n";
293        }
294      $prev_date_line = $date_line;
295      @prev_coauthors = @coauthors;
296
297      # Omit "Co-authored-by..." and "Signed-off-by..." lines.
298      @line = grep !/^Signed-off-by: .*>$/, @line;
299      @line = grep !/^Co-authored-by: /, @line;
300
301      # Remove everything after a line with 2 dashes at the beginning.
302      if ($tear_off)
303        {
304           my @tmpline;
305           foreach (@line)
306             {
307	       last if /^--\s*$/;
308               push @tmpline,$_;
309             }
310           @line = @tmpline;
311        }
312
313      # Remove leading and trailing blank lines.
314      if (@line)
315        {
316          while ($line[0] =~ /^\s*$/) { shift @line; }
317          while ($line[$#line] =~ /^\s*$/) { pop @line; }
318        }
319
320      # If there were any lines
321      if (@line == 0)
322        {
323          warn "$ME: warning: empty commit message:\n  $date_line\n";
324        }
325      else
326        {
327          if ($append_dot)
328            {
329              # If the first line of the message has enough room, then
330              if (length $line[0] < 72)
331                {
332                  # append a dot if there is no other punctuation or blank
333                  # at the end.
334                  $line[0] =~ /[[:punct:]\s]$/
335                    or $line[0] .= '.';
336                }
337            }
338
339          # Prefix each non-empty line with a TAB.
340          @line = map { length $_ ? "\t$_" : '' } @line;
341
342          print "\n", join ("\n", @line), "\n";
343        }
344
345    SKIPCOMMIT:
346      defined ($in = <PIPE>)
347        or last;
348      $in ne "\n"
349        and die "$ME:$.: unexpected line:\n$in";
350    }
351
352  close PIPE
353    or die "$ME: error closing pipe from " . quoted_cmd (@cmd) . "\n";
354  # FIXME-someday: include $PROCESS_STATUS in the diagnostic
355
356  # Complain about any unused entry in the --amend=F specified file.
357  my $fail = 0;
358  foreach my $sha (keys %$amend_code)
359    {
360      warn "$ME:$amend_file: unused entry: $sha\n";
361      $fail = 1;
362    }
363
364  exit $fail;
365}
366
367# Local Variables:
368# mode: perl
369# indent-tabs-mode: nil
370# eval: (add-hook 'write-file-hooks 'time-stamp)
371# time-stamp-start: "my $VERSION = '"
372# time-stamp-format: "%:y-%02m-%02d %02H:%02M (wk)"
373# time-stamp-time-zone: "UTC"
374# time-stamp-end: "'; # UTC"
375# End:
376