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