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