]> granicus.if.org Git - postgresql/blob - src/tools/git_changelog
Stamp HEAD as 9.4devel.
[postgresql] / src / tools / git_changelog
1 #!/usr/bin/perl
2
3 #
4 # src/tools/git_changelog
5 #
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
15 # release.
16 #
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
22 # it appears in.
23 #
24 # Typical usage to generate major release notes:
25 #   git_changelog --since '2010-07-09 00:00:00' --master-only --oldest-first --details-after
26 #
27 # To find the branch start date, use:
28 #   git show $(git merge-base REL9_0_STABLE master)
29
30
31 use strict;
32 use warnings;
33 require Time::Local;
34 require Getopt::Long;
35 require IPC::Open2;
36
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);
45
46 # Might want to make this parameter user-settable.
47 my $timestamp_slop = 600;
48
49 my $details_after = 0;
50 my $post_date     = 0;
51 my $master_only   = 0;
52 my $oldest_first  = 0;
53 my $since;
54 my @output_buffer;
55 my $output_line = '';
56
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();
63 usage() if @ARGV;
64
65 my @git = qw(git log --format=fuller --date=iso);
66 push @git, '--since=' . $since if defined $since;
67
68 # Collect the release tag data
69 my %rel_tags;
70
71 {
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>)
76         {
77                 if ($line =~ m|^([a-f0-9]+)\s+commit\s+refs/tags/(\S+)|)
78                 {
79                         my $commit = $1;
80                         my $tag    = $2;
81                         if (   $tag =~ /^REL\d+_\d+$/
82                                 || $tag =~ /^REL\d+_\d+_\d+$/)
83                         {
84                                 $rel_tags{$commit} = $tag;
85                         }
86                 }
87         }
88         waitpid($pid, 0);
89         my $child_exit_status = $? >> 8;
90         die "$cmd failed" if $child_exit_status != 0;
91 }
92
93 # Collect the commit data
94 my %all_commits;
95 my %all_commits_by_branch;
96
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.
101 my %sprout_tags;
102
103 for my $branch (@BRANCHES)
104 {
105         my @cmd = @git;
106         if ($branch eq "master")
107         {
108                 push @cmd, "origin/$branch";
109         }
110         else
111         {
112                 push @cmd, "--parents";
113                 push @cmd, "master..origin/$branch";
114         }
115         my $pid = IPC::Open2::open2(my $git_out, my $git_in, @cmd)
116           || die "can't run @cmd: $!";
117         my $last_tag = undef;
118         my $last_parent;
119         my %commit;
120         while (my $line = <$git_out>)
121         {
122                 if ($line =~ /^commit\s+(\S+)/)
123                 {
124                         push_commit(\%commit) if %commit;
125                         $last_tag = $rel_tags{$1} if defined $rel_tags{$1};
126                         %commit = (
127                                 'branch'   => $branch,
128                                 'commit'   => $1,
129                                 'last_tag' => $last_tag,
130                                 'message'  => '',);
131                         if ($line =~ /^commit\s+\S+\s+(\S+)/)
132                         {
133                                 $last_parent = $1;
134                         }
135                         else
136                         {
137                                 $last_parent = undef;
138                         }
139                 }
140                 elsif ($line =~ /^Author:\s+(.*)/)
141                 {
142                         $commit{'author'} = $1;
143                 }
144                 elsif ($line =~ /^CommitDate:\s+(.*)/)
145                 {
146                         $commit{'date'} = $1;
147                 }
148                 elsif ($line =~ /^\s\s/)
149                 {
150                         $commit{'message'} .= $line;
151                 }
152         }
153         push_commit(\%commit) if %commit;
154         $sprout_tags{$last_parent} = $branch if defined $last_parent;
155         waitpid($pid, 0);
156         my $child_exit_status = $? >> 8;
157         die "@cmd failed" if $child_exit_status != 0;
158 }
159
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.
164 {
165         my $last_tag = undef;
166         my %sprouted_branches;
167         for my $cc (@{ $all_commits_by_branch{'master'} })
168         {
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})
173                 {
174                         $last_tag = $sprout_tags{$commit};
175
176                         # normalize branch names for making sprout tags
177                         $last_tag =~ s/^(REL\d+_\d+).*/$1_BR/;
178                 }
179                 $c->{'last_tag'} = $last_tag;
180                 if ($post_date)
181                 {
182                         if (defined $sprout_tags{$commit})
183                         {
184                                 $sprouted_branches{ $sprout_tags{$commit} } = 1;
185                         }
186
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)
190                         {
191                                 my $ccopy = { %{$c} };
192                                 $ccopy->{'branch'} = $branch;
193                                 push @new_commits, $ccopy;
194                         }
195                         $cc->{'commits'} = [ @new_commits, @{ $cc->{'commits'} } ];
196                 }
197         }
198 }
199
200 my %position;
201 for my $branch (@BRANCHES)
202 {
203         $position{$branch} = 0;
204 }
205
206 while (1)
207 {
208         my $best_branch;
209         my $best_timestamp;
210         for my $branch (@BRANCHES)
211         {
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)
216                 {
217                         $best_branch    = $branch;
218                         $best_timestamp = $leader->{'timestamp'};
219                 }
220         }
221         last if !defined $best_branch;
222         my $winner =
223           $all_commits_by_branch{$best_branch}->[ $position{$best_branch} ];
224
225         # check for master-only
226         if (!$master_only
227                 || ($winner->{'commits'}[0]->{'branch'} eq 'master'
228                         && @{ $winner->{'commits'} } == 1))
229         {
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);
234                 $output_line = '';
235         }
236
237         $winner->{'done'} = 1;
238         for my $branch (@BRANCHES)
239         {
240                 my $leader = $all_commits_by_branch{$branch}->[ $position{$branch} ];
241                 if (defined $leader && $leader->{'done'})
242                 {
243                         ++$position{$branch};
244                         redo;
245                 }
246         }
247 }
248
249 print @output_buffer if ($oldest_first);
250
251 sub push_commit
252 {
253         my ($c) = @_;
254         my $ht  = hash_commit($c);
255         my $ts  = parse_datetime($c->{'date'});
256         my $cc;
257
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} })
264         {
265                 my $diff = abs($ts - $candidate->{'timestamp'});
266                 if ($diff < $timestamp_slop
267                         && !exists $candidate->{'branch_position'}{ $c->{'branch'} })
268                 {
269                         if (!defined $cc
270                                 || $diff < abs($ts - $cc->{'timestamp'}))
271                         {
272                                 $cc = $candidate;
273                         }
274                 }
275         }
276         if (!defined $cc)
277         {
278                 $cc = {
279                         'author'    => $c->{'author'},
280                         'message'   => $c->{'message'},
281                         'commit'    => $c->{'commit'},
282                         'commits'   => [],
283                         'timestamp' => $ts };
284                 push @{ $all_commits{$ht} }, $cc;
285         }
286
287         # stash only the fields we'll need later
288         my $smallc = {
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'} } };
297 }
298
299 sub hash_commit
300 {
301         my ($c) = @_;
302         return $c->{'author'} . "\0" . $c->{'message'};
303 }
304
305 sub parse_datetime
306 {
307         my ($dt) = @_;
308         $dt =~
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;
314 }
315
316 sub output_str
317 {
318         ($oldest_first) ? ($output_line .= sprintf(shift, @_)) : printf(@_);
319 }
320
321 sub output_details
322 {
323         my $item = shift;
324
325         if ($details_after)
326         {
327                 $item->{'author'} =~ m{^(.*?)\s*<[^>]*>$};
328
329                 # output only author name, not email address
330                 output_str("(%s)\n", $1);
331         }
332         else
333         {
334                 output_str("Author: %s\n", $item->{'author'});
335         }
336         foreach my $c (@{ $item->{'commits'} })
337         {
338                 output_str("Branch: %s ", $c->{'branch'}) if (!$master_only);
339                 if (defined $c->{'last_tag'})
340                 {
341                         output_str("Release: %s ", $c->{'last_tag'});
342                 }
343                 output_str("[%s] %s\n", substr($c->{'commit'}, 0, 9), $c->{'date'});
344         }
345         output_str("\n");
346 }
347
348 sub usage
349 {
350         print STDERR <<EOM;
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
357 EOM
358         exit 1;
359 }