@ -7,9 +7,6 @@ from sets import Set
@@ -7,9 +7,6 @@ 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') + \
@ -21,11 +18,23 @@ def setupIndex(temporary):
@@ -21,11 +18,23 @@ def setupIndex(temporary):
pass
if temporary:
newIndex = temporaryIndexFile
os.environ
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.'''
@ -35,6 +44,7 @@ def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
@@ -35,6 +44,7 @@ def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
def infoMsg(*args):
sys.stdout.write(' '*callDepth)
printList(args)
infoMsg('Merging:')
infoMsg(h1)
infoMsg(h2)
@ -46,27 +56,27 @@ def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
@@ -46,27 +56,27 @@ def merge(h1, h2, branch1Name, branch2Name, graph, callDepth=0):
infoMsg(x)
sys.stdout.flush()
Ms = ca[0]
mergedCA = 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))
[mergedCA, dummy] = merge(mergedCA, h,
'Temporary shared merge branch 1',
'Temporary shared merge branch 2',
graph, callDepth+1)
assert(isinstance(mergedCA, Commit))
global cacheOnly
if callDepth == 0:
setupIndex(False)
cleanCache = False
cacheOnly = False
else:
setupIndex(True)
runProgram(['git-read-tree', h1.tree()])
cleanCache = True
cacheOnly = True
[shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), Ms.tree(),
branch1Name, branch2Name,
cleanCache)
[shaRes, clean] = mergeTrees(h1.tree(), h2.tree(), mergedCA.tree(),
branch1Name, branch2Name)
if clean or cleanCache:
if clean or cacheOnly:
res = Commit(None, [h1, h2], tree=shaRes)
graph.addNode(res)
else:
@ -89,16 +99,255 @@ def getFilesAndDirs(tree):
@@ -89,16 +99,255 @@ def getFilesAndDirs(tree):
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:
print '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:
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 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()]
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
@ -108,155 +357,340 @@ def unmergedCacheEntries():
@@ -108,155 +357,340 @@ def unmergedCacheEntries():
lines = runProgram(['git-ls-files', '-z', '--unmerged']).split('\0')
lines.pop()
res = {}
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)) - 1
stage = int(m.group(3))
path = m.group(4)
if res.has_key(path):
e = res[path]
else:
e = res.get(path)
if not e:
e = CacheEntry(path)
res[path] = e
res.add(e)
e.stages[stage].mode = mode
e.stages[stage].sha1 = sha1
else:
die('Error: Merge program failed: Unexpected output from', \
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.'''
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.'''
assert(isSha(head) and isSha(merge) and isSha(common))
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]
if common == merge:
print 'Already uptodate!'
return [head, True]
res = CacheEntry(path)
if cleanCache:
updateArg = '-i'
else:
updateArg = '-u'
[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]))
[out, code] = runProgram(['git-read-tree', updateArg, '-m', common, head, merge], returnCode = True)
if code != 0:
die('git-read-tree:', out)
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
cleanMerge = True
return res
[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()
# 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:
tree = None
endIndex = x
break
if len(path) > 0:
return '/'.join(path) + \
'/{' + '/'.join(srcPath[endIndex:]) + ' => ' + \
'/'.join(dstPath[endIndex:]) + '}'
else:
cleanMerge = True
return src + ' => ' + dst
return [tree, cleanMerge]
def processRenames(renamesA, renamesB, branchNameA, branchNameB):
srcNames = Set()
for x in renamesA:
srcNames.add(x.srcName)
for x in renamesB:
srcNames.add(x.srcName)
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'.'''
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
# 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
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
# clean == True => non-conflict case
# False => conflict case
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
# If cleanCache == False then the cache shouldn't be updated if clean == False
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
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 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 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 merge:
print 'Auto-merging', ren1.dstName
if updateWd and updateCache:
runProgram(['git-update-index', '--add', '--', path])
elif updateCache:
runProgram(['git-update-index', '--add', '--cacheinfo',
'0%o' % mode, sha, path])
if not clean:
print 'CONFLICT (rename/modify): Merge conflict in', ren1.dstName
cleanMerge = False
def removeFile(clean, path):
if cleanCache or (not cleanCache and clean):
runProgram(['git-update-index', '--force-remove', '--', path])
if not cacheOnly:
updateFileExt(ren1.dstSha, ren1.dstMode, ren1.dstName,
updateCache=True, updateWd=False)
updateFile(clean, resSha, resMode, ren1.dstName)
if not cleanCache and clean:
try:
os.unlink(path)
except OSError, e:
if e.errno != errno.ENOENT and e.errno != errno.EISDIR:
raise
return cleanMerge
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
# Per entry merge function
# ------------------------
debug('processing', entry.path, 'clean cache:', cleanCache)
def processEntry(entry, branch1Name, branch2Name):
'''Merge one cache entry.'''
debug('processing', entry.path, 'clean cache:', cacheOnly)
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
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))
@ -275,28 +709,26 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
@@ -275,28 +709,26 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
(not aSha and bSha == oSha):
# Deleted in both or deleted in one and unchanged in the other
if aSha:
print 'Removing ' + path
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'
print '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 (mod/del): "' + path + '" deleted in', \
branch2Name, 'and modified in', branch1Name + \
'. Version', branch1Name, 'of "' + path + \
'" left in tree'
print '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):
#
@ -307,27 +739,26 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
@@ -307,27 +739,26 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
otherBranch = branch2Name
mode = aMode
sha = aSha
conf = 'file/dir'
conf = 'file/directory'
else:
addBranch = branch2Name
otherBranch = branch1Name
mode = bMode
sha = bSha
conf = 'dir/file'
conf = 'directory/file'
if path in dirs:
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 + '"'
print 'CONFLICT (' + conf + '):', \
'There is a directory with name', path, 'in', \
otherBranch + '. Adding', path, 'as', newPath
removeFile(False, path)
path = newPath
updateFile(False, sha, mode, newPath)
else:
print 'Adding "' + path + '"'
updateFile(True, sha, mode, path)
print 'Adding', path
updateFile(True, sha, mode, path)
elif not oSha and aSha and bSha:
#
@ -336,10 +767,9 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
@@ -336,10 +767,9 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
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: 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)
@ -350,8 +780,9 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
@@ -350,8 +780,9 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
cleanMerge = False
newPath1 = uniquePath(path, branch1Name)
newPath2 = uniquePath(path, branch2Name)
print 'CONFLICT (add/add): File "' + path + \
'" added non-identically in both branches.'
print '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)
@ -360,39 +791,24 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
@@ -360,39 +791,24 @@ def processEntry(entry, branch1Name, branch2Name, files, dirs, cleanCache):
#
# 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
print '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:
mode = aMode
sha = runProgram(['git-hash-object', '-t', 'blob', '-w',
src1]).rstrip()
if ret != 0:
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)
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)
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.")
@ -416,7 +832,7 @@ for nextArg in xrange(1, len(sys.argv)):
@@ -416,7 +832,7 @@ for nextArg in xrange(1, len(sys.argv)):
try:
h1 = firstBranch = sys.argv[nextArg + 1]
h2 = secondBranch = sys.argv[nextArg + 2]
except IndexError:
except IndexError:
usage()
break
@ -428,8 +844,8 @@ try:
@@ -428,8 +844,8 @@ try:
graph = buildGraph([h1, h2])
[res, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
firstBranch, secondBranch, graph)
[dummy, clean] = merge(graph.shaMap[h1], graph.shaMap[h2],
firstBranch, secondBranch, graph)
print ''
except: