Blame build-aux/gitlog-to-changelog

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