]> granicus.if.org Git - postgresql/blob - src/tools/git_changelog
Add options to git_changelog for use in major release note creation:
[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
25 use strict;
26 use warnings;
27 require Time::Local;
28 require Getopt::Long;
29 require IPC::Open2;
30
31 # Adjust this list when the set of interesting branches changes.
32 # (We could get this from "git branches", but not worth the trouble.)
33 # NB: master must be first!
34 my @BRANCHES = qw(master
35     REL9_1_STABLE REL9_0_STABLE
36     REL8_4_STABLE REL8_3_STABLE REL8_2_STABLE REL8_1_STABLE REL8_0_STABLE
37     REL7_4_STABLE REL7_3_STABLE REL7_2_STABLE REL7_1_STABLE REL7_0_PATCHES
38     REL6_5_PATCHES REL6_4);
39
40 # Might want to make this parameter user-settable.
41 my $timestamp_slop = 600;
42
43 my $details_after = 0;
44 my $post_date = 0;
45 my $master_only = 0;
46 my $oldest_first = 0;
47 my $since;
48 my @output_buffer;
49 my $output_line = '';
50
51 Getopt::Long::GetOptions('details-after' => \$details_after,
52                          'master-only' => \$master_only,
53                          'post-date' => \$post_date,
54                          'oldest-first' => \$oldest_first,
55                          'since=s' => \$since) || usage();
56 usage() if @ARGV;
57
58 my @git = qw(git log --format=fuller --date=iso);
59 push @git, '--since=' . $since if defined $since;
60
61 # Collect the release tag data
62 my %rel_tags;
63
64 {
65         my $cmd = "git for-each-ref refs/tags";
66         my $pid = IPC::Open2::open2(my $git_out, my $git_in, $cmd)
67                 || die "can't run $cmd: $!";
68         while (my $line = <$git_out>) {
69                 if ($line =~ m|^([a-f0-9]+)\s+commit\s+refs/tags/(\S+)|) {
70                     my $commit = $1;
71                     my $tag = $2;
72                     if ($tag =~ /^REL\d+_\d+$/ ||
73                         $tag =~ /^REL\d+_\d+_\d+$/) {
74                         $rel_tags{$commit} = $tag;
75                     }
76                 }
77         }
78         waitpid($pid, 0);
79         my $child_exit_status = $? >> 8;
80         die "$cmd failed" if $child_exit_status != 0;
81 }
82
83 # Collect the commit data
84 my %all_commits;
85 my %all_commits_by_branch;
86 # This remembers where each branch sprouted from master.  Note the values
87 # will be wrong if --since terminates the log listing before the branch
88 # sprouts; but in that case it doesn't matter since we also won't reach
89 # the part of master where it would matter.
90 my %sprout_tags;
91
92 for my $branch (@BRANCHES) {
93         my @cmd = @git;
94         if ($branch eq "master") {
95             push @cmd, "origin/$branch";
96         } else {
97             push @cmd, "--parents";
98             push @cmd, "master..origin/$branch";
99         }
100         my $pid = IPC::Open2::open2(my $git_out, my $git_in, @cmd)
101                 || die "can't run @cmd: $!";
102         my $last_tag = undef;
103         my $last_parent;
104         my %commit;
105         while (my $line = <$git_out>) {
106                 if ($line =~ /^commit\s+(\S+)/) {
107                         push_commit(\%commit) if %commit;
108                         $last_tag = $rel_tags{$1} if defined $rel_tags{$1};
109                         %commit = (
110                                 'branch' => $branch,
111                                 'commit' => $1,
112                                 'last_tag' => $last_tag,
113                                 'message' => '',
114                         );
115                         if ($line =~ /^commit\s+\S+\s+(\S+)/) {
116                                 $last_parent = $1;
117                         } else {
118                                 $last_parent = undef;
119                         }
120                 }
121                 elsif ($line =~ /^Author:\s+(.*)/) {
122                         $commit{'author'} = $1;
123                 }
124                 elsif ($line =~ /^CommitDate:\s+(.*)/) {
125                         $commit{'date'} = $1;
126                 }
127                 elsif ($line =~ /^\s\s/) {
128                         $commit{'message'} .= $line;
129                 }
130         }
131         push_commit(\%commit) if %commit;
132         $sprout_tags{$last_parent} = $branch if defined $last_parent;
133         waitpid($pid, 0);
134         my $child_exit_status = $? >> 8;
135         die "@cmd failed" if $child_exit_status != 0;
136 }
137
138 # Run through the master branch and apply tags.  We already tagged the other
139 # branches, but master needs a separate pass after we've acquired the
140 # sprout_tags data.  Also, in post-date mode we need to add phony entries
141 # for branches that sprouted after a particular master commit was made.
142 {
143         my $last_tag = undef;
144         my %sprouted_branches;
145         for my $cc (@{$all_commits_by_branch{'master'}}) {
146             my $commit = $cc->{'commit'};
147             my $c = $cc->{'commits'}->[0];
148             $last_tag = $rel_tags{$commit} if defined $rel_tags{$commit};
149             if (defined $sprout_tags{$commit}) {
150                 $last_tag = $sprout_tags{$commit};
151                 # normalize branch names for making sprout tags
152                 $last_tag =~ s/^(REL\d+_\d+).*/$1_BR/;
153             }
154             $c->{'last_tag'} = $last_tag;
155             if ($post_date) {
156                 if (defined $sprout_tags{$commit}) {
157                     $sprouted_branches{$sprout_tags{$commit}} = 1;
158                 }
159                 # insert new commits between master and any other commits
160                 my @new_commits = ( shift @{$cc->{'commits'}} );
161                 for my $branch (reverse sort keys %sprouted_branches) {
162                     my $ccopy = {%{$c}};
163                     $ccopy->{'branch'} = $branch;
164                     push @new_commits, $ccopy;
165                 }
166                 $cc->{'commits'} = [ @new_commits, @{$cc->{'commits'}} ];
167             }
168         }
169 }
170
171 my %position;
172 for my $branch (@BRANCHES) {
173         $position{$branch} = 0;
174 }
175
176 while (1) {
177         my $best_branch;
178         my $best_timestamp;
179         for my $branch (@BRANCHES) {
180                 my $leader = $all_commits_by_branch{$branch}->[$position{$branch}];
181                 next if !defined $leader;
182                 if (!defined $best_branch ||
183                     $leader->{'timestamp'} > $best_timestamp) {
184                         $best_branch = $branch;
185                         $best_timestamp = $leader->{'timestamp'};
186                 }
187         }
188         last if !defined $best_branch;
189         my $winner =
190                 $all_commits_by_branch{$best_branch}->[$position{$best_branch}];
191
192         # check for master-only
193         if (! $master_only || ($winner->{'commits'}[0]->{'branch'} eq 'master' &&
194             @{$winner->{'commits'}} == 1)) {
195                 output_details($winner) if (! $details_after);
196                 output_str("%s", $winner->{'message'} . "\n");
197                 output_details($winner) if ($details_after);
198                 unshift(@output_buffer, $output_line) if ($oldest_first);
199                 $output_line = '';
200         }
201
202         $winner->{'done'} = 1;
203         for my $branch (@BRANCHES) {
204                 my $leader = $all_commits_by_branch{$branch}->[$position{$branch}];
205                 if (defined $leader && $leader->{'done'}) {
206                         ++$position{$branch};
207                         redo;
208                 }
209         }
210 }
211
212 print @output_buffer if ($oldest_first);
213
214 sub push_commit {
215         my ($c) = @_;
216         my $ht = hash_commit($c);
217         my $ts = parse_datetime($c->{'date'});
218         my $cc;
219         # Note that this code will never merge two commits on the same branch,
220         # even if they have the same hash (author/message) and nearby
221         # timestamps.  This means that there could be multiple potential
222         # matches when we come to add a commit from another branch.  Prefer
223         # the closest-in-time one.
224         for my $candidate (@{$all_commits{$ht}}) {
225                 my $diff = abs($ts - $candidate->{'timestamp'});
226                 if ($diff < $timestamp_slop &&
227                     !exists $candidate->{'branch_position'}{$c->{'branch'}})
228                 {
229                     if (!defined $cc ||
230                         $diff < abs($ts - $cc->{'timestamp'})) {
231                         $cc = $candidate;
232                     }
233                 }
234         }
235         if (!defined $cc) {
236                 $cc = {
237                         'author' => $c->{'author'},
238                         'message' => $c->{'message'},
239                         'commit' => $c->{'commit'},
240                         'commits' => [],
241                         'timestamp' => $ts
242                 };
243                 push @{$all_commits{$ht}}, $cc;
244         }
245         # stash only the fields we'll need later
246         my $smallc = {
247             'branch' => $c->{'branch'},
248             'commit' => $c->{'commit'},
249             'date' => $c->{'date'},
250             'last_tag' => $c->{'last_tag'}
251         };
252         push @{$cc->{'commits'}}, $smallc;
253         push @{$all_commits_by_branch{$c->{'branch'}}}, $cc;
254         $cc->{'branch_position'}{$c->{'branch'}} =
255                 -1+@{$all_commits_by_branch{$c->{'branch'}}};
256 }
257
258 sub hash_commit {
259         my ($c) = @_;
260         return $c->{'author'} . "\0" . $c->{'message'};
261 }
262
263 sub parse_datetime {
264         my ($dt) = @_;
265         $dt =~ /^(\d\d\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)\s+([-+])(\d\d)(\d\d)$/;
266         my $gm = Time::Local::timegm($6, $5, $4, $3, $2-1, $1);
267         my $tzoffset = ($8 * 60 + $9) * 60;
268         $tzoffset = - $tzoffset if $7 eq '-';
269         return $gm - $tzoffset;
270 }
271
272 sub output_str {
273         ($oldest_first) ? ($output_line .= sprintf(shift, @_)) : printf(@_);
274 }
275
276 sub output_details {
277         my $item = shift;
278
279         if ($details_after) {
280                 $item->{'author'} =~ m{^(.*?)\s*<[^>]*>$};
281                 # output only author name, not email address
282                 output_str("(%s)\n", $1);
283         } else {
284                 output_str("Author: %s\n", $item->{'author'});
285         }
286         foreach my $c (@{$item->{'commits'}}) {
287             output_str("Branch: %s ", $c->{'branch'}) if (! $master_only);
288             if (defined $c->{'last_tag'}) {
289                 output_str("Release: %s ", $c->{'last_tag'});
290             }
291             output_str("[%s] %s\n", substr($c->{'commit'}, 0, 9), $c->{'date'});
292         }
293         output_str("\n");
294 }
295
296 sub usage {
297         print STDERR <<EOM;
298 Usage: git_changelog [--details-after/-d] [--master-only/-m] [--oldest-first/-o] [--post-date/-p] [--since=SINCE]
299     --details-after Show branch and author info after the commit description
300     --master-only   Show commits made exclusively to the master branch
301     --oldest-first  Show oldest commits first
302     --post-date     Show branches made after a commit occurred
303     --since         Print only commits dated since SINCE
304 EOM
305         exit 1;
306 }