#!/usr/bin/perl

#
# src/tools/git_changelog
#
# Display all commits on active branches, merging together commits from
# different branches that occur close together in time and with identical
# log messages.
#
# Most of the time, matchable commits occur in the same order on all branches,
# and we print them out in that order.  However, if commit A occurs before
# commit B on branch X and commit B occurs before commit A on branch Y, then
# there's no ordering which is consistent with both branches.
#
# When we encounter a situation where there's no single "best" commit to
# print next, we print the one that involves the least distortion of the
# commit order, summed across all branches.  In the event of a tie on the
# distortion measure (which is actually the common case: normally, the
# distortion is zero), we choose the commit with latest timestamp.  If
# that's a tie too, the commit from the newer branch prints first.
#

use strict;
use warnings;
require Time::Local;
require Getopt::Long;
require IPC::Open2;

# Adjust this list when the set of active branches changes.
my @BRANCHES = qw(master REL9_0_STABLE REL8_4_STABLE REL8_3_STABLE
    REL8_2_STABLE REL8_1_STABLE REL8_0_STABLE REL7_4_STABLE);

# Might want to make this parameter user-settable.
my $timestamp_slop = 600;

my $since;
Getopt::Long::GetOptions('since=s' => \$since) || usage();
usage() if @ARGV;

my @git = qw(git log --date=iso);
push @git, '--since=' . $since if defined $since;

my %all_commits;
my %all_commits_by_branch;

for my $branch (@BRANCHES) {
	my $pid =
	  IPC::Open2::open2(my $git_out, my $git_in, @git, "origin/$branch")
	      || die "can't run @git origin/$branch: $!";
	my $commitnum = 0;
	my %commit;
	while (my $line = <$git_out>) {
		if ($line =~ /^commit\s+(.*)/) {
			push_commit(\%commit) if %commit;
			%commit = (
				'branch' => $branch,
				'commit' => $1,
				'message' => '',
				'commitnum' => $commitnum++,
			);
		}
		elsif ($line =~ /^Author:\s+(.*)/) {
			$commit{'author'} = $1;
		}
		elsif ($line =~ /^Date:\s+(.*)/) {
			$commit{'date'} = $1;
		}
		elsif ($line =~ /^\s\s/) {
			$commit{'message'} .= $line;
		}
	}
	push_commit(\%commit) if %commit;
	waitpid($pid, 0);
	my $child_exit_status = $? >> 8;
	die "@git origin/$branch failed" if $child_exit_status != 0;
}

my %position;
for my $branch (@BRANCHES) {
	$position{$branch} = 0;
}

while (1) {
	my $best_branch;
	my $best_inversions;
	my $best_timestamp;
	for my $branch (@BRANCHES) {
		my $leader = $all_commits_by_branch{$branch}->[$position{$branch}];
		next if !defined $leader;
		my $inversions = 0;
		for my $branch2 (@BRANCHES) {
			if (defined $leader->{'branch_position'}{$branch2}) {
				$inversions += $leader->{'branch_position'}{$branch2}
					- $position{$branch2};
			}
		}
		if (!defined $best_inversions ||
		    $inversions < $best_inversions ||
		    ($inversions == $best_inversions &&
		     $leader->{'timestamp'} > $best_timestamp)) {
			$best_branch = $branch;
			$best_inversions = $inversions;
			$best_timestamp = $leader->{'timestamp'};
		}
	}
	last if !defined $best_branch;
	my $winner =
		$all_commits_by_branch{$best_branch}->[$position{$best_branch}];
	print $winner->{'header'};
	print "Commit-Order-Inversions: $best_inversions\n"
		if $best_inversions != 0;
	print "\n";
	print $winner->{'message'};
	print "\n";
	$winner->{'done'} = 1;
	for my $branch (@BRANCHES) {
		my $leader = $all_commits_by_branch{$branch}->[$position{$branch}];
		if (defined $leader && $leader->{'done'}) {
			++$position{$branch};
			redo;
		}
	}
}

sub push_commit {
	my ($c) = @_;
	my $ht = hash_commit($c);
	my $ts = parse_datetime($c->{'date'});
	my $cc;
	for my $candidate (@{$all_commits{$ht}}) {
		if (abs($ts - $candidate->{'timestamp'}) < $timestamp_slop
			&& !exists $candidate->{'branch_position'}{$c->{'branch'}})
		{
			$cc = $candidate;
			last;
		}
	}
	if (!defined $cc) {
		$cc = {
			'header' => sprintf("Author: %s\n", $c->{'author'}),
			'message' => $c->{'message'},
			'timestamp' => $ts
		};
		push @{$all_commits{$ht}}, $cc;
	}
	$cc->{'header'} .= sprintf "Branch: %s [%s] %s\n",
		$c->{'branch'}, substr($c->{'commit'}, 0, 9), $c->{'date'};
	push @{$all_commits_by_branch{$c->{'branch'}}}, $cc;
	$cc->{'branch_position'}{$c->{'branch'}} =
		-1+@{$all_commits_by_branch{$c->{'branch'}}};
}

sub hash_commit {
	my ($c) = @_;
	return $c->{'author'} . "\0" . $c->{'message'};
}

sub parse_datetime {
	my ($dt) = @_;
	$dt =~ /^(\d\d\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)\s+([-+])(\d\d)(\d\d)$/;
	my $gm = Time::Local::timegm($6, $5, $4, $3, $2-1, $1);
	my $tzoffset = ($8 * 60 + $9) * 60;
	$tzoffset = - $tzoffset if $7 eq '-';
	return $gm - $tzoffset;
}

sub usage {
	print STDERR <<EOM;
Usage: git_changelog [--since=SINCE]
EOM
	exit 1;
}
