Blob Blame History Raw
#!/usr/bin/perl

use strict;
use warnings;

use POSIX qw(strftime setlocale LC_ALL);

$ENV{'TZ'} = "CET";
setlocale(LC_ALL, "C");

{
my ($last_ts, $last_email, $last_commit, $last_message) = (0, "");
sub print_commit {
	my ($commit, $email, $ts, @message) = @_;

	return unless $commit;
	# display series by the same author and with the same author date
	# as a single changelog entry (see scripts/log2)
	if ($last_ts != $ts || $last_email ne $email) {
		if ($last_commit) {
			print "- commit " . substr($last_commit, 0, 7) . "\n\n";
		}
		return unless @message;
		print "-" x 67 . "\n";
		print strftime("%a %b %e %H:%M:%S %Z %Y - $email\n\n",
			localtime($ts));
		$last_commit = $commit;
		$last_message = "";
	}
	$last_ts = $ts;
	$last_email = $email;
	my $first = 1;
	for my $line (@message) {
		if ($line !~ /^[- ] /) {
			if ($first) {
				$line = "- $line";
			} else {
				$line = "  $line";
			}
		}
		$first = 0;
	}
	my $msg = join("\n", @message);
	if ($msg eq $last_message) {
		# avoid printing cherry-picked commits twice
		# FIXME: Handle the case where a whole patch series is
		# cherry-picked one by one. At the same time, we do not want
		# to filter commits, that have the same changelog within a
		# series, but are different. See for example
		# git grep 'e1000e: update driver version number' SLE11-SP3
		# and many others
		return;
	}
	$last_message = $msg;
	print $msg, "\n";
}
}

sub parse_gitlog {
	my $fh = shift;

	my @res;
	my $cur = { message => [] };
	my @states = qw(commit tree parent author committer blank message);
	my $st = 0;
	my $gpgsig = 0;
	while (my $line = <$fh>) {
		next if $line =~ /^#/;
		chomp($line);
		my $expect = $states[$st];
		if ($expect eq "blank") {
			if ($gpgsig > 0) {
				if ($line =~ /-----END PGP SIGNATURE-----/) {
					$gpgsig = 0;
				}
				next;
			}
			if ($line =~ /^gpgsig/) {
				$gpgsig = 1;
				next;
			}
			if ($line ne "") {
				die "Malformed git rev-parse output ($cur->{commit}): expected blank line, got \"$line\"\n";
			}
			$st++;
			next;
		}
		if ($expect eq "message") {
			if ($line eq "") {
				push(@res, $cur);
				$cur = { message => [] };
				$st = 0;
				next;
			}
			if ($line !~ s/^ {4}//) {
				die "Malformed git rev-parse output ($cur->{commit}): expected log message, got \"$line\"\n";
			}
			next unless $line;
			# delete Signed-off-by: et al
			next if $line =~ /^[A-Z][-a-zA-Z]+-by: /;
			push(@{$cur->{message}}, $line) if $line;
			next;
		}
		# parsing commit headers
		next if $expect eq "commit" && $line eq "";
		(my $got = $line) =~ s/ .*//;
		# Root commit has no "parent" header. Multiple "parent" headers are
		# not possible, since we use --no-merges
		if ($expect eq "parent" && $got eq "author") {
			$expect = $states[++$st];
		}
		if ($got ne $expect) {
			$cur->{commit} ||= "commit unknown";
			die "Malformed git rev-parse output ($cur->{commit}): expected \"$expect\", got \"$got\"\n";
		}
		if ($got eq "commit") {
			($cur->{commit} = $line) =~ s/^commit //;
		} elsif ($got eq "author") {
			($cur->{email} = $line) =~ s/.*<(.+)>.*/$1/;
			($cur->{ts} = $line) =~ s/.*> (\d+) [-+]\d{4}$/$1/;
			if (!$cur->{email} || !$cur->{ts}) {
				die "Malformed author header ($cur->{commit}): $line\n";
			}
		}
		$st++;
	}
	return @res;
}

my $excludes_file;
if ($ARGV[0] eq "--excludes") {
	shift(@ARGV);
	$excludes_file = shift(@ARGV);
}

my @fixups;
if ($ARGV[0] eq "--fixups") {
	shift(@ARGV);
	my $fixups_file = shift(@ARGV);
	open(my $fh, '<', $fixups_file) or die "$fixups_file: $!\n";
	@fixups = parse_gitlog($fh);
	close($fh);
}

open(my $pipe, '-|', "git", "rev-list", "--no-merges", "--pretty=raw", @ARGV)
	or die "Error running git rev-list: $!\n";
my @commits = parse_gitlog($pipe);
close($pipe) or die "Error running git rev-list: $!\n";

# apply any fixups
my %commits_h = map { $_->{commit} => $_ } @commits;
for my $fix (@fixups) {
	my $orig = $commits_h{$fix->{commit}};
	if (!$fix->{message}) {
		# delete the original commit
		$orig->{commit} = undef;
	} else {
		$orig->{email} = $fix->{email};
		$orig->{ts} = $fix->{ts};
		$orig->{message} = $fix->{message};
	}
}

if ($excludes_file) {
	open(my $fh, '<', $excludes_file) or die "$excludes_file: $!\n";
	while (my $id = <$fh>) {
		next if $id =~ /^#/;
		chomp($id);
		# delete the original commit
		$commits_h{$id}->{commit} = undef;
	}
	close($fh);
}

for my $c (sort { $b->{ts} - $a->{ts} } @commits) {
	print_commit($c->{commit}, $c->{email}, $c->{ts}, @{$c->{message}});
}
# print "- commit 1234567" for the last commit
print_commit($commits[$#commits]->{commit}, "", 0);