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