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