diff --git a/mercurial/merge.py b/mercurial/merge.py --- a/mercurial/merge.py +++ b/mercurial/merge.py @@ -110,54 +110,6 @@ def _checkunknown(repo, wctx, mctx): raise util.Abort(_("untracked files in working directory differ " "from files in requested revision")) -def _remains(f, m, ma, workingctx=False): - """check whether specified file remains after merge. - - It is assumed that specified file is not contained in the manifest - of the other context. - """ - if f in ma: - n = m[f] - if n != ma[f]: - return True # because it is changed locally - # even though it doesn't remain, if "remote deleted" is - # chosen in manifestmerge() - elif workingctx and n[20:] == "a": - return True # because it is added locally (linear merge specific) - else: - return False # because it is removed remotely - else: - return True # because it is added locally - -def _checkcollision(mctx, extractxs): - "check for case folding collisions in the destination context" - folded = {} - for fn in mctx: - fold = util.normcase(fn) - if fold in folded: - raise util.Abort(_("case-folding collision between %s and %s") - % (fn, folded[fold])) - folded[fold] = fn - - if extractxs: - wctx, actx = extractxs - # class to delay looking up copy mapping - class pathcopies(object): - @util.propertycache - def map(self): - # {dst@mctx: src@wctx} copy mapping - return copies.pathcopies(wctx, mctx) - pc = pathcopies() - - for fn in wctx: - fold = util.normcase(fn) - mfn = folded.get(fold, None) - if (mfn and mfn != fn and pc.map.get(mfn) != fn and - _remains(fn, wctx.manifest(), actx.manifest(), True) and - _remains(mfn, mctx.manifest(), actx.manifest())): - raise util.Abort(_("case-folding collision between %s and %s") - % (mfn, fn)) - def _forgetremoved(wctx, mctx, branchmerge): """ Forget removed files @@ -186,6 +138,62 @@ def _forgetremoved(wctx, mctx, branchmer return actions +def _checkcollision(repo, wmf, actions, prompts): + # build provisional merged manifest up + pmmf = set(wmf) + + def addop(f, args): + pmmf.add(f) + def removeop(f, args): + pmmf.discard(f) + def nop(f, args): + pass + + def renameop(f, args): + f2, fd, flags = args + if f: + pmmf.discard(f) + pmmf.add(fd) + def mergeop(f, args): + f2, fd, move = args + if move: + pmmf.discard(f) + pmmf.add(fd) + + opmap = { + "a": addop, + "d": renameop, + "dr": nop, + "e": nop, + "f": addop, # untracked file should be kept in working directory + "g": addop, + "m": mergeop, + "r": removeop, + "rd": nop, + } + for f, m, args, msg in actions: + op = opmap.get(m) + assert op, m + op(f, args) + + opmap = { + "cd": addop, + "dc": addop, + } + for f, m in prompts: + op = opmap.get(m) + assert op, m + op(f, None) + + # check case-folding collision in provisional merged manifest + foldmap = {} + for f in sorted(pmmf): + fold = util.normcase(f) + if fold in foldmap: + raise util.Abort(_("case-folding collision between %s and %s") + % (f, foldmap[fold])) + foldmap[fold] = f + def manifestmerge(repo, wctx, p2, pa, branchmerge, force, partial, acceptremote=False): """ @@ -342,6 +350,14 @@ def manifestmerge(repo, wctx, p2, pa, br raise util.Abort(_("untracked files in working directory differ " "from files in requested revision")) + if not util.checkcase(repo.path): + # check collision between files only in p2 for clean update + if (not branchmerge and + (force or not wctx.dirty(missing=True, branch=False))): + _checkcollision(repo, m2, [], []) + else: + _checkcollision(repo, m1, actions, prompts) + for f, m in sorted(prompts): if m == "cd": if acceptremote: @@ -541,14 +557,6 @@ def calculateupdates(repo, tctx, mctx, a acceptremote=False): "Calculate the actions needed to merge mctx into tctx" actions = [] - folding = not util.checkcase(repo.path) - if folding: - # collision check is not needed for clean update - if (not branchmerge and - (force or not tctx.dirty(missing=True, branch=False))): - _checkcollision(mctx, None) - else: - _checkcollision(mctx, (tctx, ancestor)) actions += manifestmerge(repo, tctx, mctx, ancestor, branchmerge, force, diff --git a/tests/test-casecollision-merge.t b/tests/test-casecollision-merge.t --- a/tests/test-casecollision-merge.t +++ b/tests/test-casecollision-merge.t @@ -17,14 +17,17 @@ this is also case for issue3370. $ echo a > a $ hg add a $ hg commit -m '#0' + $ hg tag -l A $ hg rename a tmp $ hg rename tmp A $ hg commit -m '#1' + $ hg tag -l B $ hg update -q 0 $ touch x $ hg add x $ hg commit -m '#2' created new head + $ hg tag -l C $ hg merge -q $ hg status -A @@ -37,6 +40,46 @@ this is also case for issue3370. $ hg status -A M x C A + $ hg commit -m '(D)' + $ hg tag -l D + +additional test for issue3452: + +| this assumes the history below. +| +| (A) -- (C) -- (E) ------- +| \ \ \ +| \ \ \ +| (B) -- (D) -- (F) -- (G) +| +| A: add file 'a' +| B: rename from 'a' to 'A' +| C: add 'x' (or operation other than modification of 'a') +| D: merge C into B +| E: modify 'a' +| F: modify 'A' +| G: merge E into F +| +| issue3452 occurs when (B) is recorded before (C) + + $ hg update -q --clean C + $ echo "modify 'a' at (E)" > a + $ hg commit -m '(E)' + created new head + $ hg tag -l E + + $ hg update -q --clean D + $ echo "modify 'A' at (F)" > A + $ hg commit -m '(F)' + $ hg tag -l F + + $ hg merge -q --tool internal:other E + $ hg status -A + M A + a + C x + $ cat A + modify 'a' at (E) $ cd .. @@ -63,7 +106,7 @@ this is also case for issue3370. $ hg commit -m '#4' $ hg merge - abort: case-folding collision between A and a + abort: case-folding collision between a and A [255] $ hg parents --template '{rev}\n' 4