#!/usr/bin/python import sys, math, random, os, re, signal, tempfile, stat, errno, traceback from heapq import heappush, heappop from sets import Set sys.path.append('@@GIT_PYTHON_PATH@@') from gitMergeCommon import * # The actual merge code # --------------------- originalIndexFile = os.environ.get('GIT_INDEX_FILE', os.environ.get('GIT_DIR', '.git') + '/index') temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \ '/merge-recursive-tmp-index' def setupIndex(temporary): try: os.unlink(temporaryIndexFile) except OSError: pass if temporary: newIndex = temporaryIndexFile os.environ else: newIndex = originalIndexFile os.environ['GIT_INDEX_FILE'] = newIndex def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0): '''Merge the commits h1 and h2, return the resulting virtual commit object and a flag indicating the cleaness of the merge.''' assert(isinstance(h1, Commit) and isinstance(h2, Commit)) assert(isinstance(graph, Graph)) def infoMsg(*args): sys.stdout.write(' '*callDepth) printList(args) infoMsg('Merging:') infoMsg(h1) infoMsg(h2) sys.stdout.flush() ca = getCommonAncestors(graph, h1, h2) infoMsg('found', len(ca), 'common ancestor(s):') for x in ca: infoMsg(x) sys.stdout.flush() Ms = ca[0] for h in ca[1:]: [Ms, ignore] = merge(Ms, h, 'Temporary shared merge branch 1', 'Temporary shared merge branch 2', graph, callDepth+1) assert(isinstance(Ms, Commit)) if callDepth == 0: setupIndex(False) cleanCache = False else: setupIndex(True) runProgram(['git-read-tree', h1.tree()]) cleanCache = True [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), Ms.tree(), branch1Name, branch2Name, cleanCache) if clean or cleanCache: res = Commit(None, [h1, h2], tree=shaRes) graph.addNode(res) else: res = None return [res, clean] getFilesRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)$', re.S) def getFilesAndDirs(tree): files = Set() dirs = Set() out = runProgram(['git-ls-tree', '-r', '-z', tree]) for l in out.split('\0'): m = getFilesRE.match(l) if m: if m.group(2) == 'tree': dirs.add(m.group(4)) elif m.group(2) == 'blob': files.add(m.group(4)) return [files, dirs] class CacheEntry: def __init__(self, path): class Stage: def __init__(self): self.sha1 = None self.mode = None self.stages = [Stage(), Stage(), Stage()] self.path = path unmergedRE = re.compile(r'^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S) def unmergedCacheEntries(): '''Create a dictionary mapping file names to CacheEntry objects. The dictionary contains one entry for every path with a non-zero stage entry.''' lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0') lines.pop() res = {} for l in lines: m = unmergedRE.match(l) if m: mode = int(m.group(1), 8) sha1 = m.group(2) stage = int(m.group(3)) - 1 path = m.group(4) if res.has_key(path): e = res[path] else: e = CacheEntry(path) res[path] = e e.stages[stage].mode = mode e.stages[stage].sha1 = sha1 else: die('Error: Merge program failed: Unexpected output from', \ 'git-ls-files:', l) return res def mergeTrees(head, merge, common, branch1Name, branch2Name, cleanCache): '''Merge the trees 'head' and 'merge' with the common ancestor 'common'. The name of the head branch is 'branch1Name' and the name of the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge) where tree is the resulting tree and cleanMerge is True iff the merge was clean.''' assert(isSha(head) and isSha(merge) and isSha(common)) if common == merge: print 'Already uptodate!' return [head, True] if cleanCache: updateArg = '-i' else: updateArg = '-u' runProgram(['git-read-tree', updateArg, '-m', common, head, merge]) cleanMerge = True [tree, code] = runProgram('git-write-tree', returnCode=True) tree = tree.rstrip() if code != 0: [files, dirs] = getFilesAndDirs(head) [filesM, dirsM] = getFilesAndDirs(merge) files.union_update(filesM) dirs.union_update(dirsM) cleanMerge = True entries = unmergedCacheEntries() for name in entries: if not processEntry(entries[name], branch1Name, branch2Name, files, dirs, cleanCache): cleanMerge = False if cleanMerge or cleanCache: tree = runProgram('git-write-tree').rstrip() else: tree = None else: cleanMerge = True return [tree, cleanMerge] def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache): '''Merge one cache entry. 'files' is a Set with the files in both of the heads that we are going to merge. 'dirs' contains the corresponding data for directories. If 'cleanCache' is True no non-zero stages will be left in the cache for the path corresponding to the entry 'entry'.''' # cleanCache == True => Don't leave any non-stage 0 entries in the cache and # don't update the working directory # False => Leave unmerged entries and update the working directory # clean == True => non-conflict case # False => conflict case # If cleanCache == False then the cache shouldn't be updated if clean == False def updateFile(clean, sha, mode, path, onlyWd=False): updateCache = not onlyWd and (cleanCache or (not cleanCache and clean)) updateWd = onlyWd or (not cleanCache and clean) if updateWd: prog = ['git-cat-file', 'blob', sha] if stat.S_ISREG(mode): try: os.unlink(path) except OSError: pass if mode & 0100: mode = 0777 else: mode = 0666 fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode) proc = subprocess.Popen(prog, stdout=fd) proc.wait() os.close(fd) elif stat.S_ISLNK(mode): linkTarget = runProgram(prog) os.symlink(linkTarget, path) else: assert(False) if updateWd and updateCache: runProgram(['git-update-index', '--add', '--', path]) elif updateCache: runProgram(['git-update-index', '--add', '--cacheinfo', '0%o' % mode, sha, path]) def removeFile(clean, path): if cleanCache or (not cleanCache and clean): runProgram(['git-update-index', '--force-remove', '--', path]) if not cleanCache and clean: try: os.unlink(path) except OSError, e: if e.errno != errno.ENOENT and e.errno != errno.EISDIR: raise def uniquePath(path, branch): newPath = path + '_' + branch suffix = 0 while newPath in files or newPath in dirs: suffix += 1 newPath = path + '_' + branch + '_' + str(suffix) files.add(newPath) return newPath debug('processing', entry.path, 'clean cache:', cleanCache) cleanMerge = True path = entry.path oSha = entry.stages[0].sha1 oMode = entry.stages[0].mode aSha = entry.stages[1].sha1 aMode = entry.stages[1].mode bSha = entry.stages[2].sha1 bMode = entry.stages[2].mode assert(oSha == None or isSha(oSha)) assert(aSha == None or isSha(aSha)) assert(bSha == None or isSha(bSha)) assert(oMode == None or type(oMode) is int) assert(aMode == None or type(aMode) is int) assert(bMode == None or type(bMode) is int) if (oSha and (not aSha or not bSha)): # # Case A: Deleted in one # if (not aSha and not bSha) or \ (aSha == oSha and not bSha) or \ (not aSha and bSha == oSha): # Deleted in both or deleted in one and unchanged in the other if aSha: print 'Removing ' + path removeFile(True, path) else: # Deleted in one and changed in the other cleanMerge = False if not aSha: print 'CONFLICT (del/mod): "' + path + '" deleted in', \ branch1Name, 'and modified in', branch2Name, \ '. Version', branch2Name, ' of "' + path + \ '" left in tree' mode = bMode sha = bSha else: print 'CONFLICT (mod/del): "' + path + '" deleted in', \ branch2Name, 'and modified in', branch1Name + \ '. Version', branch1Name, 'of "' + path + \ '" left in tree' mode = aMode sha = aSha updateFile(False, sha, mode, path) elif (not oSha and aSha and not bSha) or \ (not oSha and not aSha and bSha): # # Case B: Added in one. # if aSha: addBranch = branch1Name otherBranch = branch2Name mode = aMode sha = aSha conf = 'file/dir' else: addBranch = branch2Name otherBranch = branch1Name mode = bMode sha = bSha conf = 'dir/file' if path in dirs: cleanMerge = False newPath = uniquePath(path, addBranch) print 'CONFLICT (' + conf + \ '): There is a directory with name "' + path + '" in', \ otherBranch + '. Adding "' + path + '" as "' + newPath + '"' removeFile(False, path) path = newPath else: print 'Adding "' + path + '"' updateFile(True, sha, mode, path) elif not oSha and aSha and bSha: # # Case C: Added in both (check for same permissions). # if aSha == bSha: if aMode != bMode: cleanMerge = False print 'CONFLICT: File "' + path + \ '" added identically in both branches,', \ 'but permissions conflict', '0%o' % aMode, '->', \ '0%o' % bMode print 'CONFLICT: adding with permission:', '0%o' % aMode updateFile(False, aSha, aMode, path) else: # This case is handled by git-read-tree assert(False) else: cleanMerge = False newPath1 = uniquePath(path, branch1Name) newPath2 = uniquePath(path, branch2Name) print 'CONFLICT (add/add): File "' + path + \ '" added non-identically in both branches.' removeFile(False, path) updateFile(False, aSha, aMode, newPath1) updateFile(False, bSha, bMode, newPath2) elif oSha and aSha and bSha: # # case D: Modified in both, but differently. # print 'Auto-merging', path orig = runProgram(['git-unpack-file', oSha]).rstrip() src1 = runProgram(['git-unpack-file', aSha]).rstrip() src2 = runProgram(['git-unpack-file', bSha]).rstrip() [out, ret] = runProgram(['merge', '-L', branch1Name + '/' + path, '-L', 'orig/' + path, '-L', branch2Name + '/' + path, src1, orig, src2], returnCode=True) if aMode == oMode: mode = bMode else: mode = aMode sha = runProgram(['git-hash-object', '-t', 'blob', '-w', src1]).rstrip() if ret != 0: cleanMerge = False print 'CONFLICT (content): Merge conflict in "' + path + '".' if cleanCache: updateFile(False, sha, mode, path) else: updateFile(True, aSha, aMode, path) updateFile(False, sha, mode, path, True) else: updateFile(True, sha, mode, path) os.unlink(orig) os.unlink(src1) os.unlink(src2) else: die("ERROR: Fatal merge failure, shouldn't happen.") return cleanMerge def usage(): die('Usage:', sys.argv[0], ' ... -- ..') # main entry point as merge strategy module # The first parameters up to -- are merge bases, and the rest are heads. # This strategy module figures out merge bases itself, so we only # get heads. if len(sys.argv) < 4: usage() for nextArg in xrange(1, len(sys.argv)): if sys.argv[nextArg] == '--': if len(sys.argv) != nextArg + 3: die('Not handling anything other than two heads merge.') try: h1 = firstBranch = sys.argv[nextArg + 1] h2 = secondBranch = sys.argv[nextArg + 2] except IndexError: usage() break print 'Merging', h1, 'with', h2 try: h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip() h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip() graph = buildGraph([h1, h2]) [res, clean] = merge(graph.shaMap[h1], graph.shaMap[h2], firstBranch, secondBranch, graph) print '' except: traceback.print_exc(None, sys.stderr) sys.exit(2) if clean: sys.exit(0) else: sys.exit(1)