4 # src/tools/git_changelog
6 # Display all commits on active branches, merging together commits from
7 # different branches that occur close together in time and with identical
8 # log messages. Commits are annotated with branch and release info thus:
9 # Branch: REL8_3_STABLE Release: REL8_3_2 [92c3a8004] 2008-03-29 00:15:37 +0000
10 # This shows that the commit on REL8_3_STABLE was released in 8.3.2.
11 # Commits on master will usually instead have notes like
12 # Branch: master Release: REL8_4_BR [6fc9d4272] 2008-03-29 00:15:28 +0000
13 # showing that this commit is ancestral to release branches 8.4 and later.
14 # If no Release: marker appears, the commit hasn't yet made it into any
17 # Most of the time, matchable commits occur in the same order on all branches,
18 # and we print them out in that order. However, if commit A occurs before
19 # commit B on branch X and commit B occurs before commit A on branch Y, then
20 # there's no ordering which is consistent with both branches.
22 # When we encounter a situation where there's no single "best" commit to
23 # print next, we print the one that involves the least distortion of the
24 # commit order, summed across all branches. In the event of a tie on the
25 # distortion measure (which is actually the common case: normally, the
26 # distortion is zero), we choose the commit with latest timestamp. If
27 # that's a tie too, the commit from the newer branch prints first.
36 # Adjust this list when the set of interesting branches changes.
37 # (We could get this from "git branches", but not worth the trouble.)
38 # NB: master must be first!
39 my @BRANCHES = qw(master REL9_0_STABLE REL8_4_STABLE REL8_3_STABLE
40 REL8_2_STABLE REL8_1_STABLE REL8_0_STABLE REL7_4_STABLE REL7_3_STABLE
41 REL7_2_STABLE REL7_1_STABLE REL7_0_PATCHES REL6_5_PATCHES REL6_4);
43 # Might want to make this parameter user-settable.
44 my $timestamp_slop = 600;
48 Getopt::Long::GetOptions('post-date' => \$post_date,
49 'since=s' => \$since) || usage();
52 my @git = qw(git log --date=iso);
53 push @git, '--since=' . $since if defined $since;
55 # Collect the release tag data
59 my $cmd = "git for-each-ref refs/tags";
60 my $pid = IPC::Open2::open2(my $git_out, my $git_in, $cmd)
61 || die "can't run $cmd: $!";
62 while (my $line = <$git_out>) {
63 if ($line =~ m|^([a-f0-9]+)\s+commit\s+refs/tags/(\S+)|) {
66 if ($tag =~ /^REL\d+_\d+$/ ||
67 $tag =~ /^REL\d+_\d+_\d+$/) {
68 $rel_tags{$commit} = $tag;
73 my $child_exit_status = $? >> 8;
74 die "$cmd failed" if $child_exit_status != 0;
77 # Collect the commit data
79 my %all_commits_by_branch;
80 # This remembers where each branch sprouted from master. Note the values
81 # will be wrong if --since terminates the log listing before the branch
82 # sprouts; but in that case it doesn't matter since we also won't reach
83 # the part of master where it would matter.
86 for my $branch (@BRANCHES) {
88 if ($branch eq "master") {
89 push @cmd, "origin/$branch";
91 push @cmd, "--parents";
92 push @cmd, "master..origin/$branch";
94 my $pid = IPC::Open2::open2(my $git_out, my $git_in, @cmd)
95 || die "can't run @cmd: $!";
99 while (my $line = <$git_out>) {
100 if ($line =~ /^commit\s+(\S+)/) {
101 push_commit(\%commit) if %commit;
102 $last_tag = $rel_tags{$1} if defined $rel_tags{$1};
106 'last_tag' => $last_tag,
109 if ($line =~ /^commit\s+\S+\s+(\S+)/) {
112 $last_parent = undef;
115 elsif ($line =~ /^Author:\s+(.*)/) {
116 $commit{'author'} = $1;
118 elsif ($line =~ /^Date:\s+(.*)/) {
119 $commit{'date'} = $1;
121 elsif ($line =~ /^\s\s/) {
122 $commit{'message'} .= $line;
125 push_commit(\%commit) if %commit;
126 $sprout_tags{$last_parent} = $branch if defined $last_parent;
128 my $child_exit_status = $? >> 8;
129 die "@cmd failed" if $child_exit_status != 0;
132 # Run through the master branch and apply tags. We already tagged the other
133 # branches, but master needs a separate pass after we've acquired the
134 # sprout_tags data. Also, in post-date mode we need to add phony entries
135 # for branches that sprouted after a particular master commit was made.
137 my $last_tag = undef;
138 my %sprouted_branches;
139 for my $cc (@{$all_commits_by_branch{'master'}}) {
140 my $commit = $cc->{'commit'};
141 my $c = $cc->{'commits'}->[0];
142 $last_tag = $rel_tags{$commit} if defined $rel_tags{$commit};
143 if (defined $sprout_tags{$commit}) {
144 $last_tag = $sprout_tags{$commit};
145 # normalize branch names for making sprout tags
146 $last_tag =~ s/^(REL\d+_\d+).*/$1_BR/;
148 $c->{'last_tag'} = $last_tag;
150 if (defined $sprout_tags{$commit}) {
151 $sprouted_branches{$sprout_tags{$commit}} = 1;
153 # insert new commits between master and any other commits
154 my @new_commits = ( shift @{$cc->{'commits'}} );
155 for my $branch (reverse sort keys %sprouted_branches) {
157 $ccopy->{'branch'} = $branch;
158 push @new_commits, $ccopy;
160 $cc->{'commits'} = [ @new_commits, @{$cc->{'commits'}} ];
166 for my $branch (@BRANCHES) {
167 $position{$branch} = 0;
174 for my $branch (@BRANCHES) {
175 my $leader = $all_commits_by_branch{$branch}->[$position{$branch}];
176 next if !defined $leader;
178 for my $branch2 (@BRANCHES) {
179 if (defined $leader->{'branch_position'}{$branch2}) {
180 $inversions += $leader->{'branch_position'}{$branch2}
181 - $position{$branch2};
184 if (!defined $best_inversions ||
185 $inversions < $best_inversions ||
186 ($inversions == $best_inversions &&
187 $leader->{'timestamp'} > $best_timestamp)) {
188 $best_branch = $branch;
189 $best_inversions = $inversions;
190 $best_timestamp = $leader->{'timestamp'};
193 last if !defined $best_branch;
195 $all_commits_by_branch{$best_branch}->[$position{$best_branch}];
196 printf "Author: %s\n", $winner->{'author'};
197 foreach my $c (@{$winner->{'commits'}}) {
198 printf "Branch: %s", $c->{'branch'};
199 if (defined $c->{'last_tag'}) {
200 printf " Release: %s", $c->{'last_tag'};
202 printf " [%s] %s\n", substr($c->{'commit'}, 0, 9), $c->{'date'};
204 print "Commit-Order-Inversions: $best_inversions\n"
205 if $best_inversions != 0;
207 print $winner->{'message'};
209 $winner->{'done'} = 1;
210 for my $branch (@BRANCHES) {
211 my $leader = $all_commits_by_branch{$branch}->[$position{$branch}];
212 if (defined $leader && $leader->{'done'}) {
213 ++$position{$branch};
221 my $ht = hash_commit($c);
222 my $ts = parse_datetime($c->{'date'});
224 # Note that this code will never merge two commits on the same branch,
225 # even if they have the same hash (author/message) and nearby
226 # timestamps. This means that there could be multiple potential
227 # matches when we come to add a commit from another branch. Prefer
228 # the closest-in-time one.
229 for my $candidate (@{$all_commits{$ht}}) {
230 my $diff = abs($ts - $candidate->{'timestamp'});
231 if ($diff < $timestamp_slop &&
232 !exists $candidate->{'branch_position'}{$c->{'branch'}})
235 $diff < abs($ts - $cc->{'timestamp'})) {
242 'author' => $c->{'author'},
243 'message' => $c->{'message'},
244 'commit' => $c->{'commit'},
248 push @{$all_commits{$ht}}, $cc;
250 # stash only the fields we'll need later
252 'branch' => $c->{'branch'},
253 'commit' => $c->{'commit'},
254 'date' => $c->{'date'},
255 'last_tag' => $c->{'last_tag'}
257 push @{$cc->{'commits'}}, $smallc;
258 push @{$all_commits_by_branch{$c->{'branch'}}}, $cc;
259 $cc->{'branch_position'}{$c->{'branch'}} =
260 -1+@{$all_commits_by_branch{$c->{'branch'}}};
265 return $c->{'author'} . "\0" . $c->{'message'};
270 $dt =~ /^(\d\d\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)\s+([-+])(\d\d)(\d\d)$/;
271 my $gm = Time::Local::timegm($6, $5, $4, $3, $2-1, $1);
272 my $tzoffset = ($8 * 60 + $9) * 60;
273 $tzoffset = - $tzoffset if $7 eq '-';
274 return $gm - $tzoffset;
279 Usage: git_changelog [--post-date/-p] [--since=SINCE]
280 --post-date Show branches made after a commit occurred
281 --since Print only commits dated since SINCE