[PATCH] Teach the recursive merge strategy about renames.

It will now merge cases where a file was renamed in one branch and
modified in the other branch cleanly. We also detect a couple of
conflict cases now that wasn't detected before.

Signed-off-by: Fredrik Kuivinen <freku045@student.liu.se>
Signed-off-by: Junio C Hamano <junkio@cox.net>
This commit is contained in:
Fredrik Kuivinen
2005-10-02 17:43:07 +02:00
committed by Junio C Hamano
parent 7dd43575f8
commit af215114f5

View File

@ -7,9 +7,6 @@ from sets import Set
sys.path.append('@@GIT_PYTHON_PATH@@') sys.path.append('@@GIT_PYTHON_PATH@@')
from gitMergeCommon import * from gitMergeCommon import *
# The actual merge code
# ---------------------
originalIndexFile = os.environ.get('GIT_INDEX_FILE', originalIndexFile = os.environ.get('GIT_INDEX_FILE',
os.environ.get('GIT_DIR', '.git') + '/index') os.environ.get('GIT_DIR', '.git') + '/index')
temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \ temporaryIndexFile = os.environ.get('GIT_DIR', '.git') + \
@ -21,11 +18,23 @@ def setupIndex(temporary):
pass pass
if temporary: if temporary:
newIndex = temporaryIndexFile newIndex = temporaryIndexFile
os.environ
else: else:
newIndex = originalIndexFile newIndex = originalIndexFile
os.environ['GIT_INDEX_FILE'] = newIndex 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): def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
'''Merge the commits h1 and h2, return the resulting virtual '''Merge the commits h1 and h2, return the resulting virtual
commit object and a flag indicating the cleaness of the merge.''' commit object and a flag indicating the cleaness of the merge.'''
@ -35,6 +44,7 @@ def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
def infoMsg(*args): def infoMsg(*args):
sys.stdout.write(' '*callDepth) sys.stdout.write(' '*callDepth)
printList(args) printList(args)
infoMsg('Merging:') infoMsg('Merging:')
infoMsg(h1) infoMsg(h1)
infoMsg(h2) infoMsg(h2)
@ -46,27 +56,27 @@ def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
infoMsg(x) infoMsg(x)
sys.stdout.flush() sys.stdout.flush()
Ms = ca[0] mergedCA = ca[0]
for h in ca[1:]: for h in ca[1:]:
[Ms, ignore] = merge(Ms, h, [mergedCA, dummy] = merge(mergedCA, h,
'Temporary shared merge branch 1', 'Temporary shared merge branch 1',
'Temporary shared merge branch 2', 'Temporary shared merge branch 2',
graph, callDepth+1) graph, callDepth+1)
assert(isinstance(Ms, Commit)) assert(isinstance(mergedCA, Commit))
global cacheOnly
if callDepth == 0: if callDepth == 0:
setupIndex(False) setupIndex(False)
cleanCache = False cacheOnly = False
else: else:
setupIndex(True) setupIndex(True)
runProgram(['git-read-tree', h1.tree()]) runProgram(['git-read-tree', h1.tree()])
cleanCache = True cacheOnly = True
[shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), Ms.tree(), [shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(),
branch1Name, branch2Name, branch1Name, branch2Name)
cleanCache)
if clean or cleanCache: if clean or cacheOnly:
res = Commit(None, [h1, h2], tree=shaRes) res = Commit(None, [h1, h2], tree=shaRes)
graph.addNode(res) graph.addNode(res)
else: else:
@ -89,49 +99,14 @@ def getFilesAndDirs(tree):
return [files, dirs] return [files, dirs]
class CacheEntry: # Those two global variables are used in a number of places but only
def __init__(self, path): # written to in 'mergeTrees' and 'uniquePath'. They keep track of
class Stage: # every file and directory in the two branches that are about to be
def __init__(self): # merged.
self.sha1 = None currentFileSet = None
self.mode = None currentDirectorySet = 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 mergeTrees(head, merge, common, branch1Name, branch2Name):
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 '''Merge the trees 'head' and 'merge' with the common ancestor
'common'. The name of the head branch is 'branch1Name' and the name of 'common'. The name of the head branch is 'branch1Name' and the name of
the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge) the merge branch is 'branch2Name'. Return a tuple (tree, cleanMerge)
@ -144,33 +119,38 @@ def mergeTrees(head, merge, common, branch1Name, branch2Name,
print 'Already uptodate!' print 'Already uptodate!'
return [head, True] return [head, True]
if cleanCache: if cacheOnly:
updateArg = '-i' updateArg = '-i'
else: else:
updateArg = '-u' updateArg = '-u'
[out, code] = runProgram(['git-read-tree', updateArg, '-m', common, head, merge], returnCode = True) [out, code] = runProgram(['git-read-tree', updateArg, '-m',
common, head, merge], returnCode = True)
if code != 0: if code != 0:
die('git-read-tree:', out) die('git-read-tree:', out)
cleanMerge = True
[tree, code] = runProgram('git-write-tree', returnCode=True) [tree, code] = runProgram('git-write-tree', returnCode=True)
tree = tree.rstrip() tree = tree.rstrip()
if code != 0: if code != 0:
[files, dirs] = getFilesAndDirs(head) global currentFileSet, currentDirectorySet
[currentFileSet, currentDirectorySet] = getFilesAndDirs(head)
[filesM, dirsM] = getFilesAndDirs(merge) [filesM, dirsM] = getFilesAndDirs(merge)
files.union_update(filesM) currentFileSet.union_update(filesM)
dirs.union_update(dirsM) currentDirectorySet.union_update(dirsM)
cleanMerge = True
entries = unmergedCacheEntries() entries = unmergedCacheEntries()
for name in entries: renamesHead = getRenames(head, common, head, merge, entries)
if not processEntry(entries[name], branch1Name, branch2Name, renamesMerge = getRenames(merge, common, head, merge, entries)
files, dirs, cleanCache):
cleanMerge = processRenames(renamesHead, renamesMerge,
branch1Name, branch2Name)
for entry in entries:
if entry.processed:
continue
if not processEntry(entry, branch1Name, branch2Name):
cleanMerge = False cleanMerge = False
if cleanMerge or cleanCache: if cleanMerge or cacheOnly:
tree = runProgram('git-write-tree').rstrip() tree = runProgram('git-write-tree').rstrip()
else: else:
tree = None tree = None
@ -179,84 +159,538 @@ def mergeTrees(head, merge, common, branch1Name, branch2Name,
return [tree, cleanMerge] return [tree, cleanMerge]
def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache): # Low level file merging, update and removal
'''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 def mergeFile(oPath, oSha, oMode, aPath, aSha, aMode, bPath, bSha, bMode,
# don't update the working directory branch1Name, branch2Name):
# False => Leave unmerged entries and update the working directory
# clean == True => non-conflict case merge = False
# False => conflict case clean = True
# If cleanCache == False then the cache shouldn't be updated if clean == False 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
def updateFile(clean, sha, mode, path, onlyWd=False): if aMode == oMode:
updateCache = not onlyWd and (cleanCache or (not cleanCache and clean)) mode = bMode
updateWd = onlyWd or (not cleanCache and clean) else:
mode = aMode
if updateWd: if aSha == oSha:
prog = ['git-cat-file', 'blob', sha] sha = bSha
if stat.S_ISREG(mode): 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:
createDir = True
if createDir:
try: try:
os.unlink(path) os.mkdir(p)
except OSError: except OSError, e:
pass die("Couldn't create directory", p, e.strerror)
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: prog = ['git-cat-file', 'blob', sha]
runProgram(['git-update-index', '--add', '--', path]) if stat.S_ISREG(mode):
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: try:
os.unlink(path) os.unlink(path)
except OSError, e: except OSError:
if e.errno != errno.ENOENT and e.errno != errno.EISDIR: pass
raise 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)
def uniquePath(path, branch): if updateWd and updateCache:
newPath = path + '_' + branch runProgram(['git-update-index', '--add', '--', path])
suffix = 0 elif updateCache:
while newPath in files or newPath in dirs: runProgram(['git-update-index', '--add', '--cacheinfo',
suffix += 1 '0%o' % mode, sha, path])
newPath = path + '_' + branch + '_' + str(suffix)
files.add(newPath)
return newPath
debug('processing', entry.path, 'clean cache:', cleanCache) 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
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
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
removeFile(True, ren1.srcName)
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:
print '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.'
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.'
removeFile(False, ren2.dstName)
else:
dstName2 = ren1.dstName
updateFile(False, ren1.dstSha, ren1.dstMode, dstName1)
updateFile(False, ren2.dstSha, ren2.dstMode, dstName2)
else:
print 'Renaming', fmtRename(path, ren1.dstName)
[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:
print 'Auto-merging', ren1.dstName
if not clean:
print 'CONFLICT (content): merge conflict in', ren1.dstName
cleanMerge = False
if not cacheOnly:
updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
updateCache=True, updateWd=False)
updateFile(clean, resSha, resMode, ren1.dstName)
else:
# 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)
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'
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
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'
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)
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'
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:
print 'Renaming', fmtRename(ren1.srcName, ren1.dstName)
[resSha, resMode, clean, merge] = \
mergeFile(ren1.srcName, ren1.srcSha, ren1.srcMode,
ren1.dstName, ren1.dstSha, ren1.dstMode,
ren1.srcName, srcShaOtherBranch, srcModeOtherBranch,
branchName1, branchName2)
if merge:
print 'Auto-merging', ren1.dstName
if not clean:
print 'CONFLICT (rename/modify): Merge conflict in', ren1.dstName
cleanMerge = False
if not cacheOnly:
updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
updateCache=True, updateWd=False)
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 cleanMerge = True
path = entry.path path = entry.path
oSha = entry.stages[0].sha1 oSha = entry.stages[1].sha1
oMode = entry.stages[0].mode oMode = entry.stages[1].mode
aSha = entry.stages[1].sha1 aSha = entry.stages[2].sha1
aMode = entry.stages[1].mode aMode = entry.stages[2].mode
bSha = entry.stages[2].sha1 bSha = entry.stages[3].sha1
bMode = entry.stages[2].mode bMode = entry.stages[3].mode
assert(oSha == None or isSha(oSha)) assert(oSha == None or isSha(oSha))
assert(aSha == None or isSha(aSha)) assert(aSha == None or isSha(aSha))
@ -275,28 +709,26 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
(not aSha and bSha == oSha): (not aSha and bSha == oSha):
# Deleted in both or deleted in one and unchanged in the other # Deleted in both or deleted in one and unchanged in the other
if aSha: if aSha:
print 'Removing ' + path print 'Removing', path
removeFile(True, path) removeFile(True, path)
else: else:
# Deleted in one and changed in the other # Deleted in one and changed in the other
cleanMerge = False cleanMerge = False
if not aSha: if not aSha:
print 'CONFLICT (del/mod): "' + path + '" deleted in', \ print 'CONFLICT (delete/modify):', path, 'deleted in', \
branch1Name, 'and modified in', branch2Name, \ branch1Name, 'and modified in', branch2Name + '.', \
'. Version', branch2Name, ' of "' + path + \ 'Version', branch2Name, 'of', path, 'left in tree.'
'" left in tree'
mode = bMode mode = bMode
sha = bSha sha = bSha
else: else:
print 'CONFLICT (mod/del): "' + path + '" deleted in', \ print 'CONFLICT (modify/delete):', path, 'deleted in', \
branch2Name, 'and modified in', branch1Name + \ branch2Name, 'and modified in', branch1Name + '.', \
'. Version', branch1Name, 'of "' + path + \ 'Version', branch1Name, 'of', path, 'left in tree.'
'" left in tree'
mode = aMode mode = aMode
sha = aSha sha = aSha
updateFile(False, sha, mode, path) updateFile(False, sha, mode, path)
elif (not oSha and aSha and not bSha) or \ elif (not oSha and aSha and not bSha) or \
(not oSha and not aSha and bSha): (not oSha and not aSha and bSha):
# #
@ -307,27 +739,26 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
otherBranch = branch2Name otherBranch = branch2Name
mode = aMode mode = aMode
sha = aSha sha = aSha
conf = 'file/dir' conf = 'file/directory'
else: else:
addBranch = branch2Name addBranch = branch2Name
otherBranch = branch1Name otherBranch = branch1Name
mode = bMode mode = bMode
sha = bSha sha = bSha
conf = 'dir/file' conf = 'directory/file'
if path in dirs: if path in currentDirectorySet:
cleanMerge = False cleanMerge = False
newPath = uniquePath(path, addBranch) newPath = uniquePath(path, addBranch)
print 'CONFLICT (' + conf + \ print 'CONFLICT (' + conf + '):', \
'): There is a directory with name "' + path + '" in', \ 'There is a directory with name', path, 'in', \
otherBranch + '. Adding "' + path + '" as "' + newPath + '"' otherBranch + '. Adding', path, 'as', newPath
removeFile(False, path) removeFile(False, path)
path = newPath updateFile(False, sha, mode, newPath)
else: else:
print 'Adding "' + path + '"' print 'Adding', path
updateFile(True, sha, mode, path)
updateFile(True, sha, mode, path)
elif not oSha and aSha and bSha: elif not oSha and aSha and bSha:
# #
@ -336,10 +767,9 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
if aSha == bSha: if aSha == bSha:
if aMode != bMode: if aMode != bMode:
cleanMerge = False cleanMerge = False
print 'CONFLICT: File "' + path + \ print 'CONFLICT: File', path, \
'" added identically in both branches,', \ 'added identically in both branches, but permissions', \
'but permissions conflict', '0%o' % aMode, '->', \ 'conflict', '0%o' % aMode, '->', '0%o' % bMode
'0%o' % bMode
print 'CONFLICT: adding with permission:', '0%o' % aMode print 'CONFLICT: adding with permission:', '0%o' % aMode
updateFile(False, aSha, aMode, path) updateFile(False, aSha, aMode, path)
@ -350,8 +780,9 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
cleanMerge = False cleanMerge = False
newPath1 = uniquePath(path, branch1Name) newPath1 = uniquePath(path, branch1Name)
newPath2 = uniquePath(path, branch2Name) newPath2 = uniquePath(path, branch2Name)
print 'CONFLICT (add/add): File "' + path + \ print 'CONFLICT (add/add): File', path, \
'" added non-identically in both branches.' 'added non-identically in both branches. Adding as', \
newPath1, 'and', newPath2, 'instead.'
removeFile(False, path) removeFile(False, path)
updateFile(False, aSha, aMode, newPath1) updateFile(False, aSha, aMode, newPath1)
updateFile(False, bSha, bMode, newPath2) updateFile(False, bSha, bMode, newPath2)
@ -360,39 +791,24 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
# #
# case D: Modified in both, but differently. # case D: Modified in both, but differently.
# #
print 'Auto-merging', path print 'Auto-merging', path
orig = runProgram(['git-unpack-file', oSha]).rstrip() [sha, mode, clean, dummy] = \
src1 = runProgram(['git-unpack-file', aSha]).rstrip() mergeFile(path, oSha, oMode,
src2 = runProgram(['git-unpack-file', bSha]).rstrip() path, aSha, aMode,
[out, ret] = runProgram(['merge', path, bSha, bMode,
'-L', branch1Name + '/' + path, branch1Name, branch2Name)
'-L', 'orig/' + path, if clean:
'-L', branch2Name + '/' + path, updateFile(True, sha, mode, path)
src1, orig, src2], returnCode=True)
if aMode == oMode:
mode = bMode
else: else:
mode = aMode
sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
src1]).rstrip()
if ret != 0:
cleanMerge = False cleanMerge = False
print 'CONFLICT (content): Merge conflict in "' + path + '".' print 'CONFLICT (content): Merge conflict in', path
if cleanCache: if cacheOnly:
updateFile(False, sha, mode, path) updateFile(False, sha, mode, path)
else: else:
updateFile(True, aSha, aMode, path) updateFileExt(aSha, aMode, path,
updateFile(False, sha, mode, path, True) updateCache=True, updateWd=False)
else: updateFileExt(sha, mode, path, updateCache=False, updateWd=True)
updateFile(True, sha, mode, path)
os.unlink(orig)
os.unlink(src1)
os.unlink(src2)
else: else:
die("ERROR: Fatal merge failure, shouldn't happen.") die("ERROR: Fatal merge failure, shouldn't happen.")
@ -416,7 +832,7 @@ for nextArg in xrange(1, len(sys.argv)):
try: try:
h1 = firstBranch = sys.argv[nextArg + 1] h1 = firstBranch = sys.argv[nextArg + 1]
h2 = secondBranch = sys.argv[nextArg + 2] h2 = secondBranch = sys.argv[nextArg + 2]
except IndexError: except IndexError:
usage() usage()
break break
@ -428,8 +844,8 @@ try:
graph = buildGraph([h1, h2]) graph = buildGraph([h1, h2])
[res, clean] = merge(graph.shaMap[h1], graph.shaMap[h2], [dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
firstBranch, secondBranch, graph) firstBranch, secondBranch, graph)
print '' print ''
except: except: