mirror of
https://github.com/git/git
synced 2024-10-28 19:25:47 +00:00
724b511d4f
This changes the way the case two branches rename the same path to different paths is handled. Earlier, the code removed the original path and added both destinations to the index at stage0. This commit changes it to leave the original path at stage1, and two destination paths at stage2 and stage3, respectively. [jc: I am not really sure if this makes much difference in the real life merge situations. What should happen when our branch renames A to B and M to N, while their branch renames A to M? That is, M remains in our tree as is.] Signed-off-by: Junio C Hamano <junkio@cox.net>
928 lines
30 KiB
Python
Executable file
928 lines
30 KiB
Python
Executable file
#!/usr/bin/python
|
|
#
|
|
# Copyright (C) 2005 Fredrik Kuivinen
|
|
#
|
|
|
|
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
|
|
|
|
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') + \
|
|
'/merge-recursive-tmp-index'
|
|
def setupIndex(temporary):
|
|
try:
|
|
os.unlink(temporaryIndexFile)
|
|
except OSError:
|
|
pass
|
|
if temporary:
|
|
newIndex = temporaryIndexFile
|
|
else:
|
|
newIndex = originalIndexFile
|
|
os.environ['GIT_INDEX_FILE'] = newIndex
|
|
|
|
# This is a global variable which is used in a number of places but
|
|
# only written to in the 'merge' function.
|
|
|
|
# cacheOnly == True => Don't leave any non-stage 0 entries in the cache and
|
|
# don't update the working directory.
|
|
# False => Leave unmerged entries in the cache and update
|
|
# the working directory.
|
|
|
|
cacheOnly = False
|
|
|
|
# The entry point to the 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))
|
|
|
|
global outputIndent
|
|
|
|
output('Merging:')
|
|
output(h1)
|
|
output(h2)
|
|
sys.stdout.flush()
|
|
|
|
ca = getCommonAncestors(graph, h1, h2)
|
|
output('found', len(ca), 'common ancestor(s):')
|
|
for x in ca:
|
|
output(x)
|
|
sys.stdout.flush()
|
|
|
|
mergedCA = ca[0]
|
|
for h in ca[1:]:
|
|
outputIndent = callDepth+1
|
|
[mergedCA, dummy] = merge(mergedCA, h,
|
|
'Temporary merge branch 1',
|
|
'Temporary merge branch 2',
|
|
graph, callDepth+1)
|
|
outputIndent = callDepth
|
|
assert(isinstance(mergedCA, Commit))
|
|
|
|
global cacheOnly
|
|
if callDepth == 0:
|
|
setupIndex(False)
|
|
cacheOnly = False
|
|
else:
|
|
setupIndex(True)
|
|
runProgram(['git-read-tree', h1.tree()])
|
|
cacheOnly = True
|
|
|
|
[shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(),
|
|
branch1Name, branch2Name)
|
|
|
|
if clean or cacheOnly:
|
|
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', '-t', 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]
|
|
|
|
# Those two global variables are used in a number of places but only
|
|
# written to in 'mergeTrees' and 'uniquePath'. They keep track of
|
|
# every file and directory in the two branches that are about to be
|
|
# merged.
|
|
currentFileSet = None
|
|
currentDirectorySet = None
|
|
|
|
def mergeTrees(head, merge, common, branch1Name, branch2Name):
|
|
'''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:
|
|
output('Already uptodate!')
|
|
return [head, True]
|
|
|
|
if cacheOnly:
|
|
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)
|
|
|
|
[tree, code] = runProgram('git-write-tree', returnCode=True)
|
|
tree = tree.rstrip()
|
|
if code != 0:
|
|
global currentFileSet, currentDirectorySet
|
|
[currentFileSet, currentDirectorySet] = getFilesAndDirs(head)
|
|
[filesM, dirsM] = getFilesAndDirs(merge)
|
|
currentFileSet.union_update(filesM)
|
|
currentDirectorySet.union_update(dirsM)
|
|
|
|
entries = unmergedCacheEntries()
|
|
renamesHead = getRenames(head, common, head, merge, entries)
|
|
renamesMerge = getRenames(merge, common, head, merge, entries)
|
|
|
|
cleanMerge = processRenames(renamesHead, renamesMerge,
|
|
branch1Name, branch2Name)
|
|
for entry in entries:
|
|
if entry.processed:
|
|
continue
|
|
if not processEntry(entry, branch1Name, branch2Name):
|
|
cleanMerge = False
|
|
|
|
if cleanMerge or cacheOnly:
|
|
tree = runProgram('git-write-tree').rstrip()
|
|
else:
|
|
tree = None
|
|
else:
|
|
cleanMerge = True
|
|
|
|
return [tree, cleanMerge]
|
|
|
|
# Low level file merging, update and removal
|
|
# ------------------------------------------
|
|
|
|
def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode,
|
|
branch1Name, branch2Name):
|
|
|
|
merge = False
|
|
clean = True
|
|
|
|
if stat.S_IFMT(aMode) != stat.S_IFMT(bMode):
|
|
clean = False
|
|
if stat.S_ISREG(aMode):
|
|
mode = aMode
|
|
sha = aSha
|
|
else:
|
|
mode = bMode
|
|
sha = bSha
|
|
else:
|
|
if aSha != oSha and bSha != oSha:
|
|
merge = True
|
|
|
|
if aMode == oMode:
|
|
mode = bMode
|
|
else:
|
|
mode = aMode
|
|
|
|
if aSha == oSha:
|
|
sha = bSha
|
|
elif bSha == oSha:
|
|
sha = aSha
|
|
elif stat.S_ISREG(aMode):
|
|
assert(stat.S_ISREG(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)
|
|
|
|
sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
|
|
src1]).rstrip()
|
|
|
|
os.unlink(orig)
|
|
os.unlink(src1)
|
|
os.unlink(src2)
|
|
|
|
clean = (code == 0)
|
|
else:
|
|
assert(stat.S_ISLNK(aMode) and stat.S_ISLNK(bMode))
|
|
sha = aSha
|
|
|
|
if aSha != bSha:
|
|
clean = False
|
|
|
|
return [sha, mode, clean, merge]
|
|
|
|
def updateFile(clean, sha, mode, path):
|
|
updateCache = cacheOnly or clean
|
|
updateWd = not cacheOnly
|
|
|
|
return updateFileExt(sha, mode, path, updateCache, updateWd)
|
|
|
|
def updateFileExt(sha, mode, path, updateCache, updateWd):
|
|
if cacheOnly:
|
|
updateWd = False
|
|
|
|
if updateWd:
|
|
pathComponents = path.split('/')
|
|
for x in xrange(1, len(pathComponents)):
|
|
p = '/'.join(pathComponents[0:x])
|
|
|
|
try:
|
|
createDir = not stat.S_ISDIR(os.lstat(p).st_mode)
|
|
except OSError:
|
|
createDir = True
|
|
|
|
if createDir:
|
|
try:
|
|
os.mkdir(p)
|
|
except OSError, e:
|
|
die("Couldn't create directory", p, e.strerror)
|
|
|
|
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 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
|
|
|
|
if updateCache:
|
|
runProgram(['git-update-index', '--force-remove', '--', path])
|
|
|
|
if updateWd:
|
|
try:
|
|
os.unlink(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):
|
|
try:
|
|
os.lstat(path)
|
|
return True
|
|
except OSError, e:
|
|
if e.errno == errno.ENOENT:
|
|
return False
|
|
else:
|
|
raise
|
|
|
|
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)
|
|
currentFileSet.add(newPath)
|
|
return newPath
|
|
|
|
# Cache entry management
|
|
# ----------------------
|
|
|
|
class CacheEntry:
|
|
def __init__(self, path):
|
|
class Stage:
|
|
def __init__(self):
|
|
self.sha1 = None
|
|
self.mode = None
|
|
|
|
# Used for debugging only
|
|
def __str__(self):
|
|
if self.mode != None:
|
|
m = '0%o' % self.mode
|
|
else:
|
|
m = 'None'
|
|
|
|
if self.sha1:
|
|
sha1 = self.sha1
|
|
else:
|
|
sha1 = 'None'
|
|
return 'sha1: ' + sha1 + ' mode: ' + m
|
|
|
|
self.stages = [Stage(), Stage(), Stage(), Stage()]
|
|
self.path = path
|
|
self.processed = False
|
|
|
|
def __str__(self):
|
|
return 'path: ' + self.path + ' stages: ' + repr([str(x) for x in self.stages])
|
|
|
|
class CacheEntryContainer:
|
|
def __init__(self):
|
|
self.entries = {}
|
|
|
|
def add(self, entry):
|
|
self.entries[entry.path] = entry
|
|
|
|
def get(self, path):
|
|
return self.entries.get(path)
|
|
|
|
def __iter__(self):
|
|
return self.entries.itervalues()
|
|
|
|
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 = CacheEntryContainer()
|
|
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))
|
|
path = m.group(4)
|
|
|
|
e = res.get(path)
|
|
if not e:
|
|
e = CacheEntry(path)
|
|
res.add(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
|
|
|
|
lsTreeRE = re.compile(r'^([0-7]+) (\S+) ([0-9a-f]{40})\t(.*)\n$', re.S)
|
|
def getCacheEntry(path, origTree, aTree, bTree):
|
|
'''Returns a CacheEntry object which doesn't have to correspond to
|
|
a real cache entry in Git's index.'''
|
|
|
|
def parse(out):
|
|
if out == '':
|
|
return [None, None]
|
|
else:
|
|
m = lsTreeRE.match(out)
|
|
if not m:
|
|
die('Unexpected output from git-ls-tree:', out)
|
|
elif m.group(2) == 'blob':
|
|
return [m.group(3), int(m.group(1), 8)]
|
|
else:
|
|
return [None, None]
|
|
|
|
res = CacheEntry(path)
|
|
|
|
[oSha, oMode] = parse(runProgram(['git-ls-tree', origTree, '--', path]))
|
|
[aSha, aMode] = parse(runProgram(['git-ls-tree', aTree, '--', path]))
|
|
[bSha, bMode] = parse(runProgram(['git-ls-tree', bTree, '--', path]))
|
|
|
|
res.stages[1].sha1 = oSha
|
|
res.stages[1].mode = oMode
|
|
res.stages[2].sha1 = aSha
|
|
res.stages[2].mode = aMode
|
|
res.stages[3].sha1 = bSha
|
|
res.stages[3].mode = bMode
|
|
|
|
return res
|
|
|
|
# Rename detection and handling
|
|
# -----------------------------
|
|
|
|
class RenameEntry:
|
|
def __init__(self,
|
|
src, srcSha, srcMode, srcCacheEntry,
|
|
dst, dstSha, dstMode, dstCacheEntry,
|
|
score):
|
|
self.srcName = src
|
|
self.srcSha = srcSha
|
|
self.srcMode = srcMode
|
|
self.srcCacheEntry = srcCacheEntry
|
|
self.dstName = dst
|
|
self.dstSha = dstSha
|
|
self.dstMode = dstMode
|
|
self.dstCacheEntry = dstCacheEntry
|
|
self.score = score
|
|
|
|
self.processed = False
|
|
|
|
class RenameEntryContainer:
|
|
def __init__(self):
|
|
self.entriesSrc = {}
|
|
self.entriesDst = {}
|
|
|
|
def add(self, entry):
|
|
self.entriesSrc[entry.srcName] = entry
|
|
self.entriesDst[entry.dstName] = entry
|
|
|
|
def getSrc(self, path):
|
|
return self.entriesSrc.get(path)
|
|
|
|
def getDst(self, path):
|
|
return self.entriesDst.get(path)
|
|
|
|
def __iter__(self):
|
|
return self.entriesSrc.itervalues()
|
|
|
|
parseDiffRenamesRE = re.compile('^:([0-7]+) ([0-7]+) ([0-9a-f]{40}) ([0-9a-f]{40}) R([0-9]*)$')
|
|
def getRenames(tree, oTree, aTree, bTree, cacheEntries):
|
|
'''Get information of all renames which occured between 'oTree' and
|
|
'tree'. We need the three trees in the merge ('oTree', 'aTree' and
|
|
'bTree') to be able to associate the correct cache entries with
|
|
the rename information. 'tree' is always equal to either aTree or bTree.'''
|
|
|
|
assert(tree == aTree or tree == bTree)
|
|
inp = runProgram(['git-diff-tree', '-M', '--diff-filter=R', '-r',
|
|
'-z', oTree, tree])
|
|
|
|
ret = RenameEntryContainer()
|
|
try:
|
|
recs = inp.split("\0")
|
|
recs.pop() # remove last entry (which is '')
|
|
it = recs.__iter__()
|
|
while True:
|
|
rec = it.next()
|
|
m = parseDiffRenamesRE.match(rec)
|
|
|
|
if not m:
|
|
die('Unexpected output from git-diff-tree:', rec)
|
|
|
|
srcMode = int(m.group(1), 8)
|
|
dstMode = int(m.group(2), 8)
|
|
srcSha = m.group(3)
|
|
dstSha = m.group(4)
|
|
score = m.group(5)
|
|
src = it.next()
|
|
dst = it.next()
|
|
|
|
srcCacheEntry = cacheEntries.get(src)
|
|
if not srcCacheEntry:
|
|
srcCacheEntry = getCacheEntry(src, oTree, aTree, bTree)
|
|
cacheEntries.add(srcCacheEntry)
|
|
|
|
dstCacheEntry = cacheEntries.get(dst)
|
|
if not dstCacheEntry:
|
|
dstCacheEntry = getCacheEntry(dst, oTree, aTree, bTree)
|
|
cacheEntries.add(dstCacheEntry)
|
|
|
|
ret.add(RenameEntry(src, srcSha, srcMode, srcCacheEntry,
|
|
dst, dstSha, dstMode, dstCacheEntry,
|
|
score))
|
|
except StopIteration:
|
|
pass
|
|
return ret
|
|
|
|
def fmtRename(src, dst):
|
|
srcPath = src.split('/')
|
|
dstPath = dst.split('/')
|
|
path = []
|
|
endIndex = min(len(srcPath), len(dstPath)) - 1
|
|
for x in range(0, endIndex):
|
|
if srcPath[x] == dstPath[x]:
|
|
path.append(srcPath[x])
|
|
else:
|
|
endIndex = x
|
|
break
|
|
|
|
if len(path) > 0:
|
|
return '/'.join(path) + \
|
|
'/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
|
|
'/'.join(dstPath[endIndex:]) + '}'
|
|
else:
|
|
return src + ' => ' + dst
|
|
|
|
def processRenames(renamesA, renamesB, branchNameA, branchNameB):
|
|
srcNames = Set()
|
|
for x in renamesA:
|
|
srcNames.add(x.srcName)
|
|
for x in renamesB:
|
|
srcNames.add(x.srcName)
|
|
|
|
cleanMerge = True
|
|
for path in srcNames:
|
|
if renamesA.getSrc(path):
|
|
renames1 = renamesA
|
|
renames2 = renamesB
|
|
branchName1 = branchNameA
|
|
branchName2 = branchNameB
|
|
else:
|
|
renames1 = renamesB
|
|
renames2 = renamesA
|
|
branchName1 = branchNameB
|
|
branchName2 = branchNameA
|
|
|
|
ren1 = renames1.getSrc(path)
|
|
ren2 = renames2.getSrc(path)
|
|
|
|
ren1.dstCacheEntry.processed = True
|
|
ren1.srcCacheEntry.processed = True
|
|
|
|
if ren1.processed:
|
|
continue
|
|
|
|
ren1.processed = True
|
|
|
|
if ren2:
|
|
# Renamed in 1 and renamed in 2
|
|
assert(ren1.srcName == ren2.srcName)
|
|
ren2.dstCacheEntry.processed = True
|
|
ren2.processed = True
|
|
|
|
if ren1.dstName != ren2.dstName:
|
|
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)
|
|
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)
|
|
output(ren2.dstName, 'is a directory in', branchName1,
|
|
'adding as', dstName2, 'instead.')
|
|
removeFile(False, ren2.dstName)
|
|
else:
|
|
dstName2 = ren2.dstName
|
|
setIndexStages(dstName1,
|
|
None, None,
|
|
ren1.dstSha, ren1.dstMode,
|
|
None, None)
|
|
setIndexStages(dstName2,
|
|
None, None,
|
|
None, None,
|
|
ren2.dstSha, ren2.dstMode)
|
|
|
|
else:
|
|
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:
|
|
output('Auto-merging', ren1.dstName)
|
|
|
|
if not clean:
|
|
output('CONFLICT (content): merge conflict in',
|
|
ren1.dstName)
|
|
cleanMerge = False
|
|
|
|
if not cacheOnly:
|
|
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
|
|
else:
|
|
stage = 2
|
|
|
|
srcShaOtherBranch = ren1.srcCacheEntry.stages[stage].sha1
|
|
srcModeOtherBranch = ren1.srcCacheEntry.stages[stage].mode
|
|
|
|
dstShaOtherBranch = ren1.dstCacheEntry.stages[stage].sha1
|
|
dstModeOtherBranch = ren1.dstCacheEntry.stages[stage].mode
|
|
|
|
tryMerge = False
|
|
|
|
if ren1.dstName in currentDirectorySet:
|
|
newPath = uniquePath(ren1.dstName, branchName1)
|
|
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:
|
|
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)
|
|
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
|
|
elif renames2.getDst(ren1.dstName):
|
|
dst2 = renames2.getDst(ren1.dstName)
|
|
newPath1 = uniquePath(ren1.dstName, branchName1)
|
|
newPath2 = uniquePath(dst2.dstName, branchName2)
|
|
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)
|
|
dst2.processed = True
|
|
cleanMerge = False
|
|
else:
|
|
tryMerge = True
|
|
|
|
if tryMerge:
|
|
|
|
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(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:
|
|
output('Auto-merging', ren1.dstName)
|
|
|
|
if not clean:
|
|
output('CONFLICT (rename/modify): Merge conflict in',
|
|
ren1.dstName)
|
|
cleanMerge = False
|
|
|
|
if not cacheOnly:
|
|
setIndexStages(ren1.dstName,
|
|
oSHA1, oMode,
|
|
aSHA1, aMode,
|
|
bSHA1, bMode)
|
|
|
|
updateFile(clean, resSha, resMode, ren1.dstName)
|
|
|
|
return cleanMerge
|
|
|
|
# Per entry merge function
|
|
# ------------------------
|
|
|
|
def processEntry(entry, branch1Name, branch2Name):
|
|
'''Merge one cache entry.'''
|
|
|
|
debug('processing', entry.path, 'clean cache:', cacheOnly)
|
|
|
|
cleanMerge = True
|
|
|
|
path = entry.path
|
|
oSha = entry.stages[1].sha1
|
|
oMode = entry.stages[1].mode
|
|
aSha = entry.stages[2].sha1
|
|
aMode = entry.stages[2].mode
|
|
bSha = entry.stages[3].sha1
|
|
bMode = entry.stages[3].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:
|
|
output('Removing', path)
|
|
removeFile(True, path)
|
|
else:
|
|
# Deleted in one and changed in the other
|
|
cleanMerge = False
|
|
if not aSha:
|
|
output('CONFLICT (delete/modify):', path, 'deleted in',
|
|
branch1Name, 'and modified in', branch2Name + '.',
|
|
'Version', branch2Name, 'of', path, 'left in tree.')
|
|
mode = bMode
|
|
sha = bSha
|
|
else:
|
|
output('CONFLICT (modify/delete):', 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/directory'
|
|
else:
|
|
addBranch = branch2Name
|
|
otherBranch = branch1Name
|
|
mode = bMode
|
|
sha = bSha
|
|
conf = 'directory/file'
|
|
|
|
if path in currentDirectorySet:
|
|
cleanMerge = False
|
|
newPath = uniquePath(path, addBranch)
|
|
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:
|
|
output('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
|
|
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:
|
|
# This case is handled by git-read-tree
|
|
assert(False)
|
|
else:
|
|
cleanMerge = False
|
|
newPath1 = uniquePath(path, branch1Name)
|
|
newPath2 = uniquePath(path, branch2Name)
|
|
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)
|
|
|
|
elif oSha and aSha and bSha:
|
|
#
|
|
# case D: Modified in both, but differently.
|
|
#
|
|
output('Auto-merging', path)
|
|
[sha, mode, clean, dummy] = \
|
|
mergeFile(path, oSha, oMode,
|
|
path, aSha, aMode,
|
|
path, bSha, bMode,
|
|
branch1Name, branch2Name)
|
|
if clean:
|
|
updateFile(True, sha, mode, path)
|
|
else:
|
|
cleanMerge = False
|
|
output('CONFLICT (content): Merge conflict in', path)
|
|
|
|
if cacheOnly:
|
|
updateFile(False, sha, mode, path)
|
|
else:
|
|
updateFileExt(sha, mode, path, updateCache=False, updateWd=True)
|
|
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])
|
|
|
|
[dummy, 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)
|