summaryrefslogtreecommitdiff
path: root/src/tools/git_changelog
blob: 766f66a4da79cc9ecdffc46c2c3f3d56bfc88130 (plain)
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
#!/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;
}