]> granicus.if.org Git - postgresql/blob - src/tools/git_changelog
Rename git_topo_order -> git_changelog, per discussion.
[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.  Most of the time, such commits occur in the same order
9 # on all branches, and we print them out in that order.  However, if commit
10 # A occurs before commit B on branch X and commit B occurs before commit A
11 # on branch Y, then there's no ordering which is consistent with both
12 # branches.
13 #
14 # When we encounter a situation where there's no single "best" commit to
15 # print next, we print the one that involves the least distortion of the
16 # commit order, summed across all branches.  In the event of a further tie,
17 # the commit from the newer branch prints first.  It is best not to sort
18 # based on timestamp, because git timestamps aren't necessarily in order
19 # (since the timestamp is provided by the committer's machine), even though
20 # for the portion of the history we imported from CVS, we expect that they
21 # will be.
22 #
23 # Even though we don't use timestamps to order commits, they are used to
24 # identify which commits happened at about the same time, for the purpose
25 # of matching up commits from different branches.
26 #
27
28 use strict;
29 use warnings;
30 require Date::Calc;
31 require Getopt::Long;
32 require IPC::Open2;
33
34 # Adjust this list when the set of active branches changes.
35 my @BRANCHES = qw(master REL9_0_STABLE REL8_4_STABLE REL8_3_STABLE
36     REL8_2_STABLE REL8_1_STABLE REL8_0_STABLE REL7_4_STABLE);
37
38 # Might want to make this parameter user-settable.
39 my $timestamp_slop = 600;
40
41 my $since;
42 Getopt::Long::GetOptions('since=s' => \$since) || usage();
43 usage() if @ARGV;
44
45 my @git = qw(git log --date=iso);
46 push @git, '--since=' . $since if defined $since;
47
48 my %all_commits;
49 my %all_commits_by_branch;
50
51 my %commit;
52 for my $branch (@BRANCHES) {
53         my $commitnum = 0;
54         IPC::Open2::open2(my $git_out, my $git_in, @git, "origin/$branch")
55                 || die "can't run @git origin/$branch: $!";
56         while (my $line = <$git_out>) {
57                 if ($line =~ /^commit\s+(.*)/) {
58                         push_commit(\%commit) if %commit;
59                         %commit = (
60                                 'branch' => $branch,
61                                 'commit' => $1,
62                                 'message' => '',
63                                 'commitnum' => $commitnum++,
64                         );
65                 }
66                 elsif ($line =~ /^Author:\s+(.*)/) {
67                         $commit{'author'} = $1;
68                 }
69                 elsif ($line =~ /^Date:\s+(.*)/) {
70                         $commit{'date'} = $1;
71                 }
72                 elsif ($line =~ /^\s+/) {
73                         $commit{'message'} .= $line;
74                 }
75         }
76 }
77
78 my %position;
79 for my $branch (@BRANCHES) {
80         $position{$branch} = 0;
81 }
82 while (1) {
83         my $best_branch;
84         my $best_inversions;
85         for my $branch (@BRANCHES) {
86                 my $leader = $all_commits_by_branch{$branch}->[$position{$branch}];
87                 next if !defined $leader;
88                 my $inversions = 0;
89                 for my $branch2 (@BRANCHES) {
90                         if (defined $leader->{'branch_position'}{$branch2}) {
91                                 $inversions += $leader->{'branch_position'}{$branch2}
92                                         - $position{$branch2};
93                         }
94                 }
95                 if (!defined $best_inversions || $inversions < $best_inversions) {
96                         $best_branch = $branch;
97                         $best_inversions = $inversions;
98                 }
99         }
100         last if !defined $best_branch;
101         my $winner =
102                 $all_commits_by_branch{$best_branch}->[$position{$best_branch}];
103         print $winner->{'header'};
104         print "Commit-Order-Inversions: $best_inversions\n"
105                 if $best_inversions != 0;
106         print $winner->{'message'};
107         $winner->{'done'} = 1;
108         for my $branch (@BRANCHES) {
109                 my $leader = $all_commits_by_branch{$branch}->[$position{$branch}];
110                 if (defined $leader && $leader->{'done'}) {
111                         ++$position{$branch};
112                         redo;
113                 }
114         }
115 }
116
117 sub push_commit {
118         my ($c) = @_;
119         my $ht = hash_commit($c);
120         my $ts = parse_datetime($c->{'date'});
121         my $cc;
122         for my $candidate (@{$all_commits{$ht}}) {
123                 if (abs($ts - $candidate->{'timestamp'}) < $timestamp_slop
124                         && !exists $candidate->{'branch_position'}{$c->{'branch'}})
125                 {
126                         $cc = $candidate;
127                         last;
128                 }
129         }
130         if (!defined $cc) {
131                 $cc = {
132                         'header' => sprintf("Author: %s\n", $c->{'author'}),
133                         'message' => $c->{'message'},
134                         'timestamp' => $ts
135                 };
136                 push @{$all_commits{$ht}}, $cc;
137         }
138         $cc->{'header'} .= sprintf "Branch: %s [%s] %s\n",
139                 $c->{'branch'}, substr($c->{'commit'}, 0, 9), $c->{'date'};
140         push @{$all_commits_by_branch{$c->{'branch'}}}, $cc;
141         $cc->{'branch_position'}{$c->{'branch'}} =
142                 -1+@{$all_commits_by_branch{$c->{'branch'}}};
143 }
144
145 sub hash_commit {
146         my ($c) = @_;
147         return $c->{'author'} . "\0" . $c->{'message'};
148 }
149
150 sub parse_datetime {
151         my ($dt) = @_;
152         $dt =~ /^(\d\d\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/;
153         return Date::Calc::Mktime($1, $2, $3, $4, $5, $6);
154 }
155
156 sub usage {
157         print STDERR <<EOM;
158 Usage: git_changelog [--since=SINCE]
159 EOM
160         exit 1;
161 }