Home | History | Annotate | Line # | Download | only in build-aux
      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