gitlog-to-changelog 4.86 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
#!/usr/bin/perl
# Convert git log output to ChangeLog format.

my $VERSION = '2008-12-21 12:07'; # UTC
# The definition above must lie within the first 8 lines in order
# for the Emacs time-stamp write hook (at end) to update it.
# If you change this file with Emacs, please let the write hook
# do its job.  Otherwise, update this string manually.

# Copyright (C) 2008 Free Software Foundation, Inc.

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Written by Jim Meyering

use strict;
use warnings;
use Getopt::Long;
use POSIX qw(strftime);

(my $ME = $0) =~ s|.*/||;

# use File::Coda; # http://meyering.net/code/Coda/
END {
  defined fileno STDOUT or return;
  close STDOUT and return;
  warn "$ME: failed to close standard output: $!\n";
  $? ||= 1;
}

sub usage ($)
{
  my ($exit_code) = @_;
  my $STREAM = ($exit_code == 0 ? *STDOUT : *STDERR);
  if ($exit_code != 0)
    {
      print $STREAM "Try `$ME --help' for more information.\n";
    }
  else
    {
      print $STREAM <<EOF;
Usage: $ME [OPTIONS] [ARGS]

Convert git log output to ChangeLog format.  If present, any ARGS
are passed to "git log".  To avoid ARGS being parsed as options to
$ME, they may be preceded by '--'.

OPTIONS:

   --since=DATE convert only the logs since DATE;
                  the default is to convert all log entries.

   --help       display this help and exit
   --version    output version information and exit

EXAMPLE:

  $ME --since=2008-01-01 > ChangeLog
  $ME -- -n 5 foo > last-5-commits-to-branch-foo

EOF
    }
  exit $exit_code;
}

# If the string $S is a well-behaved file name, simply return it.
# If it contains white space, quotes, etc., quote it, and return the new string.
sub shell_quote($)
{
  my ($s) = @_;
  if ($s =~ m![^\w+/.,-]!)
    {
      # Convert each single quote to '\''
      $s =~ s/\'/\'\\\'\'/g;
      # Then single quote the string.
      $s = "'$s'";
    }
  return $s;
}

sub quoted_cmd(@)
{
  return join (' ', map {shell_quote $_} @_);
}

{
  my $since_date = '1970-01-01 UTC';
  GetOptions
    (
     help => sub { usage 0 },
     version => sub { print "$ME version $VERSION\n"; exit },
     'since=s' => \$since_date,
    ) or usage 1;

  my @cmd = (qw (git log --log-size), "--since=$since_date",
             '--pretty=format:%ct  %an  <%ae>%n%n%s%n%b%n', @ARGV);
  open PIPE, '-|', @cmd
    or die ("$ME: failed to run `". quoted_cmd (@cmd) ."': $!\n"
            . "(Is your Git too old?  Version 1.5.1 or later is required.)\n");

  my $prev_date_line = '';
  while (1)
    {
      defined (my $in = <PIPE>)
        or last;
      $in =~ /^log size (\d+)$/
        or die "$ME:$.: Invalid line (expected log size):\n$in";
      my $log_nbytes = $1;

      my $log;
      my $n_read = read PIPE, $log, $log_nbytes;
      $n_read == $log_nbytes
        or die "$ME:$.: unexpected EOF\n";

      my @line = split "\n", $log;
      my $author_line = shift @line;
      defined $author_line
        or die "$ME:$.: unexpected EOF\n";
      $author_line =~ /^(\d+)  (.*>)$/
        or die "$ME:$.: Invalid line "
          . "(expected date/author/email):\n$author_line\n";

      my $date_line = sprintf "%s  $2\n", strftime ("%F", localtime ($1));
      # If this line would be the same as the previous date/name/email
      # line, then arrange not to print it.
      if ($date_line ne $prev_date_line)
        {
          $prev_date_line eq ''
            or print "\n";
          print $date_line;
        }
      $prev_date_line = $date_line;

      # Omit "Signed-off-by..." lines.
      @line = grep !/^Signed-off-by: .*>$/, @line;

      # Remove leading and trailing blank lines.
      while ($line[0] =~ /^\s*$/) { shift @line; }
      while ($line[$#line] =~ /^\s*$/) { pop @line; }

      # Prefix each non-empty line with a TAB.
      @line = map { length $_ ? "\t$_" : '' } @line;

      print "\n", join ("\n", @line), "\n";

      defined ($in = <PIPE>)
        or last;
      $in ne "\n"
        and die "$ME:$.: unexpected line:\n$in";
    }

  close PIPE
    or die "$ME: error closing pipe from " . quoted_cmd (@cmd) . "\n";
  # FIXME-someday: include $PROCESS_STATUS in the diagnostic
}

# Local Variables:
# indent-tabs-mode: nil
# eval: (add-hook 'write-file-hooks 'time-stamp)
# time-stamp-start: "my $VERSION = '"
# time-stamp-format: "%:y-%02m-%02d %02H:%02M"
# time-stamp-time-zone: "UTC"
# time-stamp-end: "'; # UTC"
# End: