/
git-what-branch
executable file
·512 lines (415 loc) · 13.4 KB
/
git-what-branch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
#!/usr/bin/perl
#
# Tell us (by default) the earliest causal path of commits and merges to
# cause the requested commit got onto a named branch. If a commit was
# made directly on a named branch, that obviously is the earliest path.
#
# See the pod documentation below for more information
#
# Thanks to Artur Skawina for his assistance in developing some
# of the algorithms used by this script.
#
# License: GPL v2
# Copyright (c) 2010 Seth Robertson
#
use warnings;
no warnings "uninitialized";
use Getopt::Long;
use strict;
my $USAGE="$0: [--allref] [--all] [--quiet] [--reference-branch=branchname] [--reference=reference] [--version] <commit-hash/tag>...
--allref
Consider even remote branches as candidates for the branch a
reference is on
--all
Print all reachable branch names (and merge paths)
--quiet
Print only the branch names, not the merge paths
--reference-branch <branchname>
The command line arguments/reference are searched to see if
they can reach this branch.
--reference <hash|tag>
Specify a particular commit you which you want to
know how the commit in question was reached
--topo-order
--date-order
By default we select the merge path which merged earliest into
a specific named commit (--date-order). Instead we can select
--topo-order which will select the minimum number of merges.
--version
Determine the version of the installed program.
";
my(%OPTIONS);
Getopt::Long::Configure("bundling", "no_ignore_case", "no_auto_abbrev", "no_getopt_compat", "require_order");
GetOptions(\%OPTIONS, 'a|allref', 'all', 'quiet', 'debug', 'reference-branch=s', 'reference=s', 'verbose|v+', 'version', 'topo-order', 'date-order') || die $USAGE;
if ($OPTIONS{'version'})
{
print "$0 version is {UNTAGGED}\n";
exit(0);
}
my ($OPT_A);
$OPT_A="-a" if ($OPTIONS{'a'});
if ( $#ARGV < 0 )
{
print STDERR $USAGE;
exit(2);
}
my ($MULTI);
$MULTI=1 if ( $#ARGV > 0 );
########################################
#
# Describe a hash if necessary
#
sub describep($)
{
my ($ref) = @_;
if ($ref =~ /^[0-9a-f]{40}$/)
{
my $newref;
chomp($newref = `git describe $ref`);
$ref = $newref if ($newref && $? == 0);
}
$ref;
}
########################################
#
# Find shortest path through a dag
# Return array of shortest path
#
sub find_shortest($$$$);
sub find_shortest($$$$)
{
my ($id,$target,$tree,$mark) = @_;
print STDERR "Looking at node $id\n" if ($OPTIONS{'debug'});
while ($id ne $target)
{
# Is this a merge commit?
if ($#{$tree->{$id}->{'parent'}} > 0)
{
# Is the first parent not a descendant?
if (!$mark->{$tree->{$id}->{'parent'}->[0]})
{
my (@minp);
my ($mindef);
# See which parent is the best connected
foreach my $parent (@{$tree->{$id}->{'parent'}})
{
next unless $mark->{$parent};
my (@tmp) = find_shortest($parent,$target,$tree,$mark);
if (!$mindef || $#minp > $#tmp)
{
@minp = @tmp;
$mindef = 1;
}
}
unshift(@minp,$id);
return(@minp);
}
}
$id = $tree->{$id}->{'parent'}->[0];
}
();
}
foreach my $f (@ARGV)
{
print "Looking for $f\n++++++++++++++++++++++++++++++++++++++++\n" if ($MULTI);
# Translate into a commit hash
my ($TARGET)=`git rev-list -n 1 $f 2>/dev/null`;
die "Unknown reference $f\n" if ($?);
chomp($TARGET);
my (@first,@second);
if ($OPTIONS{'reference'})
{
my $tmp = `git rev-list -n 1 $OPTIONS{'reference'} 2>/dev/null`;
die "Unknown --reference $OPTIONS{'reference'}\n" if ($?);
chomp($tmp);
@first = ($tmp);
}
else
{
# Generate first pass list of candidate branches
@first = grep(s/^\*?\s+// && s/\n// && !/ -\> / && (!$OPTIONS{'reference-branch'} || $OPTIONS{'reference-branch'} eq $_),`git branch $OPT_A --contains $f`);
if ($#first < 0)
{
my $msg = "any named branch";
$msg = "any local named branch" unless ($OPTIONS{'a'});
$msg = "branch $OPTIONS{'reference-branch'}" if ($OPTIONS{'reference-branch'});
die "Commit $f has not merged onto $msg yet\n";
}
}
# Shortcut if we might only need direct commit branches
if (!$OPTIONS{'all'})
{
# Look for merge intos to exclude
foreach my $br (@first)
{
# Exclude branches that this commit was merged into
push(@second,$br) if (grep(/$TARGET/,`git rev-list --first-parent $br`));
}
}
if ($#second >= 0)
{
# If branch was subsequently forked via `git branch <old> <new>`
# we might have multiple answers. Only one is right, but we
# cannot figure out which is the privledged branch because the
# branch creation information is not preserved.
print join("\n",@second)."\n";
}
else
{
# Commit is on an anonymous branch, find out where it merged
my (%brtree,%min);
foreach my $br (@first)
{
my (%commits,@commits);
my $SOURCE = `git rev-list -n 1 $br 2>/dev/null`;
die "Cannot find branch reference. Huh?\n" if ($?);
chomp($SOURCE);
print STDERR "Checking branch $br\n" if ($OPTIONS{'debug'});
# Discover all "ancestry-path" commits between target and branch
my $cmd = qq^git rev-list --ancestry-path --date-order --format=raw "$TARGET".."$br"^;
my ($commit);
foreach my $line (`$cmd`)
{
my (@f) = split(/\s+/,$line);
if ($f[0] eq "commit")
{
$commit = $f[1];
$commit =~ s/^-//; # I have never seen this myself, but Artur Skawina wrote code to defend against it
unshift(@commits,$commit);
}
if ($f[0] eq "parent")
{
push(@{$commits{$commit}->{'parent'}},$f[1]);
}
if ($f[0] eq "committer")
{
$commits{$commit}->{'committime'} = $f[$#f-1];
}
}
print STDERR "Found $#commits+1\n" if ($OPTIONS{'debug'});
my (@path);
# Go through commit list (in forward chonological order)
my (%mark,$cnt);
$mark{$TARGET} = ++$cnt;
foreach my $id (@commits)
{
next unless $commits{$id}->{'parent'};
# Check to see if this commit is actually a descent of $TARGET
if (grep($mark{$_},@{$commits{$id}->{'parent'}}))
{
$mark{$id} = ++$cnt;
}
# Is this a merge commit?
if ($#{$commits{$id}->{'parent'}} > 0)
{
# Is the first parent not a descendant? (earliest merge)
if (!$mark{$commits{$id}->{'parent'}->[0]})
{
push(@path,$id);
}
}
}
# Check to make sure we have gone from TARGET or SOURCE via parents
if (!$mark{$SOURCE})
{
# Not connected
next;
}
print STDERR "Found $#path+1 initial path entries\n" if ($OPTIONS{'debug'});
if ($#path >= 0)
{
my $id = $path[$#path];
@path = find_shortest($id,$TARGET,\%commits,\%mark);
$brtree{$br}->{'path'} = \@path;
$brtree{$br}->{'cnt'} = $#path;
$brtree{$br}->{'tstamp'} = $commits{$id}->{'committime'};
if ($OPTIONS{'all'})
{
if ($OPTIONS{'quiet'})
{
print "$br\n";
}
else
{
foreach my $mp (@{$brtree{$br}->{'path'}})
{
push(@{$brtree{$br}->{'committimes'}},$commits{$mp}->{'committime'});
}
}
}
else
{
if (!defined($min{'tstamp'}) ||
($OPTIONS{'topo-order'}?
($min{'cnt'} > $brtree{$br} ||
($min{'cnt'} == $brtree{$br} &&
$min{'tstamp'} > $brtree{$br}->{'tstamp'})):
($min{'tstamp'} > $brtree{$br}->{'tstamp'})))
{
%min = %{$brtree{$br}};
$min{'br'} = $br;
$min{'commits'} = \%commits;
}
}
}
else
{
if ($OPTIONS{'all'})
{
print "$TARGET is on $br\n";
}
else
{
print "$br\n";
}
$min{'tstamp'} = 0;
delete($min{'br'});
}
}
if (!$OPTIONS{'all'})
{
if ($min{'br'})
{
if ($OPTIONS{'quiet'})
{
print "$min{'br'}\n";
}
else
{
print "$f first merged onto $min{'br'} using the following minimal".($OPTIONS{'topo-order'}?"":" temporal")." path:\n";
my $last = describep($TARGET);
foreach my $br (@{$min{'path'}})
{
my $newm = describep($br);
print " $last merged up at $newm (@{[scalar(localtime($min{'commits'}->{$br}->{'committime'}))]})\n";
$last = $newm;
}
print " $last is on $min{'br'}\n";
}
}
else
{
print "Could not find $f connected anywhere\n" unless defined($min{'tstamp'});
}
}
else
{
if (!$OPTIONS{'quiet'})
{
sub myorder
{
if ($OPTIONS{'topo-order'})
{
my $ret = $brtree{$a}->{'cnt'} <=> $brtree{$b}->{'cnt'};
$ret = $brtree{$a}->{'tstamp'} <=> $brtree{$b}->{'tstamp'} if (!$ret);
$ret;
}
else
{
$brtree{$a}->{'tstamp'} <=> $brtree{$b}->{'tstamp'};
}
}
foreach my $br (sort myorder (keys %brtree))
{
print "* $TARGET first merged onto $br using the following path:\n";
my $last = describep($TARGET);
foreach my $mp (@{$brtree{$br}->{'path'}})
{
my $newm = describep($mp);
my $ctime = shift(@{$brtree{$br}->{'committimes'}});
print " $last merged up at $newm (@{[scalar(localtime($ctime))]})\n";
$last = $newm;
}
print " $last is on $br\n";
}
}
}
}
print "----------------------------------------\n" if ($MULTI);
}
=pod
=head1 NAME
git-what-branch - Discover what branch a particular commit was made on or near
=head1 SYNOPSIS
git-what-branch [--allref] [--all] [--topo-order | --date-order ] [--quiet] [--reference-branch=branchname] [--reference=reference] <commit-hash/tag>...
=head1 OVERVIEW
Tell us (by default) the earliest causal path of commits and merges to
cause the requested commit got onto a named branch. If a commit was
made directly on a named branch, that obviously is the earliest path.
By earliest causal path, we mean the path which merged into a named
branch the earliest, by commit time (unless --topo-order is
specified).
You may specify a particular reference branch or tag or revision to
look at instead of searching (by default) the path for all named
branches. Searching the path for all named branches can take a long
time for an early commit occurring on many branches. If you
specifically name a reference branch or commit, it should normally
take seconds.
=head1 DESCRIPTION
=head2 --allref
Allow even remote branches to be candidates to see what named branch a
particular commit was make on or merged to.
=head2 --all
If the commit in question was not made directly on a named branch (in
which case all branch names would be printed), the system picks the
named branch which the commit was merged to first and prints only that
path. With this argument all paths from the commit in question to all
named branches that it was committed onto are printed.
=head2 --topo-order
Instead of selecting the merge path which resulted in the earliest
commit to a named branch, select the merge path which resulted in the
fewest merges. If multiple merge paths have the same distance, use
earliest merge to break ties.
=head2 --date-order
The default ordering where the merge path which resulted in the
earliest commit to a named branch is displayed.
=head2 --quiet
If the commit was not made on a branch, do not print the path from the
commit to the named branch, just print the branch name.
=head2 --reference-branch <branchname>
Instead of looking at all branches which contain a specific commit,
look only at the named branch. This will save a great deal of time if
many branches contain the commits in question.
=head2 --reference <tagname/commithash>
Instead of looking to see what branch the commit was in, look to see
how the commit got to a specific tag or other commit. This would be
useful to determine if a particular commit that introduced a bug (or a
fix) was in a particular release, for example.
=head1 PERFORMANCE
If many branches (e.g. hundreds) contain the commit, the system may
take a long time (for a particular commit in the linux tree, it took 8
second to explore a branch, but there were over 200 candidate
branches) to track down the path to each commit. Selection of a
particular --reference-branch --reference tag to examine will be
hundreds of times faster (if you have hundreds of candidate branches).
=head1 EXAMPLES
# git-what-branch 1f9c381fa3e0b9b9042e310c69df87eaf9b46ea
1f9c381fa3e0b9b9042e310c69df87eaf9b46ea4 first merged onto v2.6.12-n using the following minimal path:
v2.6.12-rc3-450-g1f9c381 merged up at v2.6.12-rc3-590-gbfd4bda (Thu May 5 08:59:37 2005)
v2.6.12-rc3-590-gbfd4bda merged up at v2.6.12-rc3-461-g84e48b6 (Tue May 3 18:27:24 2005)
v2.6.12-rc3-461-g84e48b6 is on v2.6.12-n
=head1 BUGS
git fast-forward merges make changes to branches without reflecting
that history in a merge commit. This means that when later reviewing
that history, git may label (via --first-parent) the wrong branch as
being named a specific name. Any lies which git makes are reflected
in the output of this program.
Branches which are created after the commit you are interested in has
been merged into another named branch you are interested in cannot be
distinguished from the original branch. Example if you have master
branch, you make commit A, then make a release branch named v1.0,
after branch v1.0 has been created there is no way to know that v1.0
was created later and so both branches will be listed as the branches
that commit A was made on. If git recorded when a branch was created,
we could avoid this problem.
If multiple branches (say due to the previous bug) are candidates and
the commit was NOT made directly on a named branch but rather on an
anonymous branch that was merged, unless you request --all, a
pseudo-random branch will be chosen as the branch advertised via the
merge path.
=head1 ACKNOWLEDGMENTS
Thanks to Artur Skawina for his assistance in developing some
of the algorithms used by this script.
=head1 COPYRIGHT/LICENSE
License: GPL v2
Copyright (c) 2010 Seth Robertson