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