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. In such cases
21 # we sort a merged commit according to its timestamp on the newest branch
24 # Typical usage to generate major release notes:
25 # git_changelog --since '2010-07-09 00:00:00' --master-only --oldest-first --details-after
27 # To find the branch start date, use:
28 # git show $(git merge-base REL9_0_STABLE master)
37 # Adjust this list when the set of interesting branches changes.
38 # (We could get this from "git branches", but not worth the trouble.)
39 # NB: master must be first!
40 my @BRANCHES = qw(master
41 REL9_3_STABLE REL9_2_STABLE REL9_1_STABLE REL9_0_STABLE
42 REL8_4_STABLE REL8_3_STABLE REL8_2_STABLE REL8_1_STABLE REL8_0_STABLE
43 REL7_4_STABLE REL7_3_STABLE REL7_2_STABLE REL7_1_STABLE REL7_0_PATCHES
44 REL6_5_PATCHES REL6_4);
46 # Might want to make this parameter user-settable.
47 my $timestamp_slop = 24 * 60 * 60;
49 my $details_after = 0;
57 Getopt::Long::GetOptions(
58 'details-after' => \$details_after,
59 'master-only' => \$master_only,
60 'post-date' => \$post_date,
61 'oldest-first' => \$oldest_first,
62 'since=s' => \$since) || usage();
65 my @git = qw(git log --format=fuller --date=iso);
66 push @git, '--since=' . $since if defined $since;
68 # Collect the release tag data
72 my $cmd = "git for-each-ref refs/tags";
73 my $pid = IPC::Open2::open2(my $git_out, my $git_in, $cmd)
74 || die "can't run $cmd: $!";
75 while (my $line = <$git_out>)
77 if ($line =~ m|^([a-f0-9]+)\s+commit\s+refs/tags/(\S+)|)
81 if ( $tag =~ /^REL\d+_\d+$/
82 || $tag =~ /^REL\d+_\d+_\d+$/)
84 $rel_tags{$commit} = $tag;
89 my $child_exit_status = $? >> 8;
90 die "$cmd failed" if $child_exit_status != 0;
93 # Collect the commit data
95 my %all_commits_by_branch;
97 # This remembers where each branch sprouted from master. Note the values
98 # will be wrong if --since terminates the log listing before the branch
99 # sprouts; but in that case it doesn't matter since we also won't reach
100 # the part of master where it would matter.
103 for my $branch (@BRANCHES)
106 if ($branch eq "master")
108 push @cmd, "origin/$branch";
112 push @cmd, "--parents";
113 push @cmd, "master..origin/$branch";
115 my $pid = IPC::Open2::open2(my $git_out, my $git_in, @cmd)
116 || die "can't run @cmd: $!";
117 my $last_tag = undef;
120 while (my $line = <$git_out>)
122 if ($line =~ /^commit\s+(\S+)/)
124 push_commit(\%commit) if %commit;
125 $last_tag = $rel_tags{$1} if defined $rel_tags{$1};
129 'last_tag' => $last_tag,
131 if ($line =~ /^commit\s+\S+\s+(\S+)/)
137 $last_parent = undef;
140 elsif ($line =~ /^Author:\s+(.*)/)
142 $commit{'author'} = $1;
144 elsif ($line =~ /^CommitDate:\s+(.*)/)
146 $commit{'date'} = $1;
148 elsif ($line =~ /^\s\s/)
150 $commit{'message'} .= $line;
153 push_commit(\%commit) if %commit;
154 $sprout_tags{$last_parent} = $branch if defined $last_parent;
156 my $child_exit_status = $? >> 8;
157 die "@cmd failed" if $child_exit_status != 0;
160 # Run through the master branch and apply tags. We already tagged the other
161 # branches, but master needs a separate pass after we've acquired the
162 # sprout_tags data. Also, in post-date mode we need to add phony entries
163 # for branches that sprouted after a particular master commit was made.
165 my $last_tag = undef;
166 my %sprouted_branches;
167 for my $cc (@{ $all_commits_by_branch{'master'} })
169 my $commit = $cc->{'commit'};
170 my $c = $cc->{'commits'}->[0];
171 $last_tag = $rel_tags{$commit} if defined $rel_tags{$commit};
172 if (defined $sprout_tags{$commit})
174 $last_tag = $sprout_tags{$commit};
176 # normalize branch names for making sprout tags
177 $last_tag =~ s/^(REL\d+_\d+).*/$1_BR/;
179 $c->{'last_tag'} = $last_tag;
182 if (defined $sprout_tags{$commit})
184 $sprouted_branches{ $sprout_tags{$commit} } = 1;
187 # insert new commits between master and any other commits
188 my @new_commits = (shift @{ $cc->{'commits'} });
189 for my $branch (reverse sort keys %sprouted_branches)
191 my $ccopy = { %{$c} };
192 $ccopy->{'branch'} = $branch;
193 push @new_commits, $ccopy;
195 $cc->{'commits'} = [ @new_commits, @{ $cc->{'commits'} } ];
201 for my $branch (@BRANCHES)
203 $position{$branch} = 0;
210 for my $branch (@BRANCHES)
212 my $leader = $all_commits_by_branch{$branch}->[ $position{$branch} ];
213 next if !defined $leader;
214 if (!defined $best_branch
215 || $leader->{'timestamp'} > $best_timestamp)
217 $best_branch = $branch;
218 $best_timestamp = $leader->{'timestamp'};
221 last if !defined $best_branch;
223 $all_commits_by_branch{$best_branch}->[ $position{$best_branch} ];
225 # check for master-only
227 || ($winner->{'commits'}[0]->{'branch'} eq 'master'
228 && @{ $winner->{'commits'} } == 1))
230 output_details($winner) if (!$details_after);
231 output_str("%s", $winner->{'message'} . "\n");
232 output_details($winner) if ($details_after);
233 unshift(@output_buffer, $output_line) if ($oldest_first);
237 $winner->{'done'} = 1;
238 for my $branch (@BRANCHES)
240 my $leader = $all_commits_by_branch{$branch}->[ $position{$branch} ];
241 if (defined $leader && $leader->{'done'})
243 ++$position{$branch};
249 print @output_buffer if ($oldest_first);
254 my $ht = hash_commit($c);
255 my $ts = parse_datetime($c->{'date'});
258 # Note that this code will never merge two commits on the same branch,
259 # even if they have the same hash (author/message) and nearby
260 # timestamps. This means that there could be multiple potential
261 # matches when we come to add a commit from another branch. Prefer
262 # the closest-in-time one.
263 for my $candidate (@{ $all_commits{$ht} })
265 my $diff = abs($ts - $candidate->{'timestamp'});
266 if ($diff < $timestamp_slop
267 && !exists $candidate->{'branch_position'}{ $c->{'branch'} })
270 || $diff < abs($ts - $cc->{'timestamp'}))
279 'author' => $c->{'author'},
280 'message' => $c->{'message'},
281 'commit' => $c->{'commit'},
283 'timestamp' => $ts };
284 push @{ $all_commits{$ht} }, $cc;
287 # stash only the fields we'll need later
289 'branch' => $c->{'branch'},
290 'commit' => $c->{'commit'},
291 'date' => $c->{'date'},
292 'last_tag' => $c->{'last_tag'} };
293 push @{ $cc->{'commits'} }, $smallc;
294 push @{ $all_commits_by_branch{ $c->{'branch'} } }, $cc;
295 $cc->{'branch_position'}{ $c->{'branch'} } =
296 -1 + @{ $all_commits_by_branch{ $c->{'branch'} } };
302 return $c->{'author'} . "\0" . $c->{'message'};
309 /^(\d\d\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)\s+([-+])(\d\d)(\d\d)$/;
310 my $gm = Time::Local::timegm($6, $5, $4, $3, $2 - 1, $1);
311 my $tzoffset = ($8 * 60 + $9) * 60;
312 $tzoffset = -$tzoffset if $7 eq '-';
313 return $gm - $tzoffset;
318 ($oldest_first) ? ($output_line .= sprintf(shift, @_)) : printf(@_);
327 $item->{'author'} =~ m{^(.*?)\s*<[^>]*>$};
329 # output only author name, not email address
330 output_str("(%s)\n", $1);
334 output_str("Author: %s\n", $item->{'author'});
336 foreach my $c (@{ $item->{'commits'} })
338 output_str("Branch: %s ", $c->{'branch'}) if (!$master_only);
339 if (defined $c->{'last_tag'})
341 output_str("Release: %s ", $c->{'last_tag'});
343 output_str("[%s] %s\n", substr($c->{'commit'}, 0, 9), $c->{'date'});
351 Usage: git_changelog [--details-after/-d] [--master-only/-m] [--oldest-first/-o] [--post-date/-p] [--since=SINCE]
352 --details-after Show branch and author info after the commit description
353 --master-only Show commits made exclusively to the master branch
354 --oldest-first Show oldest commits first
355 --post-date Show branches made after a commit occurred
356 --since Print only commits dated since SINCE