Commit | Line | Data |
---|---|---|
f5ebf5c6 LC |
1 | eval '(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 | ||
6 | my $VERSION = '2012-07-29 06:11'; # 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-2013 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 | ||
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 | { | |
50 | print $STREAM "Try '$ME --help' for more information.\n"; | |
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 | ||
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 | --format=FMT set format string for commit subject and body; | |
76 | see 'man git-log' for the list of format metacharacters; | |
77 | the default is '%s%n%b%n' | |
78 | --strip-tab remove one additional leading TAB from commit message lines. | |
79 | --strip-cherry-pick remove data inserted by "git cherry-pick"; | |
80 | this includes the "cherry picked from commit ..." line, | |
81 | and the possible final "Conflicts:" paragraph. | |
82 | --help display this help and exit | |
83 | --version output version information and exit | |
84 | ||
85 | EXAMPLE: | |
86 | ||
87 | $ME --since=2008-01-01 > ChangeLog | |
88 | $ME -- -n 5 foo > last-5-commits-to-branch-foo | |
89 | ||
90 | SPECIAL SYNTAX: | |
91 | ||
92 | The following types of strings are interpreted specially when they appear | |
93 | at the beginning of a log message line. They are not copied to the output. | |
94 | ||
95 | Copyright-paperwork-exempt: Yes | |
96 | Append the "(tiny change)" notation to the usual "date name email" | |
97 | ChangeLog header to mark a change that does not require a copyright | |
98 | assignment. | |
99 | Co-authored-by: Joe User <user\@example.com> | |
100 | List the specified name and email address on a second | |
101 | ChangeLog header, denoting a co-author. | |
102 | Signed-off-by: Joe User <user\@example.com> | |
103 | These lines are simply elided. | |
104 | ||
105 | In a FILE specified via --amend, comment lines (starting with "#") are ignored. | |
106 | FILE must consist of <SHA,CODE+> pairs where SHA is a 40-byte SHA1 (alone on | |
107 | a line) referring to a commit in the current project, and CODE refers to one | |
108 | or more consecutive lines of Perl code. Pairs must be separated by one or | |
109 | more blank line. | |
110 | ||
111 | Here is sample input for use with --amend=FILE, from coreutils: | |
112 | ||
113 | 3a169f4c5d9159283548178668d2fae6fced3030 | |
114 | # fix typo in title: | |
115 | s/all tile types/all file types/ | |
116 | ||
117 | 1379ed974f1fa39b12e2ffab18b3f7a607082202 | |
118 | # Due to a bug in vc-dwim, I mis-attributed a patch by Paul to myself. | |
119 | # Change the author to be Paul. Note the escaped "@": | |
120 | s,Jim .*>,Paul Eggert <eggert\\\@cs.ucla.edu>, | |
121 | ||
122 | EOF | |
123 | } | |
124 | exit $exit_code; | |
125 | } | |
126 | ||
127 | # If the string $S is a well-behaved file name, simply return it. | |
128 | # If it contains white space, quotes, etc., quote it, and return the new string. | |
129 | sub shell_quote($) | |
130 | { | |
131 | my ($s) = @_; | |
132 | if ($s =~ m![^\w+/.,-]!) | |
133 | { | |
134 | # Convert each single quote to '\'' | |
135 | $s =~ s/\'/\'\\\'\'/g; | |
136 | # Then single quote the string. | |
137 | $s = "'$s'"; | |
138 | } | |
139 | return $s; | |
140 | } | |
141 | ||
142 | sub quoted_cmd(@) | |
143 | { | |
144 | return join (' ', map {shell_quote $_} @_); | |
145 | } | |
146 | ||
147 | # Parse file F. | |
148 | # Comment lines (starting with "#") are ignored. | |
149 | # F must consist of <SHA,CODE+> pairs where SHA is a 40-byte SHA1 | |
150 | # (alone on a line) referring to a commit in the current project, and | |
151 | # CODE refers to one or more consecutive lines of Perl code. | |
152 | # Pairs must be separated by one or more blank line. | |
153 | sub parse_amend_file($) | |
154 | { | |
155 | my ($f) = @_; | |
156 | ||
157 | open F, '<', $f | |
158 | or die "$ME: $f: failed to open for reading: $!\n"; | |
159 | ||
160 | my $fail; | |
161 | my $h = {}; | |
162 | my $in_code = 0; | |
163 | my $sha; | |
164 | while (defined (my $line = <F>)) | |
165 | { | |
166 | $line =~ /^\#/ | |
167 | and next; | |
168 | chomp $line; | |
169 | $line eq '' | |
170 | and $in_code = 0, next; | |
171 | ||
172 | if (!$in_code) | |
173 | { | |
174 | $line =~ /^([0-9a-fA-F]{40})$/ | |
175 | or (warn "$ME: $f:$.: invalid line; expected an SHA1\n"), | |
176 | $fail = 1, next; | |
177 | $sha = lc $1; | |
178 | $in_code = 1; | |
179 | exists $h->{$sha} | |
180 | and (warn "$ME: $f:$.: duplicate SHA1\n"), | |
181 | $fail = 1, next; | |
182 | } | |
183 | else | |
184 | { | |
185 | $h->{$sha} ||= ''; | |
186 | $h->{$sha} .= "$line\n"; | |
187 | } | |
188 | } | |
189 | close F; | |
190 | ||
191 | $fail | |
192 | and exit 1; | |
193 | ||
194 | return $h; | |
195 | } | |
196 | ||
197 | # git_dir_option $SRCDIR | |
198 | # | |
199 | # From $SRCDIR, the --git-dir option to pass to git (none if $SRCDIR | |
200 | # is undef). Return as a list (0 or 1 element). | |
201 | sub git_dir_option($) | |
202 | { | |
203 | my ($srcdir) = @_; | |
204 | my @res = (); | |
205 | if (defined $srcdir) | |
206 | { | |
207 | my $qdir = shell_quote $srcdir; | |
208 | my $cmd = "cd $qdir && git rev-parse --show-toplevel"; | |
209 | my $qcmd = shell_quote $cmd; | |
210 | my $git_dir = qx($cmd); | |
211 | defined $git_dir | |
212 | or die "$ME: cannot run $qcmd: $!\n"; | |
213 | $? == 0 | |
214 | or die "$ME: $qcmd had unexpected exit code or signal ($?)\n"; | |
215 | chomp $git_dir; | |
216 | push @res, "--git-dir=$git_dir/.git"; | |
217 | } | |
218 | @res; | |
219 | } | |
220 | ||
221 | { | |
222 | my $since_date; | |
223 | my $format_string = '%s%n%b%n'; | |
224 | my $amend_file; | |
225 | my $append_dot = 0; | |
226 | my $cluster = 1; | |
227 | my $strip_tab = 0; | |
228 | my $strip_cherry_pick = 0; | |
229 | my $srcdir; | |
230 | GetOptions | |
231 | ( | |
232 | help => sub { usage 0 }, | |
233 | version => sub { print "$ME version $VERSION\n"; exit }, | |
234 | 'since=s' => \$since_date, | |
235 | 'format=s' => \$format_string, | |
236 | 'amend=s' => \$amend_file, | |
237 | 'append-dot' => \$append_dot, | |
238 | 'cluster!' => \$cluster, | |
239 | 'strip-tab' => \$strip_tab, | |
240 | 'strip-cherry-pick' => \$strip_cherry_pick, | |
241 | 'srcdir=s' => \$srcdir, | |
242 | ) or usage 1; | |
243 | ||
244 | defined $since_date | |
245 | and unshift @ARGV, "--since=$since_date"; | |
246 | ||
247 | # This is a hash that maps an SHA1 to perl code (i.e., s/old/new/) | |
248 | # that makes a correction in the log or attribution of that commit. | |
249 | my $amend_code = defined $amend_file ? parse_amend_file $amend_file : {}; | |
250 | ||
251 | my @cmd = ('git', | |
252 | git_dir_option $srcdir, | |
253 | qw(log --log-size), | |
254 | '--pretty=format:%H:%ct %an <%ae>%n%n'.$format_string, @ARGV); | |
255 | open PIPE, '-|', @cmd | |
256 | or die ("$ME: failed to run '". quoted_cmd (@cmd) ."': $!\n" | |
257 | . "(Is your Git too old? Version 1.5.1 or later is required.)\n"); | |
258 | ||
259 | my $prev_multi_paragraph; | |
260 | my $prev_date_line = ''; | |
261 | my @prev_coauthors = (); | |
262 | while (1) | |
263 | { | |
264 | defined (my $in = <PIPE>) | |
265 | or last; | |
266 | $in =~ /^log size (\d+)$/ | |
267 | or die "$ME:$.: Invalid line (expected log size):\n$in"; | |
268 | my $log_nbytes = $1; | |
269 | ||
270 | my $log; | |
271 | my $n_read = read PIPE, $log, $log_nbytes; | |
272 | $n_read == $log_nbytes | |
273 | or die "$ME:$.: unexpected EOF\n"; | |
274 | ||
275 | # Extract leading hash. | |
276 | my ($sha, $rest) = split ':', $log, 2; | |
277 | defined $sha | |
278 | or die "$ME:$.: malformed log entry\n"; | |
279 | $sha =~ /^[0-9a-fA-F]{40}$/ | |
280 | or die "$ME:$.: invalid SHA1: $sha\n"; | |
281 | ||
282 | # If this commit's log requires any transformation, do it now. | |
283 | my $code = $amend_code->{$sha}; | |
284 | if (defined $code) | |
285 | { | |
286 | eval 'use Safe'; | |
287 | my $s = new Safe; | |
288 | # Put the unpreprocessed entry into "$_". | |
289 | $_ = $rest; | |
290 | ||
291 | # Let $code operate on it, safely. | |
292 | my $r = $s->reval("$code") | |
293 | or die "$ME:$.:$sha: failed to eval \"$code\":\n$@\n"; | |
294 | ||
295 | # Note that we've used this entry. | |
296 | delete $amend_code->{$sha}; | |
297 | ||
298 | # Update $rest upon success. | |
299 | $rest = $_; | |
300 | } | |
301 | ||
302 | # Remove lines inserted by "git cherry-pick". | |
303 | if ($strip_cherry_pick) | |
304 | { | |
305 | $rest =~ s/^\s*Conflicts:\n.*//sm; | |
306 | $rest =~ s/^\s*\(cherry picked from commit [\da-f]+\)\n//m; | |
307 | } | |
308 | ||
309 | my @line = split "\n", $rest; | |
310 | my $author_line = shift @line; | |
311 | defined $author_line | |
312 | or die "$ME:$.: unexpected EOF\n"; | |
313 | $author_line =~ /^(\d+) (.*>)$/ | |
314 | or die "$ME:$.: Invalid line " | |
315 | . "(expected date/author/email):\n$author_line\n"; | |
316 | ||
317 | # Format 'Copyright-paperwork-exempt: Yes' as a standard ChangeLog | |
318 | # `(tiny change)' annotation. | |
319 | my $tiny = (grep (/^Copyright-paperwork-exempt:\s+[Yy]es$/, @line) | |
320 | ? ' (tiny change)' : ''); | |
321 | ||
322 | my $date_line = sprintf "%s %s$tiny\n", | |
323 | strftime ("%F", localtime ($1)), $2; | |
324 | ||
325 | my @coauthors = grep /^Co-authored-by:.*$/, @line; | |
326 | # Omit meta-data lines we've already interpreted. | |
327 | @line = grep !/^(?:Signed-off-by:[ ].*>$ | |
328 | |Co-authored-by:[ ] | |
329 | |Copyright-paperwork-exempt:[ ] | |
330 | )/x, @line; | |
331 | ||
332 | # Remove leading and trailing blank lines. | |
333 | if (@line) | |
334 | { | |
335 | while ($line[0] =~ /^\s*$/) { shift @line; } | |
336 | while ($line[$#line] =~ /^\s*$/) { pop @line; } | |
337 | } | |
338 | ||
339 | # Record whether there are two or more paragraphs. | |
340 | my $multi_paragraph = grep /^\s*$/, @line; | |
341 | ||
342 | # Format 'Co-authored-by: A U Thor <email@example.com>' lines in | |
343 | # standard multi-author ChangeLog format. | |
344 | for (@coauthors) | |
345 | { | |
346 | s/^Co-authored-by:\s*/\t /; | |
347 | s/\s*</ </; | |
348 | ||
349 | /<.*?@.*\..*>/ | |
350 | or warn "$ME: warning: missing email address for " | |
351 | . substr ($_, 5) . "\n"; | |
352 | } | |
353 | ||
354 | # If clustering of commit messages has been disabled, if this header | |
355 | # would be different from the previous date/name/email/coauthors header, | |
356 | # or if this or the previous entry consists of two or more paragraphs, | |
357 | # then print the header. | |
358 | if ( ! $cluster | |
359 | || $date_line ne $prev_date_line | |
360 | || "@coauthors" ne "@prev_coauthors" | |
361 | || $multi_paragraph | |
362 | || $prev_multi_paragraph) | |
363 | { | |
364 | $prev_date_line eq '' | |
365 | or print "\n"; | |
366 | print $date_line; | |
367 | @coauthors | |
368 | and print join ("\n", @coauthors), "\n"; | |
369 | } | |
370 | $prev_date_line = $date_line; | |
371 | @prev_coauthors = @coauthors; | |
372 | $prev_multi_paragraph = $multi_paragraph; | |
373 | ||
374 | # If there were any lines | |
375 | if (@line == 0) | |
376 | { | |
377 | warn "$ME: warning: empty commit message:\n $date_line\n"; | |
378 | } | |
379 | else | |
380 | { | |
381 | if ($append_dot) | |
382 | { | |
383 | # If the first line of the message has enough room, then | |
384 | if (length $line[0] < 72) | |
385 | { | |
386 | # append a dot if there is no other punctuation or blank | |
387 | # at the end. | |
388 | $line[0] =~ /[[:punct:]\s]$/ | |
389 | or $line[0] .= '.'; | |
390 | } | |
391 | } | |
392 | ||
393 | # Remove one additional leading TAB from each line. | |
394 | $strip_tab | |
395 | and map { s/^\t// } @line; | |
396 | ||
397 | # Prefix each non-empty line with a TAB. | |
398 | @line = map { length $_ ? "\t$_" : '' } @line; | |
399 | ||
400 | print "\n", join ("\n", @line), "\n"; | |
401 | } | |
402 | ||
403 | defined ($in = <PIPE>) | |
404 | or last; | |
405 | $in ne "\n" | |
406 | and die "$ME:$.: unexpected line:\n$in"; | |
407 | } | |
408 | ||
409 | close PIPE | |
410 | or die "$ME: error closing pipe from " . quoted_cmd (@cmd) . "\n"; | |
411 | # FIXME-someday: include $PROCESS_STATUS in the diagnostic | |
412 | ||
413 | # Complain about any unused entry in the --amend=F specified file. | |
414 | my $fail = 0; | |
415 | foreach my $sha (keys %$amend_code) | |
416 | { | |
417 | warn "$ME:$amend_file: unused entry: $sha\n"; | |
418 | $fail = 1; | |
419 | } | |
420 | ||
421 | exit $fail; | |
422 | } | |
423 | ||
424 | # Local Variables: | |
425 | # mode: perl | |
426 | # indent-tabs-mode: nil | |
427 | # eval: (add-hook 'write-file-hooks 'time-stamp) | |
428 | # time-stamp-start: "my $VERSION = '" | |
429 | # time-stamp-format: "%:y-%02m-%02d %02H:%02M" | |
430 | # time-stamp-time-zone: "UTC" | |
431 | # time-stamp-end: "'; # UTC" | |
432 | # End: |