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