You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
445 lines
14 KiB
445 lines
14 KiB
#!/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' |
|
|
|
[out, code] = runProgram(['git-read-tree', updateArg, '-m', common, head, merge], returnCode = True) |
|
if code != 0: |
|
die('git-read-tree:', out) |
|
|
|
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], ' <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: |
|
if isinstance(sys.exc_info()[1], SystemExit): |
|
raise |
|
else: |
|
traceback.print_exc(None, sys.stderr) |
|
sys.exit(2) |
|
|
|
if clean: |
|
sys.exit(0) |
|
else: |
|
sys.exit(1)
|
|
|