3 import sys
, math
, random
, os
, re
, signal
, tempfile
, stat
, errno
, traceback
4 from heapq
import heappush
, heappop
7 sys
.path
.append('''@@GIT_PYTHON_PATH@@''')
8 from gitMergeCommon
import *
10 originalIndexFile
= os
.environ
.get('GIT_INDEX_FILE',
11 os
.environ
.get('GIT_DIR', '.git') + '/index')
12 temporaryIndexFile
= os
.environ
.get('GIT_DIR', '.git') + \
13 '/merge-recursive-tmp-index'
14 def setupIndex(temporary
):
16 os
.unlink(temporaryIndexFile
)
20 newIndex
= temporaryIndexFile
22 newIndex
= originalIndexFile
23 os
.environ
['GIT_INDEX_FILE'] = newIndex
25 # This is a global variable which is used in a number of places but
26 # only written to in the 'merge' function.
28 # cacheOnly == True => Don't leave any non-stage 0 entries in the cache and
29 # don't update the working directory.
30 # False => Leave unmerged entries in the cache and update
31 # the working directory.
35 # The entry point to the merge code
36 # ---------------------------------
38 def merge(h1
, h2
, branch1Name
, branch2Name
, graph
, callDepth
=0):
39 '''Merge the commits h1 and h2, return the resulting virtual
40 commit object and a flag indicating the cleaness of the merge.'''
41 assert(isinstance(h1
, Commit
) and isinstance(h2
, Commit
))
42 assert(isinstance(graph
, Graph
))
45 sys
.stdout
.write(' '*callDepth
)
53 ca
= getCommonAncestors(graph
, h1
, h2
)
54 infoMsg('found', len(ca
), 'common ancestor(s):')
61 [mergedCA
, dummy
] = merge(mergedCA
, h
,
62 'Temporary shared merge branch 1',
63 'Temporary shared merge branch 2',
65 assert(isinstance(mergedCA
, Commit
))
73 runProgram(['git-read-tree', h1
.tree()])
76 [shaRes
, clean
] = mergeTrees(h1
.tree(), h2
.tree(), mergedCA
.tree(),
77 branch1Name
, branch2Name
)
79 if clean
or cacheOnly
:
80 res
= Commit(None, [h1
, h2
], tree
=shaRes
)
87 getFilesRE
= re
.compile(r
'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re
.S
)
88 def getFilesAndDirs(tree
):
91 out
= runProgram(['git-ls-tree', '-r', '-z', tree
])
92 for l
in out
.split('\0'):
93 m
= getFilesRE
.match(l
)
95 if m
.group(2) == 'tree':
97 elif m
.group(2) == 'blob':
102 # Those two global variables are used in a number of places but only
103 # written to in 'mergeTrees' and 'uniquePath'. They keep track of
104 # every file and directory in the two branches that are about to be
106 currentFileSet
= None
107 currentDirectorySet
= None
109 def mergeTrees(head
, merge
, common
, branch1Name
, branch2Name
):
110 '''Merge the trees 'head' and 'merge' with the common ancestor
111 'common'. The name of the head branch is 'branch1Name' and the name of
112 the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
113 where tree is the resulting tree and cleanMerge is True iff the
116 assert(isSha(head
) and isSha(merge
) and isSha(common
))
119 print 'Already uptodate!'
127 [out
, code
] = runProgram(['git-read-tree', updateArg
, '-m',
128 common
, head
, merge
], returnCode
= True)
130 die('git-read-tree:', out
)
132 [tree
, code
] = runProgram('git-write-tree', returnCode
=True)
135 global currentFileSet
, currentDirectorySet
136 [currentFileSet
, currentDirectorySet
] = getFilesAndDirs(head
)
137 [filesM
, dirsM
] = getFilesAndDirs(merge
)
138 currentFileSet
.union_update(filesM
)
139 currentDirectorySet
.union_update(dirsM
)
141 entries
= unmergedCacheEntries()
142 renamesHead
= getRenames(head
, common
, head
, merge
, entries
)
143 renamesMerge
= getRenames(merge
, common
, head
, merge
, entries
)
145 cleanMerge
= processRenames(renamesHead
, renamesMerge
,
146 branch1Name
, branch2Name
)
147 for entry
in entries
:
150 if not processEntry(entry
, branch1Name
, branch2Name
):
153 if cleanMerge
or cacheOnly
:
154 tree
= runProgram('git-write-tree').rstrip()
160 return [tree
, cleanMerge
]
162 # Low level file merging, update and removal
163 # ------------------------------------------
165 def mergeFile(oPath
, oSha
, oMode
, aPath
, aSha
, aMode
, bPath
, bSha
, bMode
,
166 branch1Name
, branch2Name
):
171 if stat
.S_IFMT(aMode
) != stat
.S_IFMT(bMode
):
173 if stat
.S_ISREG(aMode
):
180 if aSha
!= oSha
and bSha
!= oSha
:
192 elif stat
.S_ISREG(aMode
):
193 assert(stat
.S_ISREG(bMode
))
195 orig
= runProgram(['git-unpack-file', oSha
]).rstrip()
196 src1
= runProgram(['git-unpack-file', aSha
]).rstrip()
197 src2
= runProgram(['git-unpack-file', bSha
]).rstrip()
198 [out
, code
] = runProgram(['merge',
199 '-L', branch1Name
+ '/' + aPath
,
200 '-L', 'orig/' + oPath
,
201 '-L', branch2Name
+ '/' + bPath
,
202 src1
, orig
, src2
], returnCode
=True)
204 sha
= runProgram(['git-hash-object', '-t', 'blob', '-w',
213 assert(stat
.S_ISLNK(aMode
) and stat
.S_ISLNK(bMode
))
219 return [sha
, mode
, clean
, merge
]
221 def updateFile(clean
, sha
, mode
, path
):
222 updateCache
= cacheOnly
or clean
223 updateWd
= not cacheOnly
225 return updateFileExt(sha
, mode
, path
, updateCache
, updateWd
)
227 def updateFileExt(sha
, mode
, path
, updateCache
, updateWd
):
232 pathComponents
= path
.split('/')
233 for x
in xrange(1, len(pathComponents
)):
234 p
= '/'.join(pathComponents
[0:x
])
237 createDir
= not stat
.S_ISDIR(os
.lstat(p
).st_mode
)
245 die("Couldn't create directory", p
, e
.strerror
)
247 prog
= ['git-cat-file', 'blob', sha
]
248 if stat
.S_ISREG(mode
):
257 fd
= os
.open(path
, os
.O_WRONLY | os
.O_TRUNC | os
.O_CREAT
, mode
)
258 proc
= subprocess
.Popen(prog
, stdout
=fd
)
261 elif stat
.S_ISLNK(mode
):
262 linkTarget
= runProgram(prog
)
263 os
.symlink(linkTarget
, path
)
267 if updateWd
and updateCache
:
268 runProgram(['git-update-index', '--add', '--', path
])
270 runProgram(['git-update-index', '--add', '--cacheinfo',
271 '0%o' % mode
, sha
, path
])
273 def removeFile(clean
, path
):
274 updateCache
= cacheOnly
or clean
275 updateWd
= not cacheOnly
278 runProgram(['git-update-index', '--force-remove', '--', path
])
284 if e
.errno
!= errno
.ENOENT
and e
.errno
!= errno
.EISDIR
:
287 def uniquePath(path
, branch
):
288 def fileExists(path
):
293 if e
.errno
== errno
.ENOENT
:
298 newPath
= path
+ '_' + branch
300 while newPath
in currentFileSet
or \
301 newPath
in currentDirectorySet
or \
304 newPath
= path
+ '_' + branch
+ '_' + str(suffix
)
305 currentFileSet
.add(newPath
)
308 # Cache entry management
309 # ----------------------
312 def __init__(self
, path
):
318 # Used for debugging only
320 if self
.mode
!= None:
321 m
= '0%o' % self
.mode
329 return 'sha1: ' + sha1
+ ' mode: ' + m
331 self
.stages
= [Stage(), Stage(), Stage(), Stage()]
333 self
.processed
= False
336 return 'path: ' + self
.path
+ ' stages: ' + repr([str(x
) for x
in self
.stages
])
338 class CacheEntryContainer
:
342 def add(self
, entry
):
343 self
.entries
[entry
.path
] = entry
346 return self
.entries
.get(path
)
349 return self
.entries
.itervalues()
351 unmergedRE
= re
.compile(r
'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re
.S
)
352 def unmergedCacheEntries():
353 '''Create a dictionary mapping file names to CacheEntry
354 objects. The dictionary contains one entry for every path with a
355 non-zero stage entry.'''
357 lines
= runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
360 res
= CacheEntryContainer()
362 m
= unmergedRE
.match(l
)
364 mode
= int(m
.group(1), 8)
366 stage
= int(m
.group(3))
374 e
.stages
[stage
].mode
= mode
375 e
.stages
[stage
].sha1
= sha1
377 die('Error: Merge program failed: Unexpected output from',
381 lsTreeRE
= re
.compile(r
'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re
.S
)
382 def getCacheEntry(path
, origTree
, aTree
, bTree
):
383 '''Returns a CacheEntry object which doesn't have to correspond to
384 a real cache entry in Git's index.'''
390 m
= lsTreeRE
.match(out
)
392 die('Unexpected output from git-ls-tree:', out
)
393 elif m
.group(2) == 'blob':
394 return [m
.group(3), int(m
.group(1), 8)]
398 res
= CacheEntry(path
)
400 [oSha
, oMode
] = parse(runProgram(['git-ls-tree', origTree
, '--', path
]))
401 [aSha
, aMode
] = parse(runProgram(['git-ls-tree', aTree
, '--', path
]))
402 [bSha
, bMode
] = parse(runProgram(['git-ls-tree', bTree
, '--', path
]))
404 res
.stages
[1].sha1
= oSha
405 res
.stages
[1].mode
= oMode
406 res
.stages
[2].sha1
= aSha
407 res
.stages
[2].mode
= aMode
408 res
.stages
[3].sha1
= bSha
409 res
.stages
[3].mode
= bMode
413 # Rename detection and handling
414 # -----------------------------
418 src
, srcSha
, srcMode
, srcCacheEntry
,
419 dst
, dstSha
, dstMode
, dstCacheEntry
,
423 self
.srcMode
= srcMode
424 self
.srcCacheEntry
= srcCacheEntry
427 self
.dstMode
= dstMode
428 self
.dstCacheEntry
= dstCacheEntry
431 self
.processed
= False
433 class RenameEntryContainer
:
438 def add(self
, entry
):
439 self
.entriesSrc
[entry
.srcName
] = entry
440 self
.entriesDst
[entry
.dstName
] = entry
442 def getSrc(self
, path
):
443 return self
.entriesSrc
.get(path
)
445 def getDst(self
, path
):
446 return self
.entriesDst
.get(path
)
449 return self
.entriesSrc
.itervalues()
451 parseDiffRenamesRE
= re
.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
452 def getRenames(tree
, oTree
, aTree
, bTree
, cacheEntries
):
453 '''Get information of all renames which occured between 'oTree' and
454 'tree'. We need the three trees in the merge ('oTree', 'aTree' and
455 'bTree') to be able to associate the correct cache entries with
456 the rename information. 'tree' is always equal to either aTree or bTree.'''
458 assert(tree
== aTree
or tree
== bTree
)
459 inp
= runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
462 ret
= RenameEntryContainer()
464 recs
= inp
.split("\0")
465 recs
.pop() # remove last entry (which is '')
469 m
= parseDiffRenamesRE
.match(rec
)
472 die('Unexpected output from git-diff-tree:', rec
)
474 srcMode
= int(m
.group(1), 8)
475 dstMode
= int(m
.group(2), 8)
482 srcCacheEntry
= cacheEntries
.get(src
)
483 if not srcCacheEntry
:
484 srcCacheEntry
= getCacheEntry(src
, oTree
, aTree
, bTree
)
485 cacheEntries
.add(srcCacheEntry
)
487 dstCacheEntry
= cacheEntries
.get(dst
)
488 if not dstCacheEntry
:
489 dstCacheEntry
= getCacheEntry(dst
, oTree
, aTree
, bTree
)
490 cacheEntries
.add(dstCacheEntry
)
492 ret
.add(RenameEntry(src
, srcSha
, srcMode
, srcCacheEntry
,
493 dst
, dstSha
, dstMode
, dstCacheEntry
,
495 except StopIteration:
499 def fmtRename(src
, dst
):
500 srcPath
= src
.split('/')
501 dstPath
= dst
.split('/')
503 endIndex
= min(len(srcPath
), len(dstPath
)) - 1
504 for x
in range(0, endIndex
):
505 if srcPath
[x
] == dstPath
[x
]:
506 path
.append(srcPath
[x
])
512 return '/'.join(path
) + \
513 '/{' + '/'.join(srcPath
[endIndex
:]) + ' => ' + \
514 '/'.join(dstPath
[endIndex
:]) + '}'
516 return src
+ ' => ' + dst
518 def processRenames(renamesA
, renamesB
, branchNameA
, branchNameB
):
521 srcNames
.add(x
.srcName
)
523 srcNames
.add(x
.srcName
)
526 for path
in srcNames
:
527 if renamesA
.getSrc(path
):
530 branchName1
= branchNameA
531 branchName2
= branchNameB
535 branchName1
= branchNameB
536 branchName2
= branchNameA
538 ren1
= renames1
.getSrc(path
)
539 ren2
= renames2
.getSrc(path
)
541 ren1
.dstCacheEntry
.processed
= True
542 ren1
.srcCacheEntry
.processed
= True
547 ren1
.processed
= True
548 removeFile(True, ren1
.srcName
)
550 # Renamed in 1 and renamed in 2
551 assert(ren1
.srcName
== ren2
.srcName
)
552 ren2
.dstCacheEntry
.processed
= True
553 ren2
.processed
= True
555 if ren1
.dstName
!= ren2
.dstName
:
556 print 'CONFLICT (rename/rename): Rename', \
557 fmtRename(path
, ren1
.dstName
), 'in branch', branchName1
, \
558 'rename', fmtRename(path
, ren2
.dstName
), 'in', branchName2
561 if ren1
.dstName
in currentDirectorySet
:
562 dstName1
= uniquePath(ren1
.dstName
, branchName1
)
563 print ren1
.dstName
, 'is a directory in', branchName2
, \
564 'adding as', dstName1
, 'instead.'
565 removeFile(False, ren1
.dstName
)
567 dstName1
= ren1
.dstName
569 if ren2
.dstName
in currentDirectorySet
:
570 dstName2
= uniquePath(ren2
.dstName
, branchName2
)
571 print ren2
.dstName
, 'is a directory in', branchName1
, \
572 'adding as', dstName2
, 'instead.'
573 removeFile(False, ren2
.dstName
)
575 dstName2
= ren1
.dstName
577 updateFile(False, ren1
.dstSha
, ren1
.dstMode
, dstName1
)
578 updateFile(False, ren2
.dstSha
, ren2
.dstMode
, dstName2
)
580 [resSha
, resMode
, clean
, merge
] = \
581 mergeFile(ren1
.srcName
, ren1
.srcSha
, ren1
.srcMode
,
582 ren1
.dstName
, ren1
.dstSha
, ren1
.dstMode
,
583 ren2
.dstName
, ren2
.dstSha
, ren2
.dstMode
,
584 branchName1
, branchName2
)
586 if merge
or not clean
:
587 print 'Renaming', fmtRename(path
, ren1
.dstName
)
590 print 'Auto-merging', ren1
.dstName
593 print 'CONFLICT (content): merge conflict in', ren1
.dstName
597 updateFileExt(ren1
.dstSha
, ren1
.dstMode
, ren1
.dstName
,
598 updateCache
=True, updateWd
=False)
599 updateFile(clean
, resSha
, resMode
, ren1
.dstName
)
601 # Renamed in 1, maybe changed in 2
602 if renamesA
== renames1
:
607 srcShaOtherBranch
= ren1
.srcCacheEntry
.stages
[stage
].sha1
608 srcModeOtherBranch
= ren1
.srcCacheEntry
.stages
[stage
].mode
610 dstShaOtherBranch
= ren1
.dstCacheEntry
.stages
[stage
].sha1
611 dstModeOtherBranch
= ren1
.dstCacheEntry
.stages
[stage
].mode
615 if ren1
.dstName
in currentDirectorySet
:
616 newPath
= uniquePath(ren1
.dstName
, branchName1
)
617 print 'CONFLICT (rename/directory): Rename', \
618 fmtRename(ren1
.srcName
, ren1
.dstName
), 'in', branchName1
,\
619 'directory', ren1
.dstName
, 'added in', branchName2
620 print 'Renaming', ren1
.srcName
, 'to', newPath
, 'instead'
622 removeFile(False, ren1
.dstName
)
623 updateFile(False, ren1
.dstSha
, ren1
.dstMode
, newPath
)
624 elif srcShaOtherBranch
== None:
625 print 'CONFLICT (rename/delete): Rename', \
626 fmtRename(ren1
.srcName
, ren1
.dstName
), 'in', \
627 branchName1
, 'and deleted in', branchName2
629 updateFile(False, ren1
.dstSha
, ren1
.dstMode
, ren1
.dstName
)
630 elif dstShaOtherBranch
:
631 newPath
= uniquePath(ren1
.dstName
, branchName2
)
632 print 'CONFLICT (rename/add): Rename', \
633 fmtRename(ren1
.srcName
, ren1
.dstName
), 'in', \
634 branchName1
+ '.', ren1
.dstName
, 'added in', branchName2
635 print 'Adding as', newPath
, 'instead'
636 updateFile(False, dstShaOtherBranch
, dstModeOtherBranch
, newPath
)
639 elif renames2
.getDst(ren1
.dstName
):
640 dst2
= renames2
.getDst(ren1
.dstName
)
641 newPath1
= uniquePath(ren1
.dstName
, branchName1
)
642 newPath2
= uniquePath(dst2
.dstName
, branchName2
)
643 print 'CONFLICT (rename/rename): Rename', \
644 fmtRename(ren1
.srcName
, ren1
.dstName
), 'in', \
645 branchName1
+'. Rename', \
646 fmtRename(dst2
.srcName
, dst2
.dstName
), 'in', branchName2
647 print 'Renaming', ren1
.srcName
, 'to', newPath1
, 'and', \
648 dst2
.srcName
, 'to', newPath2
, 'instead'
649 removeFile(False, ren1
.dstName
)
650 updateFile(False, ren1
.dstSha
, ren1
.dstMode
, newPath1
)
651 updateFile(False, dst2
.dstSha
, dst2
.dstMode
, newPath2
)
652 dst2
.processed
= True
658 [resSha
, resMode
, clean
, merge
] = \
659 mergeFile(ren1
.srcName
, ren1
.srcSha
, ren1
.srcMode
,
660 ren1
.dstName
, ren1
.dstSha
, ren1
.dstMode
,
661 ren1
.srcName
, srcShaOtherBranch
, srcModeOtherBranch
,
662 branchName1
, branchName2
)
664 if merge
or not clean
:
665 print 'Renaming', fmtRename(ren1
.srcName
, ren1
.dstName
)
668 print 'Auto-merging', ren1
.dstName
671 print 'CONFLICT (rename/modify): Merge conflict in', ren1
.dstName
675 updateFileExt(ren1
.dstSha
, ren1
.dstMode
, ren1
.dstName
,
676 updateCache
=True, updateWd
=False)
677 updateFile(clean
, resSha
, resMode
, ren1
.dstName
)
681 # Per entry merge function
682 # ------------------------
684 def processEntry(entry
, branch1Name
, branch2Name
):
685 '''Merge one cache entry.'''
687 debug('processing', entry
.path
, 'clean cache:', cacheOnly
)
692 oSha
= entry
.stages
[1].sha1
693 oMode
= entry
.stages
[1].mode
694 aSha
= entry
.stages
[2].sha1
695 aMode
= entry
.stages
[2].mode
696 bSha
= entry
.stages
[3].sha1
697 bMode
= entry
.stages
[3].mode
699 assert(oSha
== None or isSha(oSha
))
700 assert(aSha
== None or isSha(aSha
))
701 assert(bSha
== None or isSha(bSha
))
703 assert(oMode
== None or type(oMode
) is int)
704 assert(aMode
== None or type(aMode
) is int)
705 assert(bMode
== None or type(bMode
) is int)
707 if (oSha
and (not aSha
or not bSha
)):
709 # Case A: Deleted in one
711 if (not aSha
and not bSha
) or \
712 (aSha
== oSha
and not bSha
) or \
713 (not aSha
and bSha
== oSha
):
714 # Deleted in both or deleted in one and unchanged in the other
716 print 'Removing', path
717 removeFile(True, path
)
719 # Deleted in one and changed in the other
722 print 'CONFLICT (delete/modify):', path
, 'deleted in', \
723 branch1Name
, 'and modified in', branch2Name
+ '.', \
724 'Version', branch2Name
, 'of', path
, 'left in tree.'
728 print 'CONFLICT (modify/delete):', path
, 'deleted in', \
729 branch2Name
, 'and modified in', branch1Name
+ '.', \
730 'Version', branch1Name
, 'of', path
, 'left in tree.'
734 updateFile(False, sha
, mode
, path
)
736 elif (not oSha
and aSha
and not bSha
) or \
737 (not oSha
and not aSha
and bSha
):
739 # Case B: Added in one.
742 addBranch
= branch1Name
743 otherBranch
= branch2Name
746 conf
= 'file/directory'
748 addBranch
= branch2Name
749 otherBranch
= branch1Name
752 conf
= 'directory/file'
754 if path
in currentDirectorySet
:
756 newPath
= uniquePath(path
, addBranch
)
757 print 'CONFLICT (' + conf
+ '):', \
758 'There is a directory with name', path
, 'in', \
759 otherBranch
+ '. Adding', path
, 'as', newPath
761 removeFile(False, path
)
762 updateFile(False, sha
, mode
, newPath
)
765 updateFile(True, sha
, mode
, path
)
767 elif not oSha
and aSha
and bSha
:
769 # Case C: Added in both (check for same permissions).
774 print 'CONFLICT: File', path
, \
775 'added identically in both branches, but permissions', \
776 'conflict', '0%o' % aMode
, '->', '0%o' % bMode
777 print 'CONFLICT: adding with permission:', '0%o' % aMode
779 updateFile(False, aSha
, aMode
, path
)
781 # This case is handled by git-read-tree
785 newPath1
= uniquePath(path
, branch1Name
)
786 newPath2
= uniquePath(path
, branch2Name
)
787 print 'CONFLICT (add/add): File', path
, \
788 'added non-identically in both branches. Adding as', \
789 newPath1
, 'and', newPath2
, 'instead.'
790 removeFile(False, path
)
791 updateFile(False, aSha
, aMode
, newPath1
)
792 updateFile(False, bSha
, bMode
, newPath2
)
794 elif oSha
and aSha
and bSha
:
796 # case D: Modified in both, but differently.
798 print 'Auto-merging', path
799 [sha
, mode
, clean
, dummy
] = \
800 mergeFile(path
, oSha
, oMode
,
803 branch1Name
, branch2Name
)
805 updateFile(True, sha
, mode
, path
)
808 print 'CONFLICT (content): Merge conflict in', path
811 updateFile(False, sha
, mode
, path
)
813 updateFileExt(aSha
, aMode
, path
,
814 updateCache
=True, updateWd
=False)
815 updateFileExt(sha
, mode
, path
, updateCache
=False, updateWd
=True)
817 die("ERROR: Fatal merge failure, shouldn't happen.")
822 die('Usage:', sys
.argv
[0], ' <base>... -- <head> <remote>..')
824 # main entry point as merge strategy module
825 # The first parameters up to -- are merge bases, and the rest are heads.
826 # This strategy module figures out merge bases itself, so we only
829 if len(sys
.argv
) < 4:
832 for nextArg
in xrange(1, len(sys
.argv
)):
833 if sys
.argv
[nextArg
] == '--':
834 if len(sys
.argv
) != nextArg
+ 3:
835 die('Not handling anything other than two heads merge.')
837 h1
= firstBranch
= sys
.argv
[nextArg
+ 1]
838 h2
= secondBranch
= sys
.argv
[nextArg
+ 2]
843 print 'Merging', h1
, 'with', h2
846 h1
= runProgram(['git-rev-parse', '--verify', h1
+ '^0']).rstrip()
847 h2
= runProgram(['git-rev-parse', '--verify', h2
+ '^0']).rstrip()
849 graph
= buildGraph([h1
, h2
])
851 [dummy
, clean
] = merge(graph
.shaMap
[h1
], graph
.shaMap
[h2
],
852 firstBranch
, secondBranch
, graph
)
856 if isinstance(sys
.exc_info()[1], SystemExit):
859 traceback
.print_exc(None, sys
.stderr
)