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 setIndexStages(path,
287 runProgram(['git-update-index', '-z', '--index-info'],
288 input="0 " + ("0" * 40) + "\t" + path + "\0" + \
289 "%o %s %d\t%s\0" % (oMode, oSHA1, 1, path) + \
290 "%o %s %d\t%s\0" % (aMode, aSHA1, 2, path) + \
291 "%o %s %d\t%s\0" % (bMode, bSHA1, 3, path))
293 def removeFile(clean, path):
294 updateCache = cacheOnly or clean
295 updateWd = not cacheOnly
298 runProgram(['git-update-index', '--force-remove', '--', path])
304 if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
307 os.removedirs(os.path.dirname(path))
311 def uniquePath(path, branch):
312 def fileExists(path):
317 if e.errno == errno.ENOENT:
322 branch = branch.replace('/', '_')
323 newPath = path + '~' + branch
325 while newPath in currentFileSet or \
326 newPath in currentDirectorySet or \
329 newPath = path + '~' + branch + '_' + str(suffix)
330 currentFileSet.add(newPath)
333 # Cache entry management
334 # ----------------------
337 def __init__(self, path):
343 # Used for debugging only
345 if self.mode != None:
346 m = '0%o' % self.mode
354 return 'sha1: ' + sha1 + ' mode: ' + m
356 self.stages = [Stage(), Stage(), Stage(), Stage()]
358 self.processed = False
361 return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
363 class CacheEntryContainer:
367 def add(self, entry):
368 self.entries[entry.path] = entry
371 return self.entries.get(path)
374 return self.entries.itervalues()
376 unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
377 def unmergedCacheEntries():
378 '''Create a dictionary mapping file names to CacheEntry
379 objects. The dictionary contains one entry for every path with a
380 non-zero stage entry.'''
382 lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
385 res = CacheEntryContainer()
387 m = unmergedRE.match(l)
389 mode = int(m.group(1), 8)
391 stage = int(m.group(3))
399 e.stages[stage].mode = mode
400 e.stages[stage].sha1 = sha1
402 die('Error: Merge program failed: Unexpected output from',
406 lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S)
407 def getCacheEntry(path, origTree, aTree, bTree):
408 '''Returns a CacheEntry object which doesn't have to correspond to
409 a real cache entry in Git's index.'''
415 m = lsTreeRE.match(out)
417 die('Unexpected output from git-ls-tree:', out)
418 elif m.group(2) == 'blob':
419 return [m.group(3), int(m.group(1), 8)]
423 res = CacheEntry(path)
425 [oSha, oMode] = parse(runProgram(['git-ls-tree', origTree, '--', path]))
426 [aSha, aMode] = parse(runProgram(['git-ls-tree', aTree, '--', path]))
427 [bSha, bMode] = parse(runProgram(['git-ls-tree', bTree, '--', path]))
429 res.stages[1].sha1 = oSha
430 res.stages[1].mode = oMode
431 res.stages[2].sha1 = aSha
432 res.stages[2].mode = aMode
433 res.stages[3].sha1 = bSha
434 res.stages[3].mode = bMode
438 # Rename detection and handling
439 # -----------------------------
443 src, srcSha, srcMode, srcCacheEntry,
444 dst, dstSha, dstMode, dstCacheEntry,
448 self.srcMode = srcMode
449 self.srcCacheEntry = srcCacheEntry
452 self.dstMode = dstMode
453 self.dstCacheEntry = dstCacheEntry
456 self.processed = False
458 class RenameEntryContainer:
463 def add(self, entry):
464 self.entriesSrc[entry.srcName] = entry
465 self.entriesDst[entry.dstName] = entry
467 def getSrc(self, path):
468 return self.entriesSrc.get(path)
470 def getDst(self, path):
471 return self.entriesDst.get(path)
474 return self.entriesSrc.itervalues()
476 parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
477 def getRenames(tree, oTree, aTree, bTree, cacheEntries):
478 '''Get information of all renames which occured between 'oTree' and
479 'tree'. We need the three trees in the merge ('oTree', 'aTree' and
480 'bTree') to be able to associate the correct cache entries with
481 the rename information. 'tree' is always equal to either aTree or bTree.'''
483 assert(tree == aTree or tree == bTree)
484 inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
487 ret = RenameEntryContainer()
489 recs = inp.split("\0")
490 recs.pop() # remove last entry (which is '')
494 m = parseDiffRenamesRE.match(rec)
497 die('Unexpected output from git-diff-tree:', rec)
499 srcMode = int(m.group(1), 8)
500 dstMode = int(m.group(2), 8)
507 srcCacheEntry = cacheEntries.get(src)
508 if not srcCacheEntry:
509 srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
510 cacheEntries.add(srcCacheEntry)
512 dstCacheEntry = cacheEntries.get(dst)
513 if not dstCacheEntry:
514 dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
515 cacheEntries.add(dstCacheEntry)
517 ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
518 dst, dstSha, dstMode, dstCacheEntry,
520 except StopIteration:
524 def fmtRename(src, dst):
525 srcPath = src.split('/')
526 dstPath = dst.split('/')
528 endIndex = min(len(srcPath), len(dstPath)) - 1
529 for x in range(0, endIndex):
530 if srcPath[x] == dstPath[x]:
531 path.append(srcPath[x])
537 return '/'.join(path) + \
538 '/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
539 '/'.join(dstPath[endIndex:]) + '}'
541 return src + ' => ' + dst
543 def processRenames(renamesA, renamesB, branchNameA, branchNameB):
546 srcNames.add(x.srcName)
548 srcNames.add(x.srcName)
551 for path in srcNames:
552 if renamesA.getSrc(path):
555 branchName1 = branchNameA
556 branchName2 = branchNameB
560 branchName1 = branchNameB
561 branchName2 = branchNameA
563 ren1 = renames1.getSrc(path)
564 ren2 = renames2.getSrc(path)
566 ren1.dstCacheEntry.processed = True
567 ren1.srcCacheEntry.processed = True
572 ren1.processed = True
573 removeFile(True, ren1.srcName)
575 # Renamed in 1 and renamed in 2
576 assert(ren1.srcName == ren2.srcName)
577 ren2.dstCacheEntry.processed = True
578 ren2.processed = True
580 if ren1.dstName != ren2.dstName:
581 output('CONFLICT (rename/rename): Rename',
582 fmtRename(path, ren1.dstName), 'in branch', branchName1,
583 'rename', fmtRename(path, ren2.dstName), 'in',
587 if ren1.dstName in currentDirectorySet:
588 dstName1 = uniquePath(ren1.dstName, branchName1)
589 output(ren1.dstName, 'is a directory in', branchName2,
590 'adding as', dstName1, 'instead.')
591 removeFile(False, ren1.dstName)
593 dstName1 = ren1.dstName
595 if ren2.dstName in currentDirectorySet:
596 dstName2 = uniquePath(ren2.dstName, branchName2)
597 output(ren2.dstName, 'is a directory in', branchName1,
598 'adding as', dstName2, 'instead.')
599 removeFile(False, ren2.dstName)
601 dstName2 = ren1.dstName
603 # NEEDSWORK: place dstNameA at stage 2 and dstNameB at stage 3
604 # What about other stages???
605 updateFile(False, ren1.dstSha, ren1.dstMode, dstName1)
606 updateFile(False, ren2.dstSha, ren2.dstMode, dstName2)
608 [resSha, resMode, clean, merge] = \
609 mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
610 ren1.dstName, ren1.dstSha, ren1.dstMode,
611 ren2.dstName, ren2.dstSha, ren2.dstMode,
612 branchName1, branchName2)
614 if merge or not clean:
615 output('Renaming', fmtRename(path, ren1.dstName))
618 output('Auto-merging', ren1.dstName)
621 output('CONFLICT (content): merge conflict in',
626 setIndexStages(ren1.dstName,
627 ren1.srcSha, ren1.srcMode,
628 ren1.dstSha, ren1.dstMode,
629 ren2.dstSha, ren2.dstMode)
631 updateFile(clean, resSha, resMode, ren1.dstName)
633 # Renamed in 1, maybe changed in 2
634 if renamesA == renames1:
639 srcShaOtherBranch = ren1.srcCacheEntry.stages[stage].sha1
640 srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
642 dstShaOtherBranch = ren1.dstCacheEntry.stages[stage].sha1
643 dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
647 if ren1.dstName in currentDirectorySet:
648 newPath = uniquePath(ren1.dstName, branchName1)
649 output('CONFLICT (rename/directory): Rename',
650 fmtRename(ren1.srcName, ren1.dstName), 'in', branchName1,
651 'directory', ren1.dstName, 'added in', branchName2)
652 output('Renaming', ren1.srcName, 'to', newPath, 'instead')
654 removeFile(False, ren1.dstName)
655 updateFile(False, ren1.dstSha, ren1.dstMode, newPath)
656 elif srcShaOtherBranch == None:
657 output('CONFLICT (rename/delete): Rename',
658 fmtRename(ren1.srcName, ren1.dstName), 'in',
659 branchName1, 'and deleted in', branchName2)
661 updateFile(False, ren1.dstSha, ren1.dstMode, ren1.dstName)
662 elif dstShaOtherBranch:
663 newPath = uniquePath(ren1.dstName, branchName2)
664 output('CONFLICT (rename/add): Rename',
665 fmtRename(ren1.srcName, ren1.dstName), 'in',
666 branchName1 + '.', ren1.dstName, 'added in', branchName2)
667 output('Adding as', newPath, 'instead')
668 updateFile(False, dstShaOtherBranch, dstModeOtherBranch, newPath)
671 elif renames2.getDst(ren1.dstName):
672 dst2 = renames2.getDst(ren1.dstName)
673 newPath1 = uniquePath(ren1.dstName, branchName1)
674 newPath2 = uniquePath(dst2.dstName, branchName2)
675 output('CONFLICT (rename/rename): Rename',
676 fmtRename(ren1.srcName, ren1.dstName), 'in',
677 branchName1+'. Rename',
678 fmtRename(dst2.srcName, dst2.dstName), 'in', branchName2)
679 output('Renaming', ren1.srcName, 'to', newPath1, 'and',
680 dst2.srcName, 'to', newPath2, 'instead')
681 removeFile(False, ren1.dstName)
682 updateFile(False, ren1.dstSha, ren1.dstMode, newPath1)
683 updateFile(False, dst2.dstSha, dst2.dstMode, newPath2)
684 dst2.processed = True
691 oName, oSHA1, oMode = ren1.srcName, ren1.srcSha, ren1.srcMode
692 aName, bName = ren1.dstName, ren1.srcName
693 aSHA1, bSHA1 = ren1.dstSha, srcShaOtherBranch
694 aMode, bMode = ren1.dstMode, srcModeOtherBranch
695 aBranch, bBranch = branchName1, branchName2
697 if renamesA != renames1:
698 aName, bName = bName, aName
699 aSHA1, bSHA1 = bSHA1, aSHA1
700 aMode, bMode = bMode, aMode
701 aBranch, bBranch = bBranch, aBranch
703 [resSha, resMode, clean, merge] = \
704 mergeFile(oName, oSHA1, oMode,
709 if merge or not clean:
710 output('Renaming', fmtRename(ren1.srcName, ren1.dstName))
713 output('Auto-merging', ren1.dstName)
716 output('CONFLICT (rename/modify): Merge conflict in',
721 setIndexStages(ren1.dstName,
726 updateFile(clean, resSha, resMode, ren1.dstName)
730 # Per entry merge function
731 # ------------------------
733 def processEntry(entry, branch1Name, branch2Name):
734 '''Merge one cache entry.'''
736 debug('processing', entry.path, 'clean cache:', cacheOnly)
741 oSha = entry.stages[1].sha1
742 oMode = entry.stages[1].mode
743 aSha = entry.stages[2].sha1
744 aMode = entry.stages[2].mode
745 bSha = entry.stages[3].sha1
746 bMode = entry.stages[3].mode
748 assert(oSha == None or isSha(oSha))
749 assert(aSha == None or isSha(aSha))
750 assert(bSha == None or isSha(bSha))
752 assert(oMode == None or type(oMode) is int)
753 assert(aMode == None or type(aMode) is int)
754 assert(bMode == None or type(bMode) is int)
756 if (oSha and (not aSha or not bSha)):
758 # Case A: Deleted in one
760 if (not aSha and not bSha) or \
761 (aSha == oSha and not bSha) or \
762 (not aSha and bSha == oSha):
763 # Deleted in both or deleted in one and unchanged in the other
765 output('Removing', path)
766 removeFile(True, path)
768 # Deleted in one and changed in the other
771 output('CONFLICT (delete/modify):', path, 'deleted in',
772 branch1Name, 'and modified in', branch2Name + '.',
773 'Version', branch2Name, 'of', path, 'left in tree.')
777 output('CONFLICT (modify/delete):', path, 'deleted in',
778 branch2Name, 'and modified in', branch1Name + '.',
779 'Version', branch1Name, 'of', path, 'left in tree.')
783 updateFile(False, sha, mode, path)
785 elif (not oSha and aSha and not bSha) or \
786 (not oSha and not aSha and bSha):
788 # Case B: Added in one.
791 addBranch = branch1Name
792 otherBranch = branch2Name
795 conf = 'file/directory'
797 addBranch = branch2Name
798 otherBranch = branch1Name
801 conf = 'directory/file'
803 if path in currentDirectorySet:
805 newPath = uniquePath(path, addBranch)
806 output('CONFLICT (' + conf + '):',
807 'There is a directory with name', path, 'in',
808 otherBranch + '. Adding', path, 'as', newPath)
810 removeFile(False, path)
811 updateFile(False, sha, mode, newPath)
813 output('Adding', path)
814 updateFile(True, sha, mode, path)
816 elif not oSha and aSha and bSha:
818 # Case C: Added in both (check for same permissions).
823 output('CONFLICT: File', path,
824 'added identically in both branches, but permissions',
825 'conflict', '0%o' % aMode, '->', '0%o' % bMode)
826 output('CONFLICT: adding with permission:', '0%o' % aMode)
828 updateFile(False, aSha, aMode, path)
830 # This case is handled by git-read-tree
834 newPath1 = uniquePath(path, branch1Name)
835 newPath2 = uniquePath(path, branch2Name)
836 output('CONFLICT (add/add): File', path,
837 'added non-identically in both branches. Adding as',
838 newPath1, 'and', newPath2, 'instead.')
839 removeFile(False, path)
840 updateFile(False, aSha, aMode, newPath1)
841 updateFile(False, bSha, bMode, newPath2)
843 elif oSha and aSha and bSha:
845 # case D: Modified in both, but differently.
847 output('Auto-merging', path)
848 [sha, mode, clean, dummy] = \
849 mergeFile(path, oSha, oMode,
852 branch1Name, branch2Name)
854 updateFile(True, sha, mode, path)
857 output('CONFLICT (content): Merge conflict in', path)
860 updateFile(False, sha, mode, path)
862 updateFileExt(sha, mode, path, updateCache=False, updateWd=True)
864 die("ERROR: Fatal merge failure, shouldn't happen.")
869 die('Usage:', sys.argv[0], ' <base>... -- <head> <remote>..')
871 # main entry point as merge strategy module
872 # The first parameters up to -- are merge bases, and the rest are heads.
873 # This strategy module figures out merge bases itself, so we only
876 if len(sys.argv) < 4:
879 for nextArg in xrange(1, len(sys.argv)):
880 if sys.argv[nextArg] == '--':
881 if len(sys.argv) != nextArg + 3:
882 die('Not handling anything other than two heads merge.')
884 h1 = firstBranch = sys.argv[nextArg + 1]
885 h2 = secondBranch = sys.argv[nextArg + 2]
890 print 'Merging', h1, 'with', h2
893 h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip()
894 h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip()
896 graph = buildGraph([h1, h2])
898 [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
899 firstBranch, secondBranch, graph)
903 if isinstance(sys.exc_info()[1], SystemExit):
906 traceback.print_exc(None, sys.stderr)