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