3 # Copyright (C) 2005 Fredrik Kuivinen
7 sys
.path
.append('''@@GIT_PYTHON_PATH@@''')
9 import math
, random
, os
, re
, signal
, tempfile
, stat
, errno
, traceback
10 from heapq
import heappush
, heappop
13 from gitMergeCommon
import *
17 sys
.stdout
.write(' '*outputIndent
)
20 originalIndexFile
= os
.environ
.get('GIT_INDEX_FILE',
21 os
.environ
.get('GIT_DIR', '.git') + '/index')
22 temporaryIndexFile
= os
.environ
.get('GIT_DIR', '.git') + \
23 '/merge-recursive-tmp-index'
24 def setupIndex(temporary
):
26 os
.unlink(temporaryIndexFile
)
30 newIndex
= temporaryIndexFile
32 newIndex
= originalIndexFile
33 os
.environ
['GIT_INDEX_FILE'] = newIndex
35 # This is a global variable which is used in a number of places but
36 # only written to in the 'merge' function.
38 # cacheOnly == True => Don't leave any non-stage 0 entries in the cache and
39 # don't update the working directory.
40 # False => Leave unmerged entries in the cache and update
41 # the working directory.
45 # The entry point to the merge code
46 # ---------------------------------
48 def merge(h1
, h2
, branch1Name
, branch2Name
, graph
, callDepth
=0):
49 '''Merge the commits h1 and h2, return the resulting virtual
50 commit object and a flag indicating the cleaness of the merge.'''
51 assert(isinstance(h1
, Commit
) and isinstance(h2
, Commit
))
52 assert(isinstance(graph
, Graph
))
61 ca
= getCommonAncestors(graph
, h1
, h2
)
62 output('found', len(ca
), 'common ancestor(s):')
69 outputIndent
= callDepth
+1
70 [mergedCA
, dummy
] = merge(mergedCA
, h
,
71 'Temporary merge branch 1',
72 'Temporary merge branch 2',
74 outputIndent
= callDepth
75 assert(isinstance(mergedCA
, Commit
))
83 runProgram(['git-read-tree', h1
.tree()])
86 [shaRes
, clean
] = mergeTrees(h1
.tree(), h2
.tree(), mergedCA
.tree(),
87 branch1Name
, branch2Name
)
89 if clean
or cacheOnly
:
90 res
= Commit(None, [h1
, h2
], tree
=shaRes
)
97 getFilesRE
= re
.compile(r
'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re
.S
)
98 def getFilesAndDirs(tree
):
101 out
= runProgram(['git-ls-tree', '-r', '-z', '-t', tree
])
102 for l
in out
.split('\0'):
103 m
= getFilesRE
.match(l
)
105 if m
.group(2) == 'tree':
107 elif m
.group(2) == 'blob':
108 files
.add(m
.group(4))
112 # Those two global variables are used in a number of places but only
113 # written to in 'mergeTrees' and 'uniquePath'. They keep track of
114 # every file and directory in the two branches that are about to be
116 currentFileSet
= None
117 currentDirectorySet
= None
119 def mergeTrees(head
, merge
, common
, branch1Name
, branch2Name
):
120 '''Merge the trees 'head' and 'merge' with the common ancestor
121 'common'. The name of the head branch is 'branch1Name' and the name of
122 the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
123 where tree is the resulting tree and cleanMerge is True iff the
126 assert(isSha(head
) and isSha(merge
) and isSha(common
))
129 output('Already uptodate!')
137 [out
, code
] = runProgram(['git-read-tree', updateArg
, '-m',
138 common
, head
, merge
], returnCode
= True)
140 die('git-read-tree:', out
)
142 [tree
, code
] = runProgram('git-write-tree', returnCode
=True)
145 global currentFileSet
, currentDirectorySet
146 [currentFileSet
, currentDirectorySet
] = getFilesAndDirs(head
)
147 [filesM
, dirsM
] = getFilesAndDirs(merge
)
148 currentFileSet
.union_update(filesM
)
149 currentDirectorySet
.union_update(dirsM
)
151 entries
= unmergedCacheEntries()
152 renamesHead
= getRenames(head
, common
, head
, merge
, entries
)
153 renamesMerge
= getRenames(merge
, common
, head
, merge
, entries
)
155 cleanMerge
= processRenames(renamesHead
, renamesMerge
,
156 branch1Name
, branch2Name
)
157 for entry
in entries
:
160 if not processEntry(entry
, branch1Name
, branch2Name
):
163 if cleanMerge
or cacheOnly
:
164 tree
= runProgram('git-write-tree').rstrip()
170 return [tree
, cleanMerge
]
172 # Low level file merging, update and removal
173 # ------------------------------------------
175 def mergeFile(oPath
, oSha
, oMode
, aPath
, aSha
, aMode
, bPath
, bSha
, bMode
,
176 branch1Name
, branch2Name
):
181 if stat
.S_IFMT(aMode
) != stat
.S_IFMT(bMode
):
183 if stat
.S_ISREG(aMode
):
190 if aSha
!= oSha
and bSha
!= oSha
:
202 elif stat
.S_ISREG(aMode
):
203 assert(stat
.S_ISREG(bMode
))
205 orig
= runProgram(['git-unpack-file', oSha
]).rstrip()
206 src1
= runProgram(['git-unpack-file', aSha
]).rstrip()
207 src2
= runProgram(['git-unpack-file', bSha
]).rstrip()
208 [out
, code
] = runProgram(['merge',
209 '-L', branch1Name
+ '/' + aPath
,
210 '-L', 'orig/' + oPath
,
211 '-L', branch2Name
+ '/' + bPath
,
212 src1
, orig
, src2
], returnCode
=True)
214 sha
= runProgram(['git-hash-object', '-t', 'blob', '-w',
223 assert(stat
.S_ISLNK(aMode
) and stat
.S_ISLNK(bMode
))
229 return [sha
, mode
, clean
, merge
]
231 def updateFile(clean
, sha
, mode
, path
):
232 updateCache
= cacheOnly
or clean
233 updateWd
= not cacheOnly
235 return updateFileExt(sha
, mode
, path
, updateCache
, updateWd
)
237 def updateFileExt(sha
, mode
, path
, updateCache
, updateWd
):
242 pathComponents
= path
.split('/')
243 for x
in xrange(1, len(pathComponents
)):
244 p
= '/'.join(pathComponents
[0:x
])
247 createDir
= not stat
.S_ISDIR(os
.lstat(p
).st_mode
)
255 die("Couldn't create directory", p
, e
.strerror
)
257 prog
= ['git-cat-file', 'blob', sha
]
258 if stat
.S_ISREG(mode
):
267 fd
= os
.open(path
, os
.O_WRONLY | os
.O_TRUNC | os
.O_CREAT
, mode
)
268 proc
= subprocess
.Popen(prog
, stdout
=fd
)
271 elif stat
.S_ISLNK(mode
):
272 linkTarget
= runProgram(prog
)
273 os
.symlink(linkTarget
, path
)
277 if updateWd
and updateCache
:
278 runProgram(['git-update-index', '--add', '--', path
])
280 runProgram(['git-update-index', '--add', '--cacheinfo',
281 '0%o' % mode
, sha
, path
])
283 def removeFile(clean
, path
):
284 updateCache
= cacheOnly
or clean
285 updateWd
= not cacheOnly
288 runProgram(['git-update-index', '--force-remove', '--', path
])
294 if e
.errno
!= errno
.ENOENT
and e
.errno
!= errno
.EISDIR
:
297 os
.removedirs(os
.path
.dirname(path
))
301 def uniquePath(path
, branch
):
302 def fileExists(path
):
307 if e
.errno
== errno
.ENOENT
:
312 branch
= branch
.replace('/', '_')
313 newPath
= path
+ '~' + branch
315 while newPath
in currentFileSet
or \
316 newPath
in currentDirectorySet
or \
319 newPath
= path
+ '~' + branch
+ '_' + str(suffix
)
320 currentFileSet
.add(newPath
)
323 # Cache entry management
324 # ----------------------
327 def __init__(self
, path
):
333 # Used for debugging only
335 if self
.mode
!= None:
336 m
= '0%o' % self
.mode
344 return 'sha1: ' + sha1
+ ' mode: ' + m
346 self
.stages
= [Stage(), Stage(), Stage(), Stage()]
348 self
.processed
= False
351 return 'path: ' + self
.path
+ ' stages: ' + repr([str(x
) for x
in self
.stages
])
353 class CacheEntryContainer
:
357 def add(self
, entry
):
358 self
.entries
[entry
.path
] = entry
361 return self
.entries
.get(path
)
364 return self
.entries
.itervalues()
366 unmergedRE
= re
.compile(r
'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re
.S
)
367 def unmergedCacheEntries():
368 '''Create a dictionary mapping file names to CacheEntry
369 objects. The dictionary contains one entry for every path with a
370 non-zero stage entry.'''
372 lines
= runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
375 res
= CacheEntryContainer()
377 m
= unmergedRE
.match(l
)
379 mode
= int(m
.group(1), 8)
381 stage
= int(m
.group(3))
389 e
.stages
[stage
].mode
= mode
390 e
.stages
[stage
].sha1
= sha1
392 die('Error: Merge program failed: Unexpected output from',
396 lsTreeRE
= re
.compile(r
'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re
.S
)
397 def getCacheEntry(path
, origTree
, aTree
, bTree
):
398 '''Returns a CacheEntry object which doesn't have to correspond to
399 a real cache entry in Git's index.'''
405 m
= lsTreeRE
.match(out
)
407 die('Unexpected output from git-ls-tree:', out
)
408 elif m
.group(2) == 'blob':
409 return [m
.group(3), int(m
.group(1), 8)]
413 res
= CacheEntry(path
)
415 [oSha
, oMode
] = parse(runProgram(['git-ls-tree', origTree
, '--', path
]))
416 [aSha
, aMode
] = parse(runProgram(['git-ls-tree', aTree
, '--', path
]))
417 [bSha
, bMode
] = parse(runProgram(['git-ls-tree', bTree
, '--', path
]))
419 res
.stages
[1].sha1
= oSha
420 res
.stages
[1].mode
= oMode
421 res
.stages
[2].sha1
= aSha
422 res
.stages
[2].mode
= aMode
423 res
.stages
[3].sha1
= bSha
424 res
.stages
[3].mode
= bMode
428 # Rename detection and handling
429 # -----------------------------
433 src
, srcSha
, srcMode
, srcCacheEntry
,
434 dst
, dstSha
, dstMode
, dstCacheEntry
,
438 self
.srcMode
= srcMode
439 self
.srcCacheEntry
= srcCacheEntry
442 self
.dstMode
= dstMode
443 self
.dstCacheEntry
= dstCacheEntry
446 self
.processed
= False
448 class RenameEntryContainer
:
453 def add(self
, entry
):
454 self
.entriesSrc
[entry
.srcName
] = entry
455 self
.entriesDst
[entry
.dstName
] = entry
457 def getSrc(self
, path
):
458 return self
.entriesSrc
.get(path
)
460 def getDst(self
, path
):
461 return self
.entriesDst
.get(path
)
464 return self
.entriesSrc
.itervalues()
466 parseDiffRenamesRE
= re
.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
467 def getRenames(tree
, oTree
, aTree
, bTree
, cacheEntries
):
468 '''Get information of all renames which occured between 'oTree' and
469 'tree'. We need the three trees in the merge ('oTree', 'aTree' and
470 'bTree') to be able to associate the correct cache entries with
471 the rename information. 'tree' is always equal to either aTree or bTree.'''
473 assert(tree
== aTree
or tree
== bTree
)
474 inp
= runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
477 ret
= RenameEntryContainer()
479 recs
= inp
.split("\0")
480 recs
.pop() # remove last entry (which is '')
484 m
= parseDiffRenamesRE
.match(rec
)
487 die('Unexpected output from git-diff-tree:', rec
)
489 srcMode
= int(m
.group(1), 8)
490 dstMode
= int(m
.group(2), 8)
497 srcCacheEntry
= cacheEntries
.get(src
)
498 if not srcCacheEntry
:
499 srcCacheEntry
= getCacheEntry(src
, oTree
, aTree
, bTree
)
500 cacheEntries
.add(srcCacheEntry
)
502 dstCacheEntry
= cacheEntries
.get(dst
)
503 if not dstCacheEntry
:
504 dstCacheEntry
= getCacheEntry(dst
, oTree
, aTree
, bTree
)
505 cacheEntries
.add(dstCacheEntry
)
507 ret
.add(RenameEntry(src
, srcSha
, srcMode
, srcCacheEntry
,
508 dst
, dstSha
, dstMode
, dstCacheEntry
,
510 except StopIteration:
514 def fmtRename(src
, dst
):
515 srcPath
= src
.split('/')
516 dstPath
= dst
.split('/')
518 endIndex
= min(len(srcPath
), len(dstPath
)) - 1
519 for x
in range(0, endIndex
):
520 if srcPath
[x
] == dstPath
[x
]:
521 path
.append(srcPath
[x
])
527 return '/'.join(path
) + \
528 '/{' + '/'.join(srcPath
[endIndex
:]) + ' => ' + \
529 '/'.join(dstPath
[endIndex
:]) + '}'
531 return src
+ ' => ' + dst
533 def processRenames(renamesA
, renamesB
, branchNameA
, branchNameB
):
536 srcNames
.add(x
.srcName
)
538 srcNames
.add(x
.srcName
)
541 for path
in srcNames
:
542 if renamesA
.getSrc(path
):
545 branchName1
= branchNameA
546 branchName2
= branchNameB
550 branchName1
= branchNameB
551 branchName2
= branchNameA
553 ren1
= renames1
.getSrc(path
)
554 ren2
= renames2
.getSrc(path
)
556 ren1
.dstCacheEntry
.processed
= True
557 ren1
.srcCacheEntry
.processed
= True
562 ren1
.processed
= True
563 removeFile(True, ren1
.srcName
)
565 # Renamed in 1 and renamed in 2
566 assert(ren1
.srcName
== ren2
.srcName
)
567 ren2
.dstCacheEntry
.processed
= True
568 ren2
.processed
= True
570 if ren1
.dstName
!= ren2
.dstName
:
571 output('CONFLICT (rename/rename): Rename',
572 fmtRename(path
, ren1
.dstName
), 'in branch', branchName1
,
573 'rename', fmtRename(path
, ren2
.dstName
), 'in',
577 if ren1
.dstName
in currentDirectorySet
:
578 dstName1
= uniquePath(ren1
.dstName
, branchName1
)
579 output(ren1
.dstName
, 'is a directory in', branchName2
,
580 'adding as', dstName1
, 'instead.')
581 removeFile(False, ren1
.dstName
)
583 dstName1
= ren1
.dstName
585 if ren2
.dstName
in currentDirectorySet
:
586 dstName2
= uniquePath(ren2
.dstName
, branchName2
)
587 output(ren2
.dstName
, 'is a directory in', branchName1
,
588 'adding as', dstName2
, 'instead.')
589 removeFile(False, ren2
.dstName
)
591 dstName2
= ren1
.dstName
593 updateFile(False, ren1
.dstSha
, ren1
.dstMode
, dstName1
)
594 updateFile(False, ren2
.dstSha
, ren2
.dstMode
, dstName2
)
596 [resSha
, resMode
, clean
, merge
] = \
597 mergeFile(ren1
.srcName
, ren1
.srcSha
, ren1
.srcMode
,
598 ren1
.dstName
, ren1
.dstSha
, ren1
.dstMode
,
599 ren2
.dstName
, ren2
.dstSha
, ren2
.dstMode
,
600 branchName1
, branchName2
)
602 if merge
or not clean
:
603 output('Renaming', fmtRename(path
, ren1
.dstName
))
606 output('Auto-merging', ren1
.dstName
)
609 output('CONFLICT (content): merge conflict in',
614 updateFileExt(ren1
.dstSha
, ren1
.dstMode
, ren1
.dstName
,
615 updateCache
=True, updateWd
=False)
616 updateFile(clean
, resSha
, resMode
, ren1
.dstName
)
618 # Renamed in 1, maybe changed in 2
619 if renamesA
== renames1
:
624 srcShaOtherBranch
= ren1
.srcCacheEntry
.stages
[stage
].sha1
625 srcModeOtherBranch
= ren1
.srcCacheEntry
.stages
[stage
].mode
627 dstShaOtherBranch
= ren1
.dstCacheEntry
.stages
[stage
].sha1
628 dstModeOtherBranch
= ren1
.dstCacheEntry
.stages
[stage
].mode
632 if ren1
.dstName
in currentDirectorySet
:
633 newPath
= uniquePath(ren1
.dstName
, branchName1
)
634 output('CONFLICT (rename/directory): Rename',
635 fmtRename(ren1
.srcName
, ren1
.dstName
), 'in', branchName1
,
636 'directory', ren1
.dstName
, 'added in', branchName2
)
637 output('Renaming', ren1
.srcName
, 'to', newPath
, 'instead')
639 removeFile(False, ren1
.dstName
)
640 updateFile(False, ren1
.dstSha
, ren1
.dstMode
, newPath
)
641 elif srcShaOtherBranch
== None:
642 output('CONFLICT (rename/delete): Rename',
643 fmtRename(ren1
.srcName
, ren1
.dstName
), 'in',
644 branchName1
, 'and deleted in', branchName2
)
646 updateFile(False, ren1
.dstSha
, ren1
.dstMode
, ren1
.dstName
)
647 elif dstShaOtherBranch
:
648 newPath
= uniquePath(ren1
.dstName
, branchName2
)
649 output('CONFLICT (rename/add): Rename',
650 fmtRename(ren1
.srcName
, ren1
.dstName
), 'in',
651 branchName1
+ '.', ren1
.dstName
, 'added in', branchName2
)
652 output('Adding as', newPath
, 'instead')
653 updateFile(False, dstShaOtherBranch
, dstModeOtherBranch
, newPath
)
656 elif renames2
.getDst(ren1
.dstName
):
657 dst2
= renames2
.getDst(ren1
.dstName
)
658 newPath1
= uniquePath(ren1
.dstName
, branchName1
)
659 newPath2
= uniquePath(dst2
.dstName
, branchName2
)
660 output('CONFLICT (rename/rename): Rename',
661 fmtRename(ren1
.srcName
, ren1
.dstName
), 'in',
662 branchName1
+'. Rename',
663 fmtRename(dst2
.srcName
, dst2
.dstName
), 'in', branchName2
)
664 output('Renaming', ren1
.srcName
, 'to', newPath1
, 'and',
665 dst2
.srcName
, 'to', newPath2
, 'instead')
666 removeFile(False, ren1
.dstName
)
667 updateFile(False, ren1
.dstSha
, ren1
.dstMode
, newPath1
)
668 updateFile(False, dst2
.dstSha
, dst2
.dstMode
, newPath2
)
669 dst2
.processed
= True
675 [resSha
, resMode
, clean
, merge
] = \
676 mergeFile(ren1
.srcName
, ren1
.srcSha
, ren1
.srcMode
,
677 ren1
.dstName
, ren1
.dstSha
, ren1
.dstMode
,
678 ren1
.srcName
, srcShaOtherBranch
, srcModeOtherBranch
,
679 branchName1
, branchName2
)
681 if merge
or not clean
:
682 output('Renaming', fmtRename(ren1
.srcName
, ren1
.dstName
))
685 output('Auto-merging', ren1
.dstName
)
688 output('CONFLICT (rename/modify): Merge conflict in',
693 updateFileExt(ren1
.dstSha
, ren1
.dstMode
, ren1
.dstName
,
694 updateCache
=True, updateWd
=False)
695 updateFile(clean
, resSha
, resMode
, ren1
.dstName
)
699 # Per entry merge function
700 # ------------------------
702 def processEntry(entry
, branch1Name
, branch2Name
):
703 '''Merge one cache entry.'''
705 debug('processing', entry
.path
, 'clean cache:', cacheOnly
)
710 oSha
= entry
.stages
[1].sha1
711 oMode
= entry
.stages
[1].mode
712 aSha
= entry
.stages
[2].sha1
713 aMode
= entry
.stages
[2].mode
714 bSha
= entry
.stages
[3].sha1
715 bMode
= entry
.stages
[3].mode
717 assert(oSha
== None or isSha(oSha
))
718 assert(aSha
== None or isSha(aSha
))
719 assert(bSha
== None or isSha(bSha
))
721 assert(oMode
== None or type(oMode
) is int)
722 assert(aMode
== None or type(aMode
) is int)
723 assert(bMode
== None or type(bMode
) is int)
725 if (oSha
and (not aSha
or not bSha
)):
727 # Case A: Deleted in one
729 if (not aSha
and not bSha
) or \
730 (aSha
== oSha
and not bSha
) or \
731 (not aSha
and bSha
== oSha
):
732 # Deleted in both or deleted in one and unchanged in the other
734 output('Removing', path
)
735 removeFile(True, path
)
737 # Deleted in one and changed in the other
740 output('CONFLICT (delete/modify):', path
, 'deleted in',
741 branch1Name
, 'and modified in', branch2Name
+ '.',
742 'Version', branch2Name
, 'of', path
, 'left in tree.')
746 output('CONFLICT (modify/delete):', path
, 'deleted in',
747 branch2Name
, 'and modified in', branch1Name
+ '.',
748 'Version', branch1Name
, 'of', path
, 'left in tree.')
752 updateFile(False, sha
, mode
, path
)
754 elif (not oSha
and aSha
and not bSha
) or \
755 (not oSha
and not aSha
and bSha
):
757 # Case B: Added in one.
760 addBranch
= branch1Name
761 otherBranch
= branch2Name
764 conf
= 'file/directory'
766 addBranch
= branch2Name
767 otherBranch
= branch1Name
770 conf
= 'directory/file'
772 if path
in currentDirectorySet
:
774 newPath
= uniquePath(path
, addBranch
)
775 output('CONFLICT (' + conf
+ '):',
776 'There is a directory with name', path
, 'in',
777 otherBranch
+ '. Adding', path
, 'as', newPath
)
779 removeFile(False, path
)
780 updateFile(False, sha
, mode
, newPath
)
782 output('Adding', path
)
783 updateFile(True, sha
, mode
, path
)
785 elif not oSha
and aSha
and bSha
:
787 # Case C: Added in both (check for same permissions).
792 output('CONFLICT: File', path
,
793 'added identically in both branches, but permissions',
794 'conflict', '0%o' % aMode
, '->', '0%o' % bMode
)
795 output('CONFLICT: adding with permission:', '0%o' % aMode
)
797 updateFile(False, aSha
, aMode
, path
)
799 # This case is handled by git-read-tree
803 newPath1
= uniquePath(path
, branch1Name
)
804 newPath2
= uniquePath(path
, branch2Name
)
805 output('CONFLICT (add/add): File', path
,
806 'added non-identically in both branches. Adding as',
807 newPath1
, 'and', newPath2
, 'instead.')
808 removeFile(False, path
)
809 updateFile(False, aSha
, aMode
, newPath1
)
810 updateFile(False, bSha
, bMode
, newPath2
)
812 elif oSha
and aSha
and bSha
:
814 # case D: Modified in both, but differently.
816 output('Auto-merging', path
)
817 [sha
, mode
, clean
, dummy
] = \
818 mergeFile(path
, oSha
, oMode
,
821 branch1Name
, branch2Name
)
823 updateFile(True, sha
, mode
, path
)
826 output('CONFLICT (content): Merge conflict in', path
)
829 updateFile(False, sha
, mode
, path
)
831 updateFileExt(sha
, mode
, path
, updateCache
=False, updateWd
=True)
833 die("ERROR: Fatal merge failure, shouldn't happen.")
838 die('Usage:', sys
.argv
[0], ' <base>... -- <head> <remote>..')
840 # main entry point as merge strategy module
841 # The first parameters up to -- are merge bases, and the rest are heads.
842 # This strategy module figures out merge bases itself, so we only
845 if len(sys
.argv
) < 4:
848 for nextArg
in xrange(1, len(sys
.argv
)):
849 if sys
.argv
[nextArg
] == '--':
850 if len(sys
.argv
) != nextArg
+ 3:
851 die('Not handling anything other than two heads merge.')
853 h1
= firstBranch
= sys
.argv
[nextArg
+ 1]
854 h2
= secondBranch
= sys
.argv
[nextArg
+ 2]
859 print 'Merging', h1
, 'with', h2
862 h1
= runProgram(['git-rev-parse', '--verify', h1
+ '^0']).rstrip()
863 h2
= runProgram(['git-rev-parse', '--verify', h2
+ '^0']).rstrip()
865 graph
= buildGraph([h1
, h2
])
867 [dummy
, clean
] = merge(graph
.shaMap
[h1
], graph
.shaMap
[h2
],
868 firstBranch
, secondBranch
, graph
)
872 if isinstance(sys
.exc_info()[1], SystemExit):
875 traceback
.print_exc(None, sys
.stderr
)