-
Notifications
You must be signed in to change notification settings - Fork 0
/
git-compare-branch
executable file
·758 lines (675 loc) · 32.3 KB
/
git-compare-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
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
#!/usr/bin/env python
# (c) Copyright 2017 Jonathan Simmonds
"""Script to compare the contents of two git branches."""
import argparse # ArgumentParser
import subprocess # call, check_output
import sys # exit
import re # match
class GitError(RuntimeError):
"""Exception caused by Git not behaving as expected."""
pass
class TreeError(RuntimeError):
"""Exception caused by the parsed tree structure not looking as expected."""
pass
class Commit(object):
"""Represents an immutable single commit object.
Attributes:
hash: string sha hash for the commit. Must be unique.
ihash: int sha hash for the commit. Must be unique. Must be the
integer representation of hash.
parents: list of string hashes of parent commits.
children: list of string hashes of child commits.
commit_date: string date the commit was pushed.
author_name: string name of commit author.
author_email: string e-mail address of commit author.
subject: string subject line of the commit.
"""
def __init__(self, sha, parents, children, commit_date, author_name,
author_email, subject):
"""Inits the Commit."""
self.hash = sha
self.ihash = int(sha, 16)
self.parents = parents
self.children = children
self.commit_date = commit_date
self.author_name = author_name
self.author_email = author_email
self.subject = subject
def __str__(self):
"""str cast operator."""
return 'commit %s\nparents %s\nchildren %s\nauthor %s <%s>\ndate %s\n%s' % \
(self.hash, ' '.join(self.parents), ' '.join(self.children),
self.author_name, self.author_email, self.commit_date, self.subject)
def __hash__(self):
"""Hash operator."""
return self.ihash
def __cmp__(self, other):
"""Basic comparison operator. Returns < 0 iff self < other"""
if isinstance(other, self.__class__):
return self.ihash - other.ihash
return -1
def __eq__(self, other):
"""Equality operator."""
if isinstance(other, self.__class__):
return self.ihash == other.ihash
return False
def __ne__(self, other):
"""Non-equality operator."""
if isinstance(other, self.__class__):
return self.ihash != other.ihash
return False
def __lt__(self, other):
"""Less than operator."""
if isinstance(other, self.__class__):
return self.ihash < other.ihash
return False
def __le__(self, other):
"""Less than or equal to operator."""
if isinstance(other, self.__class__):
return self.ihash <= other.ihash
return False
def __gt__(self, other):
"""Greater than operator."""
if isinstance(other, self.__class__):
return self.ihash > other.ihash
return False
def __ge__(self, other):
"""Greater than or equal to operator."""
if isinstance(other, self.__class__):
return self.ihash >= other.ihash
return False
def to_string(self, pretty=False):
"""Retrieves a printable representation of the commit.
Args:
pretty: bool True to print a format pleasing to humans, False to
print the raw hash.
Returns:
string: Representation of the commit.
"""
if pretty:
return '%s [%s] %s' % (self.hash[:7], self.commit_date, self.subject)
return self.hash
class Repo(object):
"""Represents a repository and its associated commit tree.
Attributes:
_tree: dict mapping hashes to commit objects.
_leaves: list of hashes of all leaf commits (those who do not have
children or whose children are not in the tree).
_root: the root commit hash (that which does not have parents or
whose parents are not in the tree). Providing LOOKBACK_THRESHOLD is
sufficient there cannot be more than one.
_heads: dict mapping branch names to hashes.
_lookback: int number of commits to look back when building the tree.
May be <= 0 to consider all commits.
"""
def __init__(self, lookback_distance):
"""Inits the Repo and sets branch heads.
Args:
lookback_distance: int number of commits to lookback when building
the tree. May be <= 0 to consider all commits.
"""
self._assert_is_git_repository()
self._tree = {}
self._leaves = []
self._root = None
self._heads = self._get_branch_heads()
self._lookback = lookback_distance
def _assert_is_git_repository(self):
"""Assert the script is being run from a git repository. This is
necessary to build the tree.
Raises:
GitError: If the script is not being run from a git repository.
"""
try:
subprocess.check_output(['git', 'branch'], stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
raise GitError('Not a git repository: \'.\'')
def _get_branch_heads(self):
"""Retrieves a dict containing mappings for all local branches.
Returns:
dict mapping all (local) branch names to their HEADs.
Raises:
GitError: If git show-ref output could not be parsed.
"""
showref_output = subprocess.check_output(
['git', 'show-ref', '--heads',]).strip().split('\n')
heads = {}
for line in showref_output:
parts = line.split(' ')
if len(parts) != 2:
raise GitError('Poorly formatted show-ref output found at "%s".'
% (line))
head_hash = parts[0]
head_ref = parts[1]
if head_ref.startswith('refs/heads/'):
branch_name = head_ref[len('refs/heads/'):]
heads[branch_name] = head_hash
return heads
def branch_exists(self, branch_name):
"""Determines whether or not a named branch exists locally.
Args:
branch_name: string name of local branch.
Returns:
bool: True if the branch exists locally, False otherwise.
"""
return branch_name in self._heads
def get_branch_head(self, branch_name):
"""Retrieves the HEAD commit of a branch.
Args:
branch_name: string name of local branch.
Returns:
string: hash of branch HEAD.
Raises:
GitError: If the named branch does not exist locally.
"""
try:
return self._heads[branch_name]
except KeyError:
raise GitError('Branch %s does not exist.' % (branch_name))
def add(self, start_hash):
"""Reads all local history starting from start_hash into internal tree.
Reads at most LOOKBACK_THRESHOLD commits. This must be sufficient to
cover the lifetime of the relevant historical action we are interested
in. Updates internal data structures.
Args:
start_hash: string hash to start reading from.
Raises:
GitError: If git rev-list output could not be parsed.
"""
self._leaves.append(start_hash)
command = ['git', 'rev-list']
if self._lookback > 0:
command += ['-n', str(self._lookback)]
command += ['--full-history', '--sparse', '--parents', '--date-order',
'--format=%cd%x00%an%x00%ae%x00%s%n', start_hash]
revlist_output = subprocess.check_output(command).strip()
revlist_entries = revlist_output.split('\n\n')
for entry in revlist_entries:
lines = entry.split('\n')
if len(lines) != 2:
raise GitError('Poorly formatted rev-list output encountered '
'at "%s".' % (entry))
hashes = lines[0].split(' ')
descriptions = lines[1].split('\0')
if len(hashes) < 2 or len(descriptions) != 4:
raise GitError('Poorly formatted rev-list output encountered '
'at "%s".' % (entry))
sha = hashes[1]
parent_hashes = hashes[2:]
commit_date = descriptions[0]
author_name = descriptions[1]
author_email = descriptions[2]
subject = descriptions[3]
self._tree[sha] = Commit(sha, parent_hashes, [], commit_date,
author_name, author_email, subject)
self._update_root(revlist_entries[-1].split('\n')[0].split(' ')[1])
self._update_children()
def _update_root(self, new_root):
"""Compares new_root with the existing _root commit and updates _root
with the true root commit.
Updates internal data structures.
Args:
new_root: string hash of root commit for consideration.
Raises:
TreeError: If neither root commit appears more 'correct' than the
other (either both good or both bad). This will only happen if
the new root exists on a sub-tree not covered by the original
root (in which case we don't have much hope of success at all).
"""
if not new_root:
return
if not self._root:
self._root = new_root
return
if new_root == self._root:
return
# Does either candidate have any parent commits in the tree?
old_apc = any([p in self._tree for p in self._tree[self._root].parents])
new_apc = any([p in self._tree for p in self._tree[new_root].parents])
if old_apc and not new_apc:
self._root = new_root
return
if new_apc and not old_apc:
return
raise TreeError('History was found to be cyclic or non-linear while '
'attempting to rebuild tree root. This may mean the '
'lookback distance (%d) is insufficient to cover the '
'relevant history.' % (self._lookback))
def _update_children(self):
"""Updates all commit children attribute.
This attribute is not set from rev-list and must be manually inferred.
Updates internal data structures.
"""
# Clear all children.
for commit in self._tree.values():
commit.children = []
# Write all children.
for commit in self._tree.values():
for parent in commit.parents:
try:
self._tree[parent].children.append(commit.hash)
except KeyError:
pass
def get_commit(self, sha):
"""Retrieves the Commit identified by the given sha.
Args:
sha: string hash for the commit to retrieve.
Returns:
Commit: Identified commit, or None if no such commit exists.
"""
try:
return self._tree[sha]
except KeyError:
return None
def get_commit_parent(self, commit, parent_num=0):
"""Retrieves the commit's given parent commit.
Args:
commit: Commit object for the commit whose parents to retrieve.
parent_num: 0-indexed number of parent to request.
Returns:
Commit: Commit object of the requested parent.
Raises:
TreeError: If the given commit does not exist.
IndexError: If the given commit does not have enough parents.
"""
try:
return self._tree[commit.parents[parent_num]]
except IndexError:
raise IndexError('Failed to get parent %d - commit %s only has %d '
'parents.' % (parent_num, commit.hash,
len(commit.parents)))
except KeyError:
raise TreeError('Cannot find parent commit of %s. This is '
'typically caused by the lookback distance '
'(%d) being insufficient to cover the relevant '
'history.' % (commit.hash, self._lookback))
def _walk_tree(self, sha):
"""Walks down the first-parent tree from sha making a list of visits.
Args:
sha: string hash of the commit to start the walk from.
Returns:
list of Commit: commit objects of all visited commits.
Raises:
TreeError: If the start commit does not exist.
"""
visits = []
try:
commit = self._tree[sha]
except KeyError:
raise TreeError('Cannot start walk from non-existant commit %s. '
'This is typically caused by the lookback distance '
'(%d) being insufficient to cover the relevant '
'history.' % (sha, self._lookback))
while commit:
visits.append(commit)
try:
commit = self._tree[commit.parents[0]]
except (KeyError, IndexError):
commit = None
return visits
def find_fork_commit(self, sha_a, sha_b):
"""Retrieves the oldest ancestor commit between two commit histories.
This takes the linearised history given by the trees starting at leaves
sha_a (tree A) and sha_b (tree B).
Args:
sha_a: string hash of the first leaf commit to list history from.
sha_b: string hash of the second leaf commit to list history from.
Returns:
Commit: Commit object of the oldest common ancestor (point before
tree divergence) which exists in A and B.
list of Commit: Commit objects for all commits in A but not in B.
list of Commit: Commit objects for all commits in B but not in A.
Raises:
TreeError: If the histories have already diverged before the start
of the internal tree, or if A and B have never diverged (i.e.
are identical).
"""
# Walk down the first-parent tree from both start locations.
visits_a = self._walk_tree(sha_a)
visits_b = self._walk_tree(sha_b)
# We want to know the commit before the earliest difference in the lists
# (which will by definition be shared).
if visits_a[-1] != visits_b[-1]:
raise TreeError('Cannot find a reference point to start looking '
'for oldest ancestory from. This is typically '
'caused by the lookback distance (%d) being '
'insufficient to cover the branch\'s lifetime.'
% (self._lookback))
ancestor_i = None
for i in range(-2, -min(len(visits_a), len(visits_b)) - 1, -1):
if visits_a[i] != visits_b[i]:
ancestor_i = i+1
break
if ancestor_i is None:
raise TreeError('Histories for %s and %s are identical.'
% (sha_a, sha_b))
#print 'Found first difference @ ' + visits_a[ancestor_i].hash
#print 'A (5 either side):'
#print ' ' + '\n '.join([c.to_string(False) for c in visits_a[ancestor_i-5:ancestor_i+5]])
#print ''
#print 'B (5 either side):'
#print ' ' + '\n '.join([c.to_string(False) for c in visits_b[ancestor_i-5:ancestor_i+5]])
#print ''
a_diffs = [a for a in visits_a if a not in visits_b]
b_diffs = [b for b in visits_b if b not in visits_a]
return visits_a[ancestor_i], a_diffs, b_diffs
def find_merge_commit(self, merged_branch, start_sha, merge_regex):
"""Retrieves the commit at which a branch was merged.
Args:
merged_branch: string name of the merged branch to find.
start_sha: string hash of the commit to start searching from.
merge_regex: re.RegexObject object to apply to all merge commit
subject lines to find the appropriate merge commit. The first
commit which matches will be returned.
Returns:
Commit: Commit object of the merge commit.
Raises:
TreeError: If start_sha does not exist or no matching merge commit
could be found.
"""
try:
commit = self._tree[start_sha]
except KeyError:
raise TreeError('Cannot find merge hash from non-existant commit '
'%s. This is typically caused by the lookback '
'distance (%d) being insufficient to cover the '
'relevant history.' % (start_sha, self._lookback))
while commit:
if len(commit.parents) > 1 and merge_regex.match(commit.subject):
return commit
try:
commit = self._tree[commit.parents[0]]
except (KeyError, IndexError):
commit = None
raise TreeError('Failed to find merge point of %s' % (merged_branch))
def __str__(self):
"""str cast operator."""
commits = []
commits.append('leaves: %s\nroot: %s' % (str(self._leaves),
str(self._root)))
to_walk = self._leaves
walked = []
while to_walk:
next_hash = to_walk.pop()
if next_hash in self._tree and next_hash not in walked:
next_commit = self._tree[next_hash]
commits.append(str(next_commit))
to_walk += next_commit.parents
walked.append(next_hash)
return '\n\n'.join(commits)
def print_summary(branch_a, branch_b, branch_b_exists, fork_commit,
merge_commit):
"""Prints a summary about the comparison.
Args:
branch_a: string name of the A branch (that being differed
to).
branch_b: string name of the B branch (that with differences
on).
branch_b_exists: bool, True if branch_b currently exists.
fork_commit: Commit object representing the fork point, or None
if the branches have not forked.
merge_commit: Commit object representing the merge point, or None
if the branches have not merged.
"""
print 'Summary:'
if branch_b_exists:
print ' %s still exists' % (branch_b)
else:
print ' %s no longer exists' % (branch_b)
if merge_commit:
print ' %s merged into %s at: %s' % (branch_b, branch_a,
merge_commit.hash)
if fork_commit:
print ' %s forked from %s at: %s' % (branch_b, branch_a,
fork_commit.hash)
def print_branch_diffs(branch_a, branch_b, b_commits, fork_commit, pretty=False,
exclude_regex=None):
"""Prints the differences between two branches.
Args:
branch_a: string name of the A branch (that being differed to).
branch_b: string name of the B branch (that with differences on).
b_commits: list of Commit objects representing those which appear
on branch B but not branch A.
fork_commit: Commit object representing the fork point.
pretty: bool, True to pretty print the commits, false otherwise.
exclude_regex: re.RegexObject object or None. If not None, any commits
whose subject lines match the regex will be excluded from the diff.
Useful to exclude merge commits onto a topic from the commit list to
get genuine commits only.
"""
print 'Commits made on %s but not %s:' % (branch_b, branch_a)
for commit in b_commits:
if commit == fork_commit:
break
if not exclude_regex or not exclude_regex.match(commit.subject):
print ' %s' % (commit.to_string(pretty))
def print_finger(branch_a, branch_b, b_commits, fork_commit,
exclude_regex=None):
"""Prints the list of authors fingered by the commit differences.
Args:
branch_a: string name of the A branch (that being differed to).
branch_b: string name of the B branch (that with differences on).
b_commits: list of Commit objects representing those which appear
on branch B but not branch A.
fork_commit: Commit object representing the fork point.
exclude_regex: re.RegexObject object or None. If not None, any commits
whose subject lines match the regex will be excluded from the
fingering. Useful to exclude merge commits onto a topic from the
fingered list to get genuine committers only.
"""
authors = []
for commit in b_commits:
if commit == fork_commit:
break
if not exclude_regex or not exclude_regex.match(commit.subject):
authors.append((commit.author_name, commit.author_email))
print 'Authors of commits on %s but not %s:' % (branch_b, branch_a)
for author in set(authors):
print ' %s %s <%s>' % (authors.count(author), author[0], author[1])
def print_lifetime_graph(repo, start_commit, end_commit, pretty=False):
"""Prints the topology graph between two commits.
Args:
repo: The Repo object to use when formatting the output.
start_commit: Commit object representing the start point of the graph.
end_commit: Commit object representing the end point of the graph.
pretty: bool, True to pretty print the commits, false otherwise.
"""
graph_lines = subprocess.check_output(
['git', 'log', '--graph', '--no-color', '--format=%x00%H', '%s~..%s'
% (start_commit.hash, end_commit.hash)]).strip().split('\n')
graph_parts = []
for line in graph_lines:
parts = line.split('\0', 1)
graph_parts.append((parts[0].strip(),
parts[1].strip() if len(parts) > 1 else ''))
max_len = max([len(p[0]) for p in graph_parts])
print 'Graph:'
for part in graph_parts:
commit = repo.get_commit(part[1])
print ' %s %s' % (part[0].ljust(max_len),
commit.to_string(pretty) if commit else part[1])
def main():
"""Main method."""
# Handle command line.
parser = argparse.ArgumentParser(description='Finds commits on branch B '
'which are not on branch A. This is able '
'to handle if B has already been merged '
'down to A. This command runs purely '
'locally and as such the branches to '
'compare should be checked out and up to '
'date before running. No state is changed '
'by running this.')
parser.add_argument('a',
type=str, metavar='BRANCH-A',
help='Branch A. This is the branch against which the '
'difference is taken. It must exist locally.')
parser.add_argument('b',
type=str, metavar='BRANCH-B',
help='Branch B. This is the branch whose differences '
'are recorded. It must either exist locally or have a '
'corresponding merge commit onto A within the lookback '
'distance. See --merge-pattern and --lookback for '
'details on identifying merge commits and setting the '
'lookback respectively.')
parser.add_argument('-b', '--both-ways',
dest='both_ways', action='store_true',
default=False,
help='Print not only the differences from B to A (the '
'default), but also the differences from A to B.')
parser.add_argument('-n', '--lookback',
type=int, metavar='NUMBER', nargs='?',
default=1000, const=0,
help='Sets the number of commits to consider in the '
'history. The lookback distance must cover the full '
'lifetime of the branch (i.e. to the fork point). May '
'be set to 0 to consider all history (on large '
'repositorys this may take some time). Defaults to '
'1000.')
parser.add_argument('-e', '--exclude-updates',
dest='exclude_updates', action='store_true',
default=False,
help='Exclude update commits (merges from A back to B) '
'from all differences. By default all differences are '
'considered.')
parser.add_argument('-m', '--merge-pattern',
type=str, metavar='PATTERN', nargs='?',
default='Merge branch', const='',
help='The stem merge commit pattern to identify the '
'merge commit from B to A. This is only necessary if '
'B does not exist. Defaults to the standard git merge '
'pattern "Merge branch". The merge commit\'s subject '
'must contain the pattern followed by the merged '
'branch name. It is matched with the following regex: '
'^PATTERN.*BRANCH_B.*$')
parser.add_argument('-u', '--update-pattern',
type=str, metavar='PATTERN', nargs='?',
default=None, const=None,
help='The stem merge commit pattern to identify any '
'\'update\' merge commits from A to B. This is only '
'necessary if using --exclude-updates and if this '
'pattern differs from --merge-pattern. Defaults to the '
'value given in --merge-pattern. All \'update\' merge '
'commit subjects must contain the pattern followed by '
'A\'s name followed by B\'s name. It is matched with '
'the following regex: ^PATTERN.*BRANCH_A.*$')
parser.add_argument('--loose-merge-pattern',
dest='loose_merge_pattern', action='store_true',
default=False,
help='Exclude the branchname from the merge and update '
'patterns (so they match just the pattern given). By '
'default the branchname is included.')
parser.add_argument('-p', '--pretty',
dest='pretty', action='store_true',
default=False,
help='Print a short hash and the subject for all '
'commits. By default just the full hash is printed.')
parser.add_argument('-s', '--summary',
dest='summary', action='store_true',
default=True,
help='Print a summary of the status of each branch and '
'their relationship. This is the default.')
parser.add_argument('-S', '--no-summary',
dest='summary', action='store_false',
default=False,
help='Do not print the summary list (see --summary).')
parser.add_argument('-c', '--commits',
dest='commits', action='store_true',
default=True,
help='Print a list of all commits which exist on '
'branch B but not branch A. This is the default.')
parser.add_argument('-C', '--no-commits',
dest='commits', action='store_false',
default=False,
help='Do not print the commit list (see --commits).')
parser.add_argument('-f', '--finger',
dest='finger', action='store_true',
default=False,
help='Print a list of all users who have made commits '
'on branch B.')
parser.add_argument('-F', '--no-finger',
dest='finger', action='store_false',
default=True,
help='Do not print the finger list (see --finger). '
'This is the default.')
parser.add_argument('-g', '--graph',
dest='graph', action='store_true',
default=False,
help='Print a chronological graph of the commits made '
'to branches A and B during their lifetime. This only '
'has an effect if branch B has been merged into branch '
'A, otherwise ignored. Corresponds to the --graph '
'option of git log.')
parser.add_argument('-G', '--no-graph',
dest='graph', action='store_false',
default=True,
help='Do not print the commit graph (see --graph). '
'This is the default.')
args = parser.parse_args()
# Post-process command line.
if args.loose_merge_pattern:
merge_re = re.compile('^%s' % (args.merge_pattern))
update_re = re.compile('^%s' %
(args.update_pattern if args.update_pattern is
not None else args.merge_pattern)) \
if args.exclude_updates else None
else:
merge_re = re.compile('^%s.*%s' % (args.merge_pattern, args.b))
update_re = re.compile('^%s.*%s' %
(args.update_pattern if args.update_pattern is
not None else args.merge_pattern, args.a)) \
if args.exclude_updates else None
# Just do it.
try:
repo = Repo(args.lookback)
a_exists = repo.branch_exists(args.a)
b_exists = repo.branch_exists(args.b)
# Check we can proceed.
if not a_exists:
raise GitError('Branch A does not exist. Branch A must always '
'exist.')
# Parse the history for the requested branches.
repo.add(repo.get_branch_head(args.a))
if b_exists:
repo.add(repo.get_branch_head(args.b))
# Locate the relevant information in the history.
if b_exists:
# Both branches exist.
merge_commit = None
a_start = repo.get_branch_head(args.a)
b_start = repo.get_branch_head(args.b)
fork_commit, a_commits, b_commits = repo.find_fork_commit(a_start,
b_start)
else:
# Only branch A exists.
a_start = repo.get_branch_head(args.a)
merge_commit = repo.find_merge_commit(args.b, a_start, merge_re)
a_start = repo.get_commit_parent(merge_commit, 0).hash
b_start = repo.get_commit_parent(merge_commit, 1).hash
fork_commit, a_commits, b_commits = repo.find_fork_commit(a_start,
b_start)
# Print the requested information.
if args.summary:
print_summary(args.a, args.b, b_exists, fork_commit, merge_commit)
if args.commits:
print ''
print_branch_diffs(args.a, args.b, b_commits, fork_commit,
args.pretty, update_re)
if args.both_ways:
print ''
print_branch_diffs(args.b, args.a, a_commits, fork_commit,
args.pretty, update_re)
if args.finger:
print ''
print_finger(args.a, args.b, b_commits, fork_commit, update_re)
if args.both_ways:
print ''
print_finger(args.b, args.a, a_commits, fork_commit, update_re)
if not b_exists and args.graph:
print ''
print_lifetime_graph(repo, fork_commit, merge_commit, args.pretty)
except (GitError, TreeError), ex:
print 'ERR:', ex
sys.exit(1)
# Entry point.
if __name__ == "__main__":
main()