1#!/usr/bin/perl
2# Convert git log output to ChangeLog format.
3
4my $VERSION = '2008-12-21 12:07'; # UTC
5# The definition above must lie within the first 8 lines in order
6# for the Emacs time-stamp write hook (at end) to update it.
7# If you change this file with Emacs, please let the write hook
8# do its job.  Otherwise, update this string manually.
9
10# Copyright (C) 2008 Free Software Foundation, Inc.
11
12# This program is free software: you can redistribute it and/or modify
13# it under the terms of the GNU General Public License as published by
14# the Free Software Foundation, either version 3 of the License, or
15# (at your option) any later version.
16
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20# GNU General Public License for more details.
21
22# You should have received a copy of the GNU General Public License
23# along with this program.  If not, see <http://www.gnu.org/licenses/>.
24
25# Written by Jim Meyering
26
27use strict;
28use warnings;
29use Getopt::Long;
30use POSIX qw(strftime);
31
32(my $ME = $0) =~ s|.*/||;
33
34# use File::Coda; # http://meyering.net/code/Coda/
35END {
36  defined fileno STDOUT or return;
37  close STDOUT and return;
38  warn "$ME: failed to close standard output: $!\n";
39  $? ||= 1;
40}
41
42sub usage ($)
43{
44  my ($exit_code) = @_;
45  my $STREAM = ($exit_code == 0 ? *STDOUT : *STDERR);
46  if ($exit_code != 0)
47    {
48      print $STREAM "Try `$ME --help' for more information.\n";
49    }
50  else
51    {
52      print $STREAM <<EOF;
53Usage: $ME [OPTIONS] [ARGS]
54
55Convert git log output to ChangeLog format.  If present, any ARGS
56are passed to "git log".  To avoid ARGS being parsed as options to
57$ME, they may be preceded by '--'.
58
59OPTIONS:
60
61   --since=DATE convert only the logs since DATE;
62                  the default is to convert all log entries.
63
64   --help       display this help and exit
65   --version    output version information and exit
66
67EXAMPLE:
68
69  $ME --since=2008-01-01 > ChangeLog
70  $ME -- -n 5 foo > last-5-commits-to-branch-foo
71
72EOF
73    }
74  exit $exit_code;
75}
76
77# If the string $S is a well-behaved file name, simply return it.
78# If it contains white space, quotes, etc., quote it, and return the new string.
79sub shell_quote($)
80{
81  my ($s) = @_;
82  if ($s =~ m![^\w+/.,-]!)
83    {
84      # Convert each single quote to '\''
85      $s =~ s/\'/\'\\\'\'/g;
86      # Then single quote the string.
87      $s = "'$s'";
88    }
89  return $s;
90}
91
92sub quoted_cmd(@)
93{
94  return join (' ', map {shell_quote $_} @_);
95}
96
97{
98  my $since_date = '1970-01-01 UTC';
99  GetOptions
100    (
101     help => sub { usage 0 },
102     version => sub { print "$ME version $VERSION\n"; exit },
103     'since=s' => \$since_date,
104    ) or usage 1;
105
106  my @cmd = (qw (git log --log-size), "--since=$since_date",
107             '--pretty=format:%ct  %an  <%ae>%n%n%s%n%b%n', @ARGV);
108  open PIPE, '-|', @cmd
109    or die ("$ME: failed to run `". quoted_cmd (@cmd) ."': $!\n"
110            . "(Is your Git too old?  Version 1.5.1 or later is required.)\n");
111
112  my $prev_date_line = '';
113  while (1)
114    {
115      defined (my $in = <PIPE>)
116        or last;
117      $in =~ /^log size (\d+)$/
118        or die "$ME:$.: Invalid line (expected log size):\n$in";
119      my $log_nbytes = $1;
120
121      my $log;
122      my $n_read = read PIPE, $log, $log_nbytes;
123      $n_read == $log_nbytes
124        or die "$ME:$.: unexpected EOF\n";
125
126      my @line = split "\n", $log;
127      my $author_line = shift @line;
128      defined $author_line
129        or die "$ME:$.: unexpected EOF\n";
130      $author_line =~ /^(\d+)  (.*>)$/
131        or die "$ME:$.: Invalid line "
132          . "(expected date/author/email):\n$author_line\n";
133
134      my $date_line = sprintf "%s  $2\n", strftime ("%F", localtime ($1));
135      # If this line would be the same as the previous date/name/email
136      # line, then arrange not to print it.
137      if ($date_line ne $prev_date_line)
138        {
139          $prev_date_line eq ''
140            or print "\n";
141          print $date_line;
142        }
143      $prev_date_line = $date_line;
144
145      # Omit "Signed-off-by..." lines.
146      @line = grep !/^Signed-off-by: .*>$/, @line;
147
148      # Remove leading and trailing blank lines.
149      while ($line[0] =~ /^\s*$/) { shift @line; }
150      while ($line[$#line] =~ /^\s*$/) { pop @line; }
151
152      # Prefix each non-empty line with a TAB.
153      @line = map { length $_ ? "\t$_" : '' } @line;
154
155      print "\n", join ("\n", @line), "\n";
156
157      defined ($in = <PIPE>)
158        or last;
159      $in ne "\n"
160        and die "$ME:$.: unexpected line:\n$in";
161    }
162
163  close PIPE
164    or die "$ME: error closing pipe from " . quoted_cmd (@cmd) . "\n";
165  # FIXME-someday: include $PROCESS_STATUS in the diagnostic
166}
167
168# Local Variables:
169# indent-tabs-mode: nil
170# eval: (add-hook 'write-file-hooks 'time-stamp)
171# time-stamp-start: "my $VERSION = '"
172# time-stamp-format: "%:y-%02m-%02d %02H:%02M"
173# time-stamp-time-zone: "UTC"
174# time-stamp-end: "'; # UTC"
175# End:
176