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
176
177
178
179
180
181
182
183
184
185
186
|
#!/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 %commit;
while (my $line = <$git_out>) {
if ($line =~ /^commit\s+(.*)/) {
push_commit(\%commit) if %commit;
%commit = (
'branch' => $branch,
'commit' => $1,
'message' => '',
);
}
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;
# Note that this code will never merge two commits on the same branch,
# even if they have the same hash (author/message) and nearby
# timestamps. This means that there could be multiple potential
# matches when we come to add a commit from another branch. Prefer
# the closest-in-time one.
for my $candidate (@{$all_commits{$ht}}) {
my $diff = abs($ts - $candidate->{'timestamp'});
if ($diff < $timestamp_slop &&
!exists $candidate->{'branch_position'}{$c->{'branch'}})
{
if (!defined $cc ||
$diff < abs($ts - $cc->{'timestamp'})) {
$cc = $candidate;
}
}
}
if (!defined $cc) {
$cc = {
'header' => sprintf("Author: %s\n", $c->{'author'}),
'message' => $c->{'message'},
'commit' => $c->{'commit'},
'timestamp' => $ts
};
push @{$all_commits{$ht}}, $cc;
} elsif ($cc->{'commit'} eq $c->{'commit'}) {
# If this is exactly the same commit we saw before on another
# branch, ignore it. Hence, a commit that's reachable from more
# than one branch head will be reported only for the first
# head it's reachable from. This will give the desired results
# so long as @BRANCHES is ordered with master first.
return;
}
$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;
}
|