Blame build-aux/gitlog-to-changelog

Packit 437b5e
eval '(exit $?0)' && eval 'exec perl -wS "$0" ${1+"$@"}'
Packit 437b5e
  & eval 'exec perl -wS "$0" $argv:q'
Packit 437b5e
    if 0;
Packit 437b5e
# Convert git log output to ChangeLog format.
Packit 437b5e
Packit 437b5e
my $VERSION = '2012-07-29 06:11'; # UTC
Packit 437b5e
# The definition above must lie within the first 8 lines in order
Packit 437b5e
# for the Emacs time-stamp write hook (at end) to update it.
Packit 437b5e
# If you change this file with Emacs, please let the write hook
Packit 437b5e
# do its job.  Otherwise, update this string manually.
Packit 437b5e
Packit 437b5e
# Copyright (C) 2008-2013 Free Software Foundation, Inc.
Packit 437b5e
Packit 437b5e
# This program is free software: you can redistribute it and/or modify
Packit 437b5e
# it under the terms of the GNU General Public License as published by
Packit 437b5e
# the Free Software Foundation, either version 3 of the License, or
Packit 437b5e
# (at your option) any later version.
Packit 437b5e
Packit 437b5e
# This program is distributed in the hope that it will be useful,
Packit 437b5e
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Packit 437b5e
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
Packit 437b5e
# GNU General Public License for more details.
Packit 437b5e
Packit 437b5e
# You should have received a copy of the GNU General Public License
Packit 437b5e
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
Packit 437b5e
Packit 437b5e
# Written by Jim Meyering
Packit 437b5e
Packit 437b5e
use strict;
Packit 437b5e
use warnings;
Packit 437b5e
use Getopt::Long;
Packit 437b5e
use POSIX qw(strftime);
Packit 437b5e
Packit 437b5e
(my $ME = $0) =~ s|.*/||;
Packit 437b5e
Packit 437b5e
# use File::Coda; # http://meyering.net/code/Coda/
Packit 437b5e
END {
Packit 437b5e
  defined fileno STDOUT or return;
Packit 437b5e
  close STDOUT and return;
Packit 437b5e
  warn "$ME: failed to close standard output: $!\n";
Packit 437b5e
  $? ||= 1;
Packit 437b5e
}
Packit 437b5e
Packit 437b5e
sub usage ($)
Packit 437b5e
{
Packit 437b5e
  my ($exit_code) = @_;
Packit 437b5e
  my $STREAM = ($exit_code == 0 ? *STDOUT : *STDERR);
Packit 437b5e
  if ($exit_code != 0)
Packit 437b5e
    {
Packit 437b5e
      print $STREAM "Try '$ME --help' for more information.\n";
Packit 437b5e
    }
Packit 437b5e
  else
Packit 437b5e
    {
Packit 437b5e
      print $STREAM <
Packit 437b5e
Usage: $ME [OPTIONS] [ARGS]
Packit 437b5e
Packit 437b5e
Convert git log output to ChangeLog format.  If present, any ARGS
Packit 437b5e
are passed to "git log".  To avoid ARGS being parsed as options to
Packit 437b5e
$ME, they may be preceded by '--'.
Packit 437b5e
Packit 437b5e
OPTIONS:
Packit 437b5e
Packit 437b5e
   --amend=FILE FILE maps from an SHA1 to perl code (i.e., s/old/new/) that
Packit 437b5e
                  makes a change to SHA1's commit log text or metadata.
Packit 437b5e
   --append-dot append a dot to the first line of each commit message if
Packit 437b5e
                  there is no other punctuation or blank at the end.
Packit 437b5e
   --no-cluster never cluster commit messages under the same date/author
Packit 437b5e
                  header; the default is to cluster adjacent commit messages
Packit 437b5e
                  if their headers are the same and neither commit message
Packit 437b5e
                  contains multiple paragraphs.
Packit 437b5e
   --srcdir=DIR the root of the source tree, from which the .git/
Packit 437b5e
                  directory can be derived.
Packit 437b5e
   --since=DATE convert only the logs since DATE;
Packit 437b5e
                  the default is to convert all log entries.
Packit 437b5e
   --format=FMT set format string for commit subject and body;
Packit 437b5e
                  see 'man git-log' for the list of format metacharacters;
Packit 437b5e
                  the default is '%s%n%b%n'
Packit 437b5e
   --strip-tab  remove one additional leading TAB from commit message lines.
Packit 437b5e
   --strip-cherry-pick  remove data inserted by "git cherry-pick";
Packit 437b5e
                  this includes the "cherry picked from commit ..." line,
Packit 437b5e
                  and the possible final "Conflicts:" paragraph.
Packit 437b5e
   --help       display this help and exit
Packit 437b5e
   --version    output version information and exit
Packit 437b5e
Packit 437b5e
EXAMPLE:
Packit 437b5e
Packit 437b5e
  $ME --since=2008-01-01 > ChangeLog
Packit 437b5e
  $ME -- -n 5 foo > last-5-commits-to-branch-foo
Packit 437b5e
Packit 437b5e
SPECIAL SYNTAX:
Packit 437b5e
Packit 437b5e
The following types of strings are interpreted specially when they appear
Packit 437b5e
at the beginning of a log message line.  They are not copied to the output.
Packit 437b5e
Packit 437b5e
  Copyright-paperwork-exempt: Yes
Packit 437b5e
    Append the "(tiny change)" notation to the usual "date name email"
Packit 437b5e
    ChangeLog header to mark a change that does not require a copyright
Packit 437b5e
    assignment.
Packit 437b5e
  Co-authored-by: Joe User <user\@example.com>
Packit 437b5e
    List the specified name and email address on a second
Packit 437b5e
    ChangeLog header, denoting a co-author.
Packit 437b5e
  Signed-off-by: Joe User <user\@example.com>
Packit 437b5e
    These lines are simply elided.
Packit 437b5e
Packit 437b5e
In a FILE specified via --amend, comment lines (starting with "#") are ignored.
Packit 437b5e
FILE must consist of <SHA,CODE+> pairs where SHA is a 40-byte SHA1 (alone on
Packit 437b5e
a line) referring to a commit in the current project, and CODE refers to one
Packit 437b5e
or more consecutive lines of Perl code.  Pairs must be separated by one or
Packit 437b5e
more blank line.
Packit 437b5e
Packit 437b5e
Here is sample input for use with --amend=FILE, from coreutils:
Packit 437b5e
Packit 437b5e
3a169f4c5d9159283548178668d2fae6fced3030
Packit 437b5e
# fix typo in title:
Packit 437b5e
s/all tile types/all file types/
Packit 437b5e
Packit 437b5e
1379ed974f1fa39b12e2ffab18b3f7a607082202
Packit 437b5e
# Due to a bug in vc-dwim, I mis-attributed a patch by Paul to myself.
Packit 437b5e
# Change the author to be Paul.  Note the escaped "@":
Packit 437b5e
s,Jim .*>,Paul Eggert <eggert\\\@cs.ucla.edu>,
Packit 437b5e
Packit 437b5e
EOF
Packit 437b5e
    }
Packit 437b5e
  exit $exit_code;
Packit 437b5e
}
Packit 437b5e
Packit 437b5e
# If the string $S is a well-behaved file name, simply return it.
Packit 437b5e
# If it contains white space, quotes, etc., quote it, and return the new string.
Packit 437b5e
sub shell_quote($)
Packit 437b5e
{
Packit 437b5e
  my ($s) = @_;
Packit 437b5e
  if ($s =~ m![^\w+/.,-]!)
Packit 437b5e
    {
Packit 437b5e
      # Convert each single quote to '\''
Packit 437b5e
      $s =~ s/\'/\'\\\'\'/g;
Packit 437b5e
      # Then single quote the string.
Packit 437b5e
      $s = "'$s'";
Packit 437b5e
    }
Packit 437b5e
  return $s;
Packit 437b5e
}
Packit 437b5e
Packit 437b5e
sub quoted_cmd(@)
Packit 437b5e
{
Packit 437b5e
  return join (' ', map {shell_quote $_} @_);
Packit 437b5e
}
Packit 437b5e
Packit 437b5e
# Parse file F.
Packit 437b5e
# Comment lines (starting with "#") are ignored.
Packit 437b5e
# F must consist of <SHA,CODE+> pairs where SHA is a 40-byte SHA1
Packit 437b5e
# (alone on a line) referring to a commit in the current project, and
Packit 437b5e
# CODE refers to one or more consecutive lines of Perl code.
Packit 437b5e
# Pairs must be separated by one or more blank line.
Packit 437b5e
sub parse_amend_file($)
Packit 437b5e
{
Packit 437b5e
  my ($f) = @_;
Packit 437b5e
Packit 437b5e
  open F, '<', $f
Packit 437b5e
    or die "$ME: $f: failed to open for reading: $!\n";
Packit 437b5e
Packit 437b5e
  my $fail;
Packit 437b5e
  my $h = {};
Packit 437b5e
  my $in_code = 0;
Packit 437b5e
  my $sha;
Packit 437b5e
  while (defined (my $line = <F>))
Packit 437b5e
    {
Packit 437b5e
      $line =~ /^\#/
Packit 437b5e
        and next;
Packit 437b5e
      chomp $line;
Packit 437b5e
      $line eq ''
Packit 437b5e
        and $in_code = 0, next;
Packit 437b5e
Packit 437b5e
      if (!$in_code)
Packit 437b5e
        {
Packit 437b5e
          $line =~ /^([0-9a-fA-F]{40})$/
Packit 437b5e
            or (warn "$ME: $f:$.: invalid line; expected an SHA1\n"),
Packit 437b5e
              $fail = 1, next;
Packit 437b5e
          $sha = lc $1;
Packit 437b5e
          $in_code = 1;
Packit 437b5e
          exists $h->{$sha}
Packit 437b5e
            and (warn "$ME: $f:$.: duplicate SHA1\n"),
Packit 437b5e
              $fail = 1, next;
Packit 437b5e
        }
Packit 437b5e
      else
Packit 437b5e
        {
Packit 437b5e
          $h->{$sha} ||= '';
Packit 437b5e
          $h->{$sha} .= "$line\n";
Packit 437b5e
        }
Packit 437b5e
    }
Packit 437b5e
  close F;
Packit 437b5e
Packit 437b5e
  $fail
Packit 437b5e
    and exit 1;
Packit 437b5e
Packit 437b5e
  return $h;
Packit 437b5e
}
Packit 437b5e
Packit 437b5e
# git_dir_option $SRCDIR
Packit 437b5e
#
Packit 437b5e
# From $SRCDIR, the --git-dir option to pass to git (none if $SRCDIR
Packit 437b5e
# is undef).  Return as a list (0 or 1 element).
Packit 437b5e
sub git_dir_option($)
Packit 437b5e
{
Packit 437b5e
  my ($srcdir) = @_;
Packit 437b5e
  my @res = ();
Packit 437b5e
  if (defined $srcdir)
Packit 437b5e
    {
Packit 437b5e
      my $qdir = shell_quote $srcdir;
Packit 437b5e
      my $cmd = "cd $qdir && git rev-parse --show-toplevel";
Packit 437b5e
      my $qcmd = shell_quote $cmd;
Packit 437b5e
      my $git_dir = qx($cmd);
Packit 437b5e
      defined $git_dir
Packit 437b5e
        or die "$ME: cannot run $qcmd: $!\n";
Packit 437b5e
      $? == 0
Packit 437b5e
        or die "$ME: $qcmd had unexpected exit code or signal ($?)\n";
Packit 437b5e
      chomp $git_dir;
Packit 437b5e
      push @res, "--git-dir=$git_dir/.git";
Packit 437b5e
    }
Packit 437b5e
  @res;
Packit 437b5e
}
Packit 437b5e
Packit 437b5e
{
Packit 437b5e
  my $since_date;
Packit 437b5e
  my $format_string = '%s%n%b%n';
Packit 437b5e
  my $amend_file;
Packit 437b5e
  my $append_dot = 0;
Packit 437b5e
  my $cluster = 1;
Packit 437b5e
  my $strip_tab = 0;
Packit 437b5e
  my $strip_cherry_pick = 0;
Packit 437b5e
  my $srcdir;
Packit 437b5e
  GetOptions
Packit 437b5e
    (
Packit 437b5e
     help => sub { usage 0 },
Packit 437b5e
     version => sub { print "$ME version $VERSION\n"; exit },
Packit 437b5e
     'since=s' => \$since_date,
Packit 437b5e
     'format=s' => \$format_string,
Packit 437b5e
     'amend=s' => \$amend_file,
Packit 437b5e
     'append-dot' => \$append_dot,
Packit 437b5e
     'cluster!' => \$cluster,
Packit 437b5e
     'strip-tab' => \$strip_tab,
Packit 437b5e
     'strip-cherry-pick' => \$strip_cherry_pick,
Packit 437b5e
     'srcdir=s' => \$srcdir,
Packit 437b5e
    ) or usage 1;
Packit 437b5e
Packit 437b5e
  defined $since_date
Packit 437b5e
    and unshift @ARGV, "--since=$since_date";
Packit 437b5e
Packit 437b5e
  # This is a hash that maps an SHA1 to perl code (i.e., s/old/new/)
Packit 437b5e
  # that makes a correction in the log or attribution of that commit.
Packit 437b5e
  my $amend_code = defined $amend_file ? parse_amend_file $amend_file : {};
Packit 437b5e
Packit 437b5e
  my @cmd = ('git',
Packit 437b5e
             git_dir_option $srcdir,
Packit 437b5e
             qw(log --log-size),
Packit 437b5e
             '--pretty=format:%H:%ct  %an  <%ae>%n%n'.$format_string, @ARGV);
Packit 437b5e
  open PIPE, '-|', @cmd
Packit 437b5e
    or die ("$ME: failed to run '". quoted_cmd (@cmd) ."': $!\n"
Packit 437b5e
            . "(Is your Git too old?  Version 1.5.1 or later is required.)\n");
Packit 437b5e
Packit 437b5e
  my $prev_multi_paragraph;
Packit 437b5e
  my $prev_date_line = '';
Packit 437b5e
  my @prev_coauthors = ();
Packit 437b5e
  while (1)
Packit 437b5e
    {
Packit 437b5e
      defined (my $in = <PIPE>)
Packit 437b5e
        or last;
Packit 437b5e
      $in =~ /^log size (\d+)$/
Packit 437b5e
        or die "$ME:$.: Invalid line (expected log size):\n$in";
Packit 437b5e
      my $log_nbytes = $1;
Packit 437b5e
Packit 437b5e
      my $log;
Packit 437b5e
      my $n_read = read PIPE, $log, $log_nbytes;
Packit 437b5e
      $n_read == $log_nbytes
Packit 437b5e
        or die "$ME:$.: unexpected EOF\n";
Packit 437b5e
Packit 437b5e
      # Extract leading hash.
Packit 437b5e
      my ($sha, $rest) = split ':', $log, 2;
Packit 437b5e
      defined $sha
Packit 437b5e
        or die "$ME:$.: malformed log entry\n";
Packit 437b5e
      $sha =~ /^[0-9a-fA-F]{40}$/
Packit 437b5e
        or die "$ME:$.: invalid SHA1: $sha\n";
Packit 437b5e
Packit 437b5e
      # If this commit's log requires any transformation, do it now.
Packit 437b5e
      my $code = $amend_code->{$sha};
Packit 437b5e
      if (defined $code)
Packit 437b5e
        {
Packit 437b5e
          eval 'use Safe';
Packit 437b5e
          my $s = new Safe;
Packit 437b5e
          # Put the unpreprocessed entry into "$_".
Packit 437b5e
          $_ = $rest;
Packit 437b5e
Packit 437b5e
          # Let $code operate on it, safely.
Packit 437b5e
          my $r = $s->reval("$code")
Packit 437b5e
            or die "$ME:$.:$sha: failed to eval \"$code\":\n$@\n";
Packit 437b5e
Packit 437b5e
          # Note that we've used this entry.
Packit 437b5e
          delete $amend_code->{$sha};
Packit 437b5e
Packit 437b5e
          # Update $rest upon success.
Packit 437b5e
          $rest = $_;
Packit 437b5e
        }
Packit 437b5e
Packit 437b5e
      # Remove lines inserted by "git cherry-pick".
Packit 437b5e
      if ($strip_cherry_pick)
Packit 437b5e
        {
Packit 437b5e
          $rest =~ s/^\s*Conflicts:\n.*//sm;
Packit 437b5e
          $rest =~ s/^\s*\(cherry picked from commit [\da-f]+\)\n//m;
Packit 437b5e
        }
Packit 437b5e
Packit 437b5e
      my @line = split "\n", $rest;
Packit 437b5e
      my $author_line = shift @line;
Packit 437b5e
      defined $author_line
Packit 437b5e
        or die "$ME:$.: unexpected EOF\n";
Packit 437b5e
      $author_line =~ /^(\d+)  (.*>)$/
Packit 437b5e
        or die "$ME:$.: Invalid line "
Packit 437b5e
          . "(expected date/author/email):\n$author_line\n";
Packit 437b5e
Packit 437b5e
      # Format 'Copyright-paperwork-exempt: Yes' as a standard ChangeLog
Packit 437b5e
      # `(tiny change)' annotation.
Packit 437b5e
      my $tiny = (grep (/^Copyright-paperwork-exempt:\s+[Yy]es$/, @line)
Packit 437b5e
                  ? '  (tiny change)' : '');
Packit 437b5e
Packit 437b5e
      my $date_line = sprintf "%s  %s$tiny\n",
Packit 437b5e
        strftime ("%F", localtime ($1)), $2;
Packit 437b5e
Packit 437b5e
      my @coauthors = grep /^Co-authored-by:.*$/, @line;
Packit 437b5e
      # Omit meta-data lines we've already interpreted.
Packit 437b5e
      @line = grep !/^(?:Signed-off-by:[ ].*>$
Packit 437b5e
                       |Co-authored-by:[ ]
Packit 437b5e
                       |Copyright-paperwork-exempt:[ ]
Packit 437b5e
                       )/x, @line;
Packit 437b5e
Packit 437b5e
      # Remove leading and trailing blank lines.
Packit 437b5e
      if (@line)
Packit 437b5e
        {
Packit 437b5e
          while ($line[0] =~ /^\s*$/) { shift @line; }
Packit 437b5e
          while ($line[$#line] =~ /^\s*$/) { pop @line; }
Packit 437b5e
        }
Packit 437b5e
Packit 437b5e
      # Record whether there are two or more paragraphs.
Packit 437b5e
      my $multi_paragraph = grep /^\s*$/, @line;
Packit 437b5e
Packit 437b5e
      # Format 'Co-authored-by: A U Thor <email@example.com>' lines in
Packit 437b5e
      # standard multi-author ChangeLog format.
Packit 437b5e
      for (@coauthors)
Packit 437b5e
        {
Packit 437b5e
          s/^Co-authored-by:\s*/\t    /;
Packit 437b5e
          s/\s*</  </;
Packit 437b5e
Packit 437b5e
          /<.*?@.*\..*>/
Packit 437b5e
            or warn "$ME: warning: missing email address for "
Packit 437b5e
              . substr ($_, 5) . "\n";
Packit 437b5e
        }
Packit 437b5e
Packit 437b5e
      # If clustering of commit messages has been disabled, if this header
Packit 437b5e
      # would be different from the previous date/name/email/coauthors header,
Packit 437b5e
      # or if this or the previous entry consists of two or more paragraphs,
Packit 437b5e
      # then print the header.
Packit 437b5e
      if ( ! $cluster
Packit 437b5e
          || $date_line ne $prev_date_line
Packit 437b5e
          || "@coauthors" ne "@prev_coauthors"
Packit 437b5e
          || $multi_paragraph
Packit 437b5e
          || $prev_multi_paragraph)
Packit 437b5e
        {
Packit 437b5e
          $prev_date_line eq ''
Packit 437b5e
            or print "\n";
Packit 437b5e
          print $date_line;
Packit 437b5e
          @coauthors
Packit 437b5e
            and print join ("\n", @coauthors), "\n";
Packit 437b5e
        }
Packit 437b5e
      $prev_date_line = $date_line;
Packit 437b5e
      @prev_coauthors = @coauthors;
Packit 437b5e
      $prev_multi_paragraph = $multi_paragraph;
Packit 437b5e
Packit 437b5e
      # If there were any lines
Packit 437b5e
      if (@line == 0)
Packit 437b5e
        {
Packit 437b5e
          warn "$ME: warning: empty commit message:\n  $date_line\n";
Packit 437b5e
        }
Packit 437b5e
      else
Packit 437b5e
        {
Packit 437b5e
          if ($append_dot)
Packit 437b5e
            {
Packit 437b5e
              # If the first line of the message has enough room, then
Packit 437b5e
              if (length $line[0] < 72)
Packit 437b5e
                {
Packit 437b5e
                  # append a dot if there is no other punctuation or blank
Packit 437b5e
                  # at the end.
Packit 437b5e
                  $line[0] =~ /[[:punct:]\s]$/
Packit 437b5e
                    or $line[0] .= '.';
Packit 437b5e
                }
Packit 437b5e
            }
Packit 437b5e
Packit 437b5e
          # Remove one additional leading TAB from each line.
Packit 437b5e
          $strip_tab
Packit 437b5e
            and map { s/^\t// } @line;
Packit 437b5e
Packit 437b5e
          # Prefix each non-empty line with a TAB.
Packit 437b5e
          @line = map { length $_ ? "\t$_" : '' } @line;
Packit 437b5e
Packit 437b5e
          print "\n", join ("\n", @line), "\n";
Packit 437b5e
        }
Packit 437b5e
Packit 437b5e
      defined ($in = <PIPE>)
Packit 437b5e
        or last;
Packit 437b5e
      $in ne "\n"
Packit 437b5e
        and die "$ME:$.: unexpected line:\n$in";
Packit 437b5e
    }
Packit 437b5e
Packit 437b5e
  close PIPE
Packit 437b5e
    or die "$ME: error closing pipe from " . quoted_cmd (@cmd) . "\n";
Packit 437b5e
  # FIXME-someday: include $PROCESS_STATUS in the diagnostic
Packit 437b5e
Packit 437b5e
  # Complain about any unused entry in the --amend=F specified file.
Packit 437b5e
  my $fail = 0;
Packit 437b5e
  foreach my $sha (keys %$amend_code)
Packit 437b5e
    {
Packit 437b5e
      warn "$ME:$amend_file: unused entry: $sha\n";
Packit 437b5e
      $fail = 1;
Packit 437b5e
    }
Packit 437b5e
Packit 437b5e
  exit $fail;
Packit 437b5e
}
Packit 437b5e
Packit 437b5e
# Local Variables:
Packit 437b5e
# mode: perl
Packit 437b5e
# indent-tabs-mode: nil
Packit 437b5e
# eval: (add-hook 'write-file-hooks 'time-stamp)
Packit 437b5e
# time-stamp-start: "my $VERSION = '"
Packit 437b5e
# time-stamp-format: "%:y-%02m-%02d %02H:%02M"
Packit 437b5e
# time-stamp-time-zone: "UTC"
Packit 437b5e
# time-stamp-end: "'; # UTC"
Packit 437b5e
# End: