diff options
author | Fredrik Kuivinen <freku045@student.liu.se> | 2005-09-13 08:22:26 +0200 |
---|---|---|
committer | Junio C Hamano <junkio@cox.net> | 2005-09-13 00:20:54 -0700 |
commit | e4cf17ce0db2dab7c9525a732f86c5e3df3b4ed0 (patch) | |
tree | e311046e26ff830a2232067e6ad00ba06db95e2d /git-merge-recursive.py | |
parent | f88ed172e7a391bd907798ad2a3347a83cd24317 (diff) | |
download | git-e4cf17ce0db2dab7c9525a732f86c5e3df3b4ed0.tar.gz |
[PATCH] Rename the 'fredrik' merge strategy to 'recursive'.
Otherwise we would regret when Fredrik comes up with another merge
algorithm with different pros-and-cons with the current one.
Signed-off-by: Fredrik Kuivinen <freku045@student.liu.se>
Signed-off-by: Junio C Hamano <junkio@cox.net>
Diffstat (limited to 'git-merge-recursive.py')
-rwxr-xr-x | git-merge-recursive.py | 429 |
1 files changed, 429 insertions, 0 deletions
diff --git a/git-merge-recursive.py b/git-merge-recursive.py new file mode 100755 index 0000000000..27b84867b0 --- /dev/null +++ b/git-merge-recursive.py @@ -0,0 +1,429 @@ +#!/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 * + +alwaysWriteTree = False + +# The actual merge code +# --------------------- + +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: + if len(ca) > 1: + runProgram(['git-read-tree', h1.tree()]) + runProgram(['git-update-cache', '-q', '--refresh']) + # Use the original index if we only have one common ancestor + + updateWd = True + if alwaysWriteTree: + cleanCache = True + else: + cleanCache = False + else: + runProgram(['git-read-tree', h1.tree()]) + updateWd = False + cleanCache = True + + [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), Ms.tree(), + branch1Name, branch2Name, + cleanCache, updateWd) + + if clean or cleanCache: + res = Commit(None, [h1, h2], tree=shaRes) + graph.addNode(res) + else: + res = None + + return [res, clean] + +getFilesRE = re.compile('([0-9]+) ([a-z0-9]+) ([0-9a-f]{40})\t(.*)') +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('^([0-9]+) ([0-9a-f]{40}) ([1-3])\t(.*)$') +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, updateWd): + '''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 updateWd: + updateArg = '-u' + else: + updateArg = '-i' + 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, updateWd): + 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, updateWd): + '''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. +# False => Leave unmerged entries + +# updateWd == True => Update the working directory to correspond to the cache +# False => Leave the working directory unchanged + +# 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): + if cleanCache or (not cleanCache and clean): + runProgram(['git-update-cache', '--add', '--cacheinfo', + '0%o' % mode, sha, path]) + + 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) + runProgram(['git-update-cache', '--', path]) + + def removeFile(clean, path): + if cleanCache or (not cleanCache and clean): + runProgram(['git-update-cache', '--force-remove', '--', path]) + + if updateWd: + 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, + 'wd:', updateWd) + + 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,' + print 'CONFLICT: 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.', \ + 'Adding "' + newPath1 + '" and "' + newPath2 + '" instead.' + 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 + '".' + updateFile(False, sha, mode, path) + 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], ' <base>... -- <head> <remote>..') + +# 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: + print 'Automatic merge failed, fix up by hand' + sys.exit(1) |