sub usage {
print <<__EOF__;
Usage:
genchangelog [OPTION(s)...] [new-version]
Options:
-f <FILE> Use `FILE' instead of "ChangeLog".
-o <old-version> Record all commits after `old-version'.
-n <new-version> Record up to (and including) `new-version'.
-i If specified and no `-o' was used, do an "initial
import", meaning to import the whole history up to
the value of the `-n' option.
A single non-option argument is used as if it were the argument to the `-n'
option.
__EOF__
}
use strict;
use warnings;
use English qw{ -no_match_vars };
use Getopt::Std;
my %config = (
'change-log' => "ChangeLog",
'hash-length' => 8,
'line-length' => 74,
'tab-width' => 8
);
my $VERSION = "20120124.1";
sub __call_command {
my (@cmd) = @ARG;
my ($pid, $kid, $kid_ret, @rv);
open $kid, q{-|}, @cmd or die "Could not fork(): $ERRNO\n";
while (my $line = <$kid>) {
chomp $line;
push @rv, $line
}
close $kid;
$kid_ret = $CHILD_ERROR >> 8;
if ($kid_ret != 0) {
if ($#rv >= 0) {
foreach my $line (@rv) {
print "$line\n";
}
}
return;
}
return \@rv;
}
sub __git_stdout {
my ($rv);
unshift @ARG, q{git};
$rv = __call_command(@ARG);
return $rv if (!defined $rv);
return join "\n", @{ $rv };
}
sub __git_stdout_lines {
my ($rv);
unshift @ARG, q{git};
return __call_command(@ARG);
}
sub read_config {
my ($file) = @ARG;
my ($fh);
return if (!defined $file);
open $fh, q{<}, $file or die "Could not open $file: $ERRNO\n";
LINE: while (my $line = <$fh>) {
chomp $line;
next LINE if ($line =~ m/^\s*$/);
next LINE if ($line =~ m/^\s*#/);
if ($line !~ m/^\s*([\w-]+)\s*=\s(.*)$/) {
warn "Invalid line ($line): Skipping.\n";
next LINE;
}
my $key = $1;
my $val = $2;
if (!defined $config{$key}) {
warn "Unknown setting ($key): Skipping.\n";
next LINE;
}
$val =~ s,^#,,;
$config{$key} = $val;
}
close $fh;
}
sub config_file {
my $dir = __git_stdout('rev-parse', '--git-dir');
if (defined $dir) {
my $file = "$dir/genchangelog.cfg";
return $file if (-e $file);
}
return;
}
sub handle_options {
my ($opts) = @ARG;
my $rc = getopts("if:n:o:", $opts);
if (!$rc) {
usage();
exit 1;
}
if ($#ARGV == 0) {
$opts->{n} = $ARGV[0];
} elsif ($#ARGV > 0) {
usage();
exit 1;
}
}
sub get_old_revision_from {
my ($file) = @ARG;
my ($fh, $sha);
if (! -e $file) {
warn "Couldn't find existing change log: $file\n";
return;
}
open $fh, q{<}, $file or die "Could not open $file: $ERRNO\n";
LINE: while (my $line = <$fh>) {
next LINE unless ($line =~ m/^\t\* ([a-f0-9]+):/);
$sha = $1;
last LINE;
}
close $fh;
return if (!defined $sha);
my $rc = __git_stdout('log', '-1', '--format=tformat:%s', "$sha^{commit}");
if (!defined $rc) {
warn "Hash-sum $sha is invalid or non-ambiguous.\n";
return;
}
return $sha;
}
sub move_old {
my ($file) = @ARG;
return 0 if (!-e $file);
my $old = "$file.gen";
if (-e $old) {
unlink $old or die "Could not unlink $old\n";
}
rename $file, $old or die "Could not rename $file to $old\n";
return 1;
}
sub same_author_and_date {
my ($new, $old) = @ARG;
foreach my $item (qw{ author email date }) {
return 0 if ($new->{$item} ne $old->{$item});
}
return 1;
}
sub print_entry_head {
my ($fh, $date, $author, $email) = @ARG;
print {$fh} "$date $author <$email>\n\n";
}
sub print_as_lines {
my ($fh, $initlen, $maxlen, $text, $joinstr, $endstr) = @ARG;
my $len = $initlen;
if ($#{ $text } < 0) {
return $len;
} elsif ($#{ $text } == 0) {
$text->[0] .= $endstr;
} else {
@{ $text } =
( map({ $_ .= $joinstr }
@{ $text }[0 .. $#{ $text } - 1]),
$text->[-1] .= $endstr );
}
foreach my $word (@{ $text }) {
my $newlen = $len + (length $word) + 1;
if ($newlen <= $maxlen) {
print {$fh} " $word";
} else {
$newlen = (length $word) + $config{'tab-width'};
print {$fh} "\n\t$word";
}
$len = $newlen;
}
return $len;
}
sub print_entry {
my ($fh, $hash, $files, $subject) = @ARG;
my $h = substr $hash, 0, $config{'hash-length'};
my @text = split /\s+/, $subject;
my $len = $config{'tab-width'};
my $start = "* $h:";
$len += length $start;
print {$fh} "\t$start";
$len = print_as_lines(
$fh, $len, $config{'line-length'}, $files, q{,}, q{:});
print_as_lines($fh, $len, $config{'line-length'}, \@text, q{}, q{});
print {$fh} "\n\n";
}
sub insert_new {
my ($file, $new, $old) = @ARG;
my ($fh, %last);
my $sums = __git_stdout_lines('log',
'--format=tformat:%H',
(defined $old ? "$old..$new"
: "$new"));
open $fh, q{>}, $file or die "Could not open $file: $ERRNO\n";
%last = ( author => "",
email => "",
date => "" );
foreach my $hash (@{ $sums }) {
my ($d, %hash_data, @files);
$d = __git_stdout_lines('log',
'-1',
'--date=short',
'--format=tformat:%an%n%ae%n%ad%n%s',
"$hash");
if (!defined $d) {
die "Could not get meta data for commit: $hash\n";
}
$hash_data{author} = $d->[0];
$hash_data{email} = $d->[1];
$hash_data{date} = $d->[2];
$hash_data{subject} = $d->[3];
$d = __git_stdout_lines('log',
'-1',
'--name-only',
'--format=format:ignore',
"$hash");
if (!defined $d) {
die "Could not get changed files for commit: $hash\n";
}
shift @{ $d };
@files = sort @{ $d };
if (!same_author_and_date(\%hash_data, \%last)) {
print_entry_head($fh,
$hash_data{date},
$hash_data{author},
$hash_data{email});
}
foreach my $item (qw{ author email date }) {
$last{$item} = $hash_data{$item};
}
print_entry($fh, $hash, \@files, $hash_data{subject});
}
close $fh;
}
sub append_old {
my ($file) = @ARG;
my $genfile = "$file.gen";
return if (!-e $genfile);
my ($fh, $gfh);
open $fh, q{>>}, $file or die "Could not open $file: $ERRNO\n";
open $gfh, q{<}, $genfile or die "Could not open $genfile: $ERRNO\n";
while (my $line = <$gfh>) {
print {$fh} $line;
}
close $fh;
close $gfh;
}
sub cleanup {
my ($file) = @ARG;
if (-e "$file.gen") {
unlink "$file.gen" or die "Could not unlink $file.gen\n";
}
}
my ($new, $old, $changelog, %opts);
my $tmp = __git_stdout('rev-parse', '--is-inside-work-tree');
if (!defined $tmp) {
die "\n Not inside a git working tree. Giving up.\n\n";
}
read_config(config_file());
handle_options(\%opts);
$changelog = $config{'change-log'};
$changelog = $opts{f} if (defined $opts{f});
$new = $opts{n};
$new = q{HEAD} if (!defined $new);
$old = $opts{o};
if (!defined $old && !defined $opts{i}) {
$old = get_old_revision_from($changelog);
if (!defined $old) {
die "Could not get old revision from `$changelog'.\n";
}
}
if (defined $old) {
print "Adding from $old to $new to $changelog...\n";
} else {
print "Adding from the start to $new to $changelog...\n";
}
my $moved = move_old($changelog);
insert_new($changelog, $new, $old);
append_old($changelog);
cleanup($changelog);
if (!$moved) {
my ($pos, $fh);
open $fh, q{+<}, $changelog or die "Couldn't open $changelog: $ERRNO\n";
while (<$fh>) {
$pos = tell $fh unless eof $fh;
}
truncate $fh, $pos or die "Couldn't truncate $changelog: $ERRNO\n";
close $fh;
}