X-Git-Url: https://git.octo.it/?p=git.git;a=blobdiff_plain;f=git-merge-recursive.py;h=ce8a31fda050f36b24b8dffa5ee29e7dde074963;hp=626d85493a64d798dedbdc52b2b0f68d56d447fd;hb=HEAD;hpb=8ac3a61f59173d4a9a328518be83a25df610a5ef diff --git a/git-merge-recursive.py b/git-merge-recursive.py index 626d8549..ce8a31fd 100755 --- a/git-merge-recursive.py +++ b/git-merge-recursive.py @@ -1,12 +1,22 @@ #!/usr/bin/python +# +# Copyright (C) 2005 Fredrik Kuivinen +# -import sys, math, random, os, re, signal, tempfile, stat, errno, traceback +import sys +sys.path.append('''@@GIT_PYTHON_PATH@@''') + +import 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 * +outputIndent = 0 +def output(*args): + sys.stdout.write(' '*outputIndent) + printList(args) + originalIndexFile = os.environ.get('GIT_INDEX_FILE', os.environ.get('GIT_DIR', '.git') + '/index') temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \ @@ -35,33 +45,36 @@ cacheOnly = False # The entry point to the merge code # --------------------------------- -def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0): +def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0, ancestor=None): '''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) + global outputIndent - infoMsg('Merging:') - infoMsg(h1) - infoMsg(h2) + output('Merging:') + output(h1) + output(h2) sys.stdout.flush() - ca = getCommonAncestors(graph, h1, h2) - infoMsg('found', len(ca), 'common ancestor(s):') + if ancestor: + ca = [ancestor] + else: + assert(isinstance(graph, Graph)) + ca = getCommonAncestors(graph, h1, h2) + output('found', len(ca), 'common ancestor(s):') for x in ca: - infoMsg(x) + output(x) sys.stdout.flush() mergedCA = ca[0] for h in ca[1:]: + outputIndent = callDepth+1 [mergedCA, dummy] = merge(mergedCA, h, - 'Temporary shared merge branch 1', - 'Temporary shared merge branch 2', + 'Temporary merge branch 1', + 'Temporary merge branch 2', graph, callDepth+1) + outputIndent = callDepth assert(isinstance(mergedCA, Commit)) global cacheOnly @@ -76,7 +89,7 @@ def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0): [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(), branch1Name, branch2Name) - if clean or cacheOnly: + if graph and (clean or cacheOnly): res = Commit(None, [h1, h2], tree=shaRes) graph.addNode(res) else: @@ -88,7 +101,7 @@ 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]) + out = runProgram(['git-ls-tree', '-r', '-z', '-t', tree]) for l in out.split('\0'): m = getFilesRE.match(l) if m: @@ -116,7 +129,7 @@ def mergeTrees(head, merge, common, branch1Name, branch2Name): assert(isSha(head) and isSha(merge) and isSha(common)) if common == merge: - print 'Already uptodate!' + output('Already uptodate!') return [head, True] if cacheOnly: @@ -195,11 +208,16 @@ def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode, orig = runProgram(['git-unpack-file', oSha]).rstrip() src1 = runProgram(['git-unpack-file', aSha]).rstrip() src2 = runProgram(['git-unpack-file', bSha]).rstrip() - [out, code] = runProgram(['merge', - '-L', branch1Name + '/' + aPath, - '-L', 'orig/' + oPath, - '-L', branch2Name + '/' + bPath, - src1, orig, src2], returnCode=True) + try: + [out, code] = runProgram(['merge', + '-L', branch1Name + '/' + aPath, + '-L', 'orig/' + oPath, + '-L', branch2Name + '/' + bPath, + src1, orig, src2], returnCode=True) + except ProgramError, e: + print >>sys.stderr, e + die("Failed to execute 'merge'. merge(1) is used as the " + "file-level merge tool. Is 'merge' in your path?") sha = runProgram(['git-hash-object', '-t', 'blob', '-w', src1]).rstrip() @@ -207,7 +225,7 @@ def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode, os.unlink(orig) os.unlink(src1) os.unlink(src2) - + clean = (code == 0) else: assert(stat.S_ISLNK(aMode) and stat.S_ISLNK(bMode)) @@ -235,7 +253,7 @@ def updateFileExt(sha, mode, path, updateCache, updateWd): try: createDir = not stat.S_ISDIR(os.lstat(p).st_mode) - except: + except OSError: createDir = True if createDir: @@ -270,6 +288,24 @@ def updateFileExt(sha, mode, path, updateCache, updateWd): runProgram(['git-update-index', '--add', '--cacheinfo', '0%o' % mode, sha, path]) +def setIndexStages(path, + oSHA1, oMode, + aSHA1, aMode, + bSHA1, bMode, + clear=True): + istring = [] + if clear: + istring.append("0 " + ("0" * 40) + "\t" + path + "\0") + if oMode: + istring.append("%o %s %d\t%s\0" % (oMode, oSHA1, 1, path)) + if aMode: + istring.append("%o %s %d\t%s\0" % (aMode, aSHA1, 2, path)) + if bMode: + istring.append("%o %s %d\t%s\0" % (bMode, bSHA1, 3, path)) + + runProgram(['git-update-index', '-z', '--index-info'], + input="".join(istring)) + def removeFile(clean, path): updateCache = cacheOnly or clean updateWd = not cacheOnly @@ -283,6 +319,10 @@ def removeFile(clean, path): except OSError, e: if e.errno != errno.ENOENT and e.errno != errno.EISDIR: raise + try: + os.removedirs(os.path.dirname(path)) + except OSError: + pass def uniquePath(path, branch): def fileExists(path): @@ -295,13 +335,14 @@ def uniquePath(path, branch): else: raise - newPath = path + '_' + branch + branch = branch.replace('/', '_') + newPath = path + '~' + branch suffix = 0 while newPath in currentFileSet or \ newPath in currentDirectorySet or \ fileExists(newPath): suffix += 1 - newPath = path + '_' + branch + '_' + str(suffix) + newPath = path + '~' + branch + '_' + str(suffix) currentFileSet.add(newPath) return newPath @@ -545,7 +586,7 @@ def processRenames(renamesA, renamesB, branchNameA, branchNameB): continue ren1.processed = True - removeFile(True, ren1.srcName) + if ren2: # Renamed in 1 and renamed in 2 assert(ren1.srcName == ren2.srcName) @@ -553,49 +594,66 @@ def processRenames(renamesA, renamesB, branchNameA, branchNameB): ren2.processed = True if ren1.dstName != ren2.dstName: - print 'CONFLICT (rename/rename): Rename', \ - fmtRename(path, ren1.dstName), 'in branch', branchName1, \ - 'rename', fmtRename(path, ren2.dstName), 'in', branchName2 + output('CONFLICT (rename/rename): Rename', + fmtRename(path, ren1.dstName), 'in branch', branchName1, + 'rename', fmtRename(path, ren2.dstName), 'in', + branchName2) cleanMerge = False if ren1.dstName in currentDirectorySet: dstName1 = uniquePath(ren1.dstName, branchName1) - print ren1.dstName, 'is a directory in', branchName2, \ - 'adding as', dstName1, 'instead.' + output(ren1.dstName, 'is a directory in', branchName2, + 'adding as', dstName1, 'instead.') removeFile(False, ren1.dstName) else: dstName1 = ren1.dstName if ren2.dstName in currentDirectorySet: dstName2 = uniquePath(ren2.dstName, branchName2) - print ren2.dstName, 'is a directory in', branchName1, \ - 'adding as', dstName2, 'instead.' + output(ren2.dstName, 'is a directory in', branchName1, + 'adding as', dstName2, 'instead.') removeFile(False, ren2.dstName) else: - dstName2 = ren1.dstName + dstName2 = ren2.dstName + setIndexStages(dstName1, + None, None, + ren1.dstSha, ren1.dstMode, + None, None) + setIndexStages(dstName2, + None, None, + None, None, + ren2.dstSha, ren2.dstMode) - updateFile(False, ren1.dstSha, ren1.dstMode, dstName1) - updateFile(False, ren2.dstSha, ren2.dstMode, dstName2) else: - print 'Renaming', fmtRename(path, ren1.dstName) + removeFile(True, ren1.srcName) + [resSha, resMode, clean, merge] = \ mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode, ren1.dstName, ren1.dstSha, ren1.dstMode, ren2.dstName, ren2.dstSha, ren2.dstMode, branchName1, branchName2) + if merge or not clean: + output('Renaming', fmtRename(path, ren1.dstName)) + if merge: - print 'Auto-merging', ren1.dstName + output('Auto-merging', ren1.dstName) if not clean: - print 'CONFLICT (content): merge conflict in', ren1.dstName + output('CONFLICT (content): merge conflict in', + ren1.dstName) cleanMerge = False if not cacheOnly: - updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName, - updateCache=True, updateWd=False) + setIndexStages(ren1.dstName, + ren1.srcSha, ren1.srcMode, + ren1.dstSha, ren1.dstMode, + ren2.dstSha, ren2.dstMode) + updateFile(clean, resSha, resMode, ren1.dstName) else: + removeFile(True, ren1.srcName) + # Renamed in 1, maybe changed in 2 if renamesA == renames1: stage = 3 @@ -612,25 +670,25 @@ def processRenames(renamesA, renamesB, branchNameA, branchNameB): if ren1.dstName in currentDirectorySet: newPath = uniquePath(ren1.dstName, branchName1) - print 'CONFLICT (rename/directory): Rename', \ - fmtRename(ren1.srcName, ren1.dstName), 'in', branchName1,\ - 'directory', ren1.dstName, 'added in', branchName2 - print 'Renaming', ren1.srcName, 'to', newPath, 'instead' + output('CONFLICT (rename/directory): Rename', + fmtRename(ren1.srcName, ren1.dstName), 'in', branchName1, + 'directory', ren1.dstName, 'added in', branchName2) + output('Renaming', ren1.srcName, 'to', newPath, 'instead') cleanMerge = False removeFile(False, ren1.dstName) updateFile(False, ren1.dstSha, ren1.dstMode, newPath) elif srcShaOtherBranch == None: - print 'CONFLICT (rename/delete): Rename', \ - fmtRename(ren1.srcName, ren1.dstName), 'in', \ - branchName1, 'and deleted in', branchName2 + output('CONFLICT (rename/delete): Rename', + fmtRename(ren1.srcName, ren1.dstName), 'in', + branchName1, 'and deleted in', branchName2) cleanMerge = False updateFile(False, ren1.dstSha, ren1.dstMode, ren1.dstName) elif dstShaOtherBranch: newPath = uniquePath(ren1.dstName, branchName2) - print 'CONFLICT (rename/add): Rename', \ - fmtRename(ren1.srcName, ren1.dstName), 'in', \ - branchName1 + '.', ren1.dstName, 'added in', branchName2 - print 'Adding as', newPath, 'instead' + output('CONFLICT (rename/add): Rename', + fmtRename(ren1.srcName, ren1.dstName), 'in', + branchName1 + '.', ren1.dstName, 'added in', branchName2) + output('Adding as', newPath, 'instead') updateFile(False, dstShaOtherBranch, dstModeOtherBranch, newPath) cleanMerge = False tryMerge = True @@ -638,12 +696,12 @@ def processRenames(renamesA, renamesB, branchNameA, branchNameB): dst2 = renames2.getDst(ren1.dstName) newPath1 = uniquePath(ren1.dstName, branchName1) newPath2 = uniquePath(dst2.dstName, branchName2) - print 'CONFLICT (rename/rename): Rename', \ - fmtRename(ren1.srcName, ren1.dstName), 'in', \ - branchName1+'. Rename', \ - fmtRename(dst2.srcName, dst2.dstName), 'in', branchName2 - print 'Renaming', ren1.srcName, 'to', newPath1, 'and', \ - dst2.srcName, 'to', newPath2, 'instead' + output('CONFLICT (rename/rename): Rename', + fmtRename(ren1.srcName, ren1.dstName), 'in', + branchName1+'. Rename', + fmtRename(dst2.srcName, dst2.dstName), 'in', branchName2) + output('Renaming', ren1.srcName, 'to', newPath1, 'and', + dst2.srcName, 'to', newPath2, 'instead') removeFile(False, ren1.dstName) updateFile(False, ren1.dstSha, ren1.dstMode, newPath1) updateFile(False, dst2.dstSha, dst2.dstMode, newPath2) @@ -653,23 +711,42 @@ def processRenames(renamesA, renamesB, branchNameA, branchNameB): tryMerge = True if tryMerge: - print 'Renaming', fmtRename(ren1.srcName, ren1.dstName) + + oName, oSHA1, oMode = ren1.srcName, ren1.srcSha, ren1.srcMode + aName, bName = ren1.dstName, ren1.srcName + aSHA1, bSHA1 = ren1.dstSha, srcShaOtherBranch + aMode, bMode = ren1.dstMode, srcModeOtherBranch + aBranch, bBranch = branchName1, branchName2 + + if renamesA != renames1: + aName, bName = bName, aName + aSHA1, bSHA1 = bSHA1, aSHA1 + aMode, bMode = bMode, aMode + aBranch, bBranch = bBranch, aBranch + [resSha, resMode, clean, merge] = \ - mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode, - ren1.dstName, ren1.dstSha, ren1.dstMode, - ren1.srcName, srcShaOtherBranch, srcModeOtherBranch, - branchName1, branchName2) + mergeFile(oName, oSHA1, oMode, + aName, aSHA1, aMode, + bName, bSHA1, bMode, + aBranch, bBranch); + + if merge or not clean: + output('Renaming', fmtRename(ren1.srcName, ren1.dstName)) if merge: - print 'Auto-merging', ren1.dstName + output('Auto-merging', ren1.dstName) if not clean: - print 'CONFLICT (rename/modify): Merge conflict in', ren1.dstName + output('CONFLICT (rename/modify): Merge conflict in', + ren1.dstName) cleanMerge = False if not cacheOnly: - updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName, - updateCache=True, updateWd=False) + setIndexStages(ren1.dstName, + oSHA1, oMode, + aSHA1, aMode, + bSHA1, bMode) + updateFile(clean, resSha, resMode, ren1.dstName) return cleanMerge @@ -709,21 +786,21 @@ def processEntry(entry, branch1Name, branch2Name): (not aSha and bSha == oSha): # Deleted in both or deleted in one and unchanged in the other if aSha: - print 'Removing', path + output('Removing', path) removeFile(True, path) else: # Deleted in one and changed in the other cleanMerge = False if not aSha: - print 'CONFLICT (delete/modify):', path, 'deleted in', \ - branch1Name, 'and modified in', branch2Name + '.', \ - 'Version', branch2Name, 'of', path, 'left in tree.' + output('CONFLICT (delete/modify):', path, 'deleted in', + branch1Name, 'and modified in', branch2Name + '.', + 'Version', branch2Name, 'of', path, 'left in tree.') mode = bMode sha = bSha else: - print 'CONFLICT (modify/delete):', path, 'deleted in', \ - branch2Name, 'and modified in', branch1Name + '.', \ - 'Version', branch1Name, 'of', path, 'left in tree.' + output('CONFLICT (modify/delete):', path, 'deleted in', + branch2Name, 'and modified in', branch1Name + '.', + 'Version', branch1Name, 'of', path, 'left in tree.') mode = aMode sha = aSha @@ -750,14 +827,14 @@ def processEntry(entry, branch1Name, branch2Name): if path in currentDirectorySet: cleanMerge = False newPath = uniquePath(path, addBranch) - print 'CONFLICT (' + conf + '):', \ - 'There is a directory with name', path, 'in', \ - otherBranch + '. Adding', path, 'as', newPath + output('CONFLICT (' + conf + '):', + 'There is a directory with name', path, 'in', + otherBranch + '. Adding', path, 'as', newPath) removeFile(False, path) updateFile(False, sha, mode, newPath) else: - print 'Adding', path + output('Adding', path) updateFile(True, sha, mode, path) elif not oSha and aSha and bSha: @@ -767,10 +844,10 @@ def processEntry(entry, branch1Name, branch2Name): 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 + output('CONFLICT: File', path, + 'added identically in both branches, but permissions', + 'conflict', '0%o' % aMode, '->', '0%o' % bMode) + output('CONFLICT: adding with permission:', '0%o' % aMode) updateFile(False, aSha, aMode, path) else: @@ -780,9 +857,9 @@ def processEntry(entry, branch1Name, branch2Name): cleanMerge = False newPath1 = uniquePath(path, branch1Name) newPath2 = uniquePath(path, branch2Name) - print 'CONFLICT (add/add): File', path, \ - 'added non-identically in both branches. Adding as', \ - newPath1, 'and', newPath2, 'instead.' + output('CONFLICT (add/add): File', path, + 'added non-identically in both branches. Adding as', + newPath1, 'and', newPath2, 'instead.') removeFile(False, path) updateFile(False, aSha, aMode, newPath1) updateFile(False, bSha, bMode, newPath2) @@ -791,7 +868,7 @@ def processEntry(entry, branch1Name, branch2Name): # # case D: Modified in both, but differently. # - print 'Auto-merging', path + output('Auto-merging', path) [sha, mode, clean, dummy] = \ mergeFile(path, oSha, oMode, path, aSha, aMode, @@ -801,13 +878,11 @@ def processEntry(entry, branch1Name, branch2Name): updateFile(True, sha, mode, path) else: cleanMerge = False - print 'CONFLICT (content): Merge conflict in', path + output('CONFLICT (content): Merge conflict in', path) if cacheOnly: updateFile(False, sha, mode, path) else: - updateFileExt(aSha, aMode, path, - updateCache=True, updateWd=False) updateFileExt(sha, mode, path, updateCache=False, updateWd=True) else: die("ERROR: Fatal merge failure, shouldn't happen.") @@ -819,12 +894,11 @@ def usage(): # 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() +bases = [] for nextArg in xrange(1, len(sys.argv)): if sys.argv[nextArg] == '--': if len(sys.argv) != nextArg + 3: @@ -835,6 +909,8 @@ for nextArg in xrange(1, len(sys.argv)): except IndexError: usage() break + else: + bases.append(sys.argv[nextArg]) print 'Merging', h1, 'with', h2 @@ -842,10 +918,17 @@ try: h1 = runProgram(['git-rev-parse', '--verify', h1 + '^0']).rstrip() h2 = runProgram(['git-rev-parse', '--verify', h2 + '^0']).rstrip() - graph = buildGraph([h1, h2]) - - [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2], - firstBranch, secondBranch, graph) + if len(bases) == 1: + base = runProgram(['git-rev-parse', '--verify', + bases[0] + '^0']).rstrip() + ancestor = Commit(base, None) + [dummy, clean] = merge(Commit(h1, None), Commit(h2, None), + firstBranch, secondBranch, None, 0, + ancestor) + else: + graph = buildGraph([h1, h2]) + [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2], + firstBranch, secondBranch, graph) print '' except: