diff --git a/mercurial/copies.py b/mercurial/copies.py --- a/mercurial/copies.py +++ b/mercurial/copies.py @@ -373,57 +373,6 @@ def _computenonoverlap(repo, c1, c2, add return u1, u2 -def _makegetfctx(ctx): - """return a 'getfctx' function suitable for _checkcopies usage - - We have to re-setup the function building 'filectx' for each - '_checkcopies' to ensure the linkrev adjustment is properly setup for - each. Linkrev adjustment is important to avoid bug in rename - detection. Moreover, having a proper '_ancestrycontext' setup ensures - the performance impact of this adjustment is kept limited. Without it, - each file could do a full dag traversal making the time complexity of - the operation explode (see issue4537). - - This function exists here mostly to limit the impact on stable. Feel - free to refactor on default. - """ - rev = ctx.rev() - repo = ctx._repo - ac = getattr(ctx, '_ancestrycontext', None) - if ac is None: - revs = [rev] - if rev is None: - revs = [p.rev() for p in ctx.parents()] - ac = repo.changelog.ancestors(revs, inclusive=True) - ctx._ancestrycontext = ac - def makectx(f, n): - if n in node.wdirfilenodeids: # in a working context? - if ctx.rev() is None: - return ctx.filectx(f) - return repo[None][f] - fctx = repo.filectx(f, fileid=n) - # setup only needed for filectx not create from a changectx - fctx._ancestrycontext = ac - fctx._descendantrev = rev - return fctx - return util.lrucachefunc(makectx) - -def _combinecopies(copyfrom, copyto, finalcopy, diverge, incompletediverge): - """combine partial copy paths""" - remainder = {} - for f in copyfrom: - if f in copyto: - finalcopy[copyto[f]] = copyfrom[f] - del copyto[f] - for f in incompletediverge: - assert f not in diverge - ic = incompletediverge[f] - if ic[0] in copyto: - diverge[f] = [copyto[ic[0]], ic[1]] - else: - remainder[f] = ic - return remainder - def mergecopies(repo, c1, c2, base): """ Finds moves and copies between context c1 and c2 that are relevant for @@ -518,6 +467,23 @@ def _isfullcopytraceable(repo, c1, base) return commits < sourcecommitlimit return False +def _checksinglesidecopies(src, dsts1, m1, m2, mb, c2, base, + copy, renamedelete): + if src not in m2: + # deleted on side 2 + if src not in m1: + # renamed on side 1, deleted on side 2 + renamedelete[src] = dsts1 + elif m2[src] != mb[src]: + if not _related(c2[src], base[src]): + return + # modified on side 2 + for dst in dsts1: + if dst not in m2: + # dst not added on side 2 (handle as regular + # "both created" case in manifestmerge otherwise) + copy[dst] = src + def _fullcopytracing(repo, c1, c2, base): """ The full copytracing algorithm which finds all the new files that were added from merge base up to the top commit and for each file it checks if @@ -526,168 +492,73 @@ def _fullcopytracing(repo, c1, c2, base) This is pretty slow when a lot of changesets are involved but will track all the copies. """ - # In certain scenarios (e.g. graft, update or rebase), base can be - # overridden We still need to know a real common ancestor in this case We - # can't just compute _c1.ancestor(_c2) and compare it to ca, because there - # can be multiple common ancestors, e.g. in case of bidmerge. Because our - # caller may not know if the revision passed in lieu of the CA is a genuine - # common ancestor or not without explicitly checking it, it's better to - # determine that here. - # - # base.isancestorof(wc) is False, work around that - _c1 = c1.p1() if c1.rev() is None else c1 - _c2 = c2.p1() if c2.rev() is None else c2 - # an endpoint is "dirty" if it isn't a descendant of the merge base - # if we have a dirty endpoint, we need to trigger graft logic, and also - # keep track of which endpoint is dirty - dirtyc1 = not base.isancestorof(_c1) - dirtyc2 = not base.isancestorof(_c2) - graft = dirtyc1 or dirtyc2 - tca = base - if graft: - tca = _c1.ancestor(_c2) - - limit = _findlimit(repo, c1, c2) - m1 = c1.manifest() m2 = c2.manifest() mb = base.manifest() - # gather data from _checkcopies: - # - diverge = record all diverges in this dict - # - copy = record all non-divergent copies in this dict - # - fullcopy = record all copies in this dict - # - incomplete = record non-divergent partial copies here - # - incompletediverge = record divergent partial copies here - diverge = {} # divergence data is shared - incompletediverge = {} - data1 = {'copy': {}, - 'fullcopy': {}, - 'incomplete': {}, - 'diverge': diverge, - 'incompletediverge': incompletediverge, - } - data2 = {'copy': {}, - 'fullcopy': {}, - 'incomplete': {}, - 'diverge': diverge, - 'incompletediverge': incompletediverge, - } + copies1 = pathcopies(base, c1) + copies2 = pathcopies(base, c2) + + inversecopies1 = {} + inversecopies2 = {} + for dst, src in copies1.items(): + inversecopies1.setdefault(src, []).append(dst) + for dst, src in copies2.items(): + inversecopies2.setdefault(src, []).append(dst) + + copy = {} + diverge = {} + renamedelete = {} + allsources = set(inversecopies1) | set(inversecopies2) + for src in allsources: + dsts1 = inversecopies1.get(src) + dsts2 = inversecopies2.get(src) + if dsts1 and dsts2: + # copied/renamed on both sides + if src not in m1 and src not in m2: + # renamed on both sides + dsts1 = set(dsts1) + dsts2 = set(dsts2) + # If there's some overlap in the rename destinations, we + # consider it not divergent. For example, if side 1 copies 'a' + # to 'b' and 'c' and deletes 'a', and side 2 copies 'a' to 'c' + # and 'd' and deletes 'a'. + if dsts1 & dsts2: + for dst in (dsts1 & dsts2): + copy[dst] = src + else: + diverge[src] = sorted(dsts1 | dsts2) + elif src in m1 and src in m2: + # copied on both sides + dsts1 = set(dsts1) + dsts2 = set(dsts2) + for dst in (dsts1 & dsts2): + copy[dst] = src + # TODO: Handle cases where it was renamed on one side and copied + # on the other side + elif dsts1: + # copied/renamed only on side 1 + _checksinglesidecopies(src, dsts1, m1, m2, mb, c2, base, + copy, renamedelete) + elif dsts2: + # copied/renamed only on side 2 + _checksinglesidecopies(src, dsts2, m2, m1, mb, c1, base, + copy, renamedelete) + + renamedeleteset = set() + divergeset = set() + for src, dsts in diverge.items(): + divergeset.update(dsts) + for src, dsts in renamedelete.items(): + renamedeleteset.update(dsts) # find interesting file sets from manifests addedinm1 = m1.filesnotin(mb, repo.narrowmatch()) addedinm2 = m2.filesnotin(mb, repo.narrowmatch()) - bothnew = sorted(addedinm1 & addedinm2) - if tca == base: - # unmatched file from base - u1r, u2r = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2) - u1u, u2u = u1r, u2r - else: - # unmatched file from base (DAG rotation in the graft case) - u1r, u2r = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2) - # unmatched file from topological common ancestors (no DAG rotation) - # need to recompute this for directory move handling when grafting - mta = tca.manifest() - u1u, u2u = _computenonoverlap(repo, c1, c2, - m1.filesnotin(mta, repo.narrowmatch()), - m2.filesnotin(mta, repo.narrowmatch()), - debug=False) - - for f in u1u: - _checkcopies(c1, c2, f, base, tca, dirtyc1, limit, data1) - - for f in u2u: - _checkcopies(c2, c1, f, base, tca, dirtyc2, limit, data2) - - copy = dict(data1['copy']) - copy.update(data2['copy']) - fullcopy = dict(data1['fullcopy']) - fullcopy.update(data2['fullcopy']) - - if dirtyc1: - _combinecopies(data2['incomplete'], data1['incomplete'], copy, diverge, - incompletediverge) - if dirtyc2: - _combinecopies(data1['incomplete'], data2['incomplete'], copy, diverge, - incompletediverge) - - renamedelete = {} - renamedeleteset = set() - divergeset = set() - for of, fl in list(diverge.items()): - if len(fl) == 1 or of in c1 or of in c2: - del diverge[of] # not actually divergent, or not a rename - if of not in c1 and of not in c2: - # renamed on one side, deleted on the other side, but filter - # out files that have been renamed and then deleted - renamedelete[of] = [f for f in fl if f in c1 or f in c2] - renamedeleteset.update(fl) # reverse map for below - else: - divergeset.update(fl) # reverse map for below + u1, u2 = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2) - bothdiverge = {} - bothincompletediverge = {} - remainder = {} - both1 = {'copy': {}, - 'fullcopy': {}, - 'incomplete': {}, - 'diverge': bothdiverge, - 'incompletediverge': bothincompletediverge - } - both2 = {'copy': {}, - 'fullcopy': {}, - 'incomplete': {}, - 'diverge': bothdiverge, - 'incompletediverge': bothincompletediverge - } - for f in bothnew: - _checkcopies(c1, c2, f, base, tca, dirtyc1, limit, both1) - _checkcopies(c2, c1, f, base, tca, dirtyc2, limit, both2) - if dirtyc1 and dirtyc2: - remainder = _combinecopies(both2['incomplete'], both1['incomplete'], - copy, bothdiverge, bothincompletediverge) - remainder1 = _combinecopies(both1['incomplete'], both2['incomplete'], - copy, bothdiverge, bothincompletediverge) - remainder.update(remainder1) - elif dirtyc1: - # incomplete copies may only be found on the "dirty" side for bothnew - assert not both2['incomplete'] - remainder = _combinecopies({}, both1['incomplete'], copy, bothdiverge, - bothincompletediverge) - elif dirtyc2: - assert not both1['incomplete'] - remainder = _combinecopies({}, both2['incomplete'], copy, bothdiverge, - bothincompletediverge) - else: - # incomplete copies and divergences can't happen outside grafts - assert not both1['incomplete'] - assert not both2['incomplete'] - assert not bothincompletediverge - for f in remainder: - assert f not in bothdiverge - ic = remainder[f] - if ic[0] in (m1 if dirtyc1 else m2): - # backed-out rename on one side, but watch out for deleted files - bothdiverge[f] = ic - for of, fl in bothdiverge.items(): - if len(fl) == 2 and fl[0] == fl[1]: - copy[fl[0]] = of # not actually divergent, just matching renames - - # Sometimes we get invalid copies here (the "and not remotebase" in - # _checkcopies() seems suspicious). Filter them out. - for dst, src in fullcopy.copy().items(): - if src not in mb: - del fullcopy[dst] - # Sometimes we forget to add entries from "copy" to "fullcopy", so fix - # that up here - for dst, src in copy.items(): - fullcopy[dst] = src - # Sometimes we forget to add entries from "diverge" to "fullcopy", so fix - # that up here - for src, dsts in diverge.items(): - for dst in dsts: - fullcopy[dst] = src - + fullcopy = copies1.copy() + fullcopy.update(copies2) if not fullcopy: return copy, {}, diverge, renamedelete, {} @@ -752,7 +623,7 @@ def _fullcopytracing(repo, c1, c2, base) movewithdir = {} # check unaccounted nonoverlapping files against directory moves - for f in u1r + u2r: + for f in u1 + u2: if f not in fullcopy: for d in dirmove: if f.startswith(d): @@ -899,99 +770,6 @@ def _related(f1, f2): except StopIteration: return False -def _checkcopies(srcctx, dstctx, f, base, tca, remotebase, limit, data): - """ - check possible copies of f from msrc to mdst - - srcctx = starting context for f in msrc - dstctx = destination context for f in mdst - f = the filename to check (as in msrc) - base = the changectx used as a merge base - tca = topological common ancestor for graft-like scenarios - remotebase = True if base is outside tca::srcctx, False otherwise - limit = the rev number to not search beyond - data = dictionary of dictionary to store copy data. (see mergecopies) - - note: limit is only an optimization, and provides no guarantee that - irrelevant revisions will not be visited - there is no easy way to make this algorithm stop in a guaranteed way - once it "goes behind a certain revision". - """ - - msrc = srcctx.manifest() - mdst = dstctx.manifest() - mb = base.manifest() - mta = tca.manifest() - # Might be true if this call is about finding backward renames, - # This happens in the case of grafts because the DAG is then rotated. - # If the file exists in both the base and the source, we are not looking - # for a rename on the source side, but on the part of the DAG that is - # traversed backwards. - # - # In the case there is both backward and forward renames (before and after - # the base) this is more complicated as we must detect a divergence. - # We use 'backwards = False' in that case. - backwards = not remotebase and base != tca and f in mb - getsrcfctx = _makegetfctx(srcctx) - getdstfctx = _makegetfctx(dstctx) - - if msrc[f] == mb.get(f) and not remotebase: - # Nothing to merge - return - - of = None - seen = {f} - for oc in getsrcfctx(f, msrc[f]).ancestors(): - of = oc.path() - if of in seen: - # check limit late - grab last rename before - if oc.linkrev() < limit: - break - continue - seen.add(of) - - # remember for dir rename detection - if backwards: - data['fullcopy'][of] = f # grafting backwards through renames - else: - data['fullcopy'][f] = of - if of not in mdst: - continue # no match, keep looking - if mdst[of] == mb.get(of): - return # no merge needed, quit early - c2 = getdstfctx(of, mdst[of]) - # c2 might be a plain new file on added on destination side that is - # unrelated to the droids we are looking for. - cr = _related(oc, c2) - if cr and (of == f or of == c2.path()): # non-divergent - if backwards: - data['copy'][of] = f - elif of in mb: - data['copy'][f] = of - elif remotebase: # special case: a <- b <- a -> b "ping-pong" rename - data['copy'][of] = f - del data['fullcopy'][f] - data['fullcopy'][of] = f - else: # divergence w.r.t. graft CA on one side of topological CA - for sf in seen: - if sf in mb: - assert sf not in data['diverge'] - data['diverge'][sf] = [f, of] - break - return - - if of in mta: - if backwards or remotebase: - data['incomplete'][of] = f - else: - for sf in seen: - if sf in mb: - if tca == base: - data['diverge'].setdefault(sf, []).append(f) - else: - data['incompletediverge'][sf] = [of, f] - return - def duplicatecopies(repo, wctx, rev, fromrev, skiprev=None): """reproduce copies from fromrev to rev in the dirstate diff --git a/tests/test-annotate.t b/tests/test-annotate.t --- a/tests/test-annotate.t +++ b/tests/test-annotate.t @@ -273,37 +273,10 @@ annotate after merge with -l > EOF $ hg ci -mc -d '3 0' created new head -BROKEN: 'a' was copied to 'b' on both sides. We should not get a merge conflict here $ hg merge merging b - warning: conflicts while merging b! (edit, then use 'hg resolve --mark') - 0 files updated, 0 files merged, 0 files removed, 1 files unresolved - use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon - [1] - $ cat b - <<<<<<< working copy: b80e3e32f75a - test: c - a - z - a - ||||||| base - ======= - a - a - a - b4 - c - b5 - >>>>>>> merge rev: 64afcdf8e29e - test: mergeb - $ cat < b - > a - > z - > a - > b4 - > c - > b5 - > EOF - $ hg resolve --mark -q - $ rm b.orig + 0 files updated, 1 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) $ echo d >> b $ hg ci -mmerge2 -d '4 0' diff --git a/tests/test-commit-amend.t b/tests/test-commit-amend.t --- a/tests/test-commit-amend.t +++ b/tests/test-commit-amend.t @@ -787,12 +787,11 @@ Amend a merge changeset (with renames du Update to p1 with 'aaa' modified. 'aaa' was renamed from 'aa' in p2. 'aa' exists in p1 too, but it was recorded as copied from p2. $ echo modified >> aaa -BROKEN: should not be follow the rename back to 'aa' here, since the rename -happened compared to p2 $ hg co -m '.^' -t :merge3 - merging aaa and aa to aa - warning: conflicts while merging aa! (edit, then use 'hg resolve --mark') - 0 files updated, 0 files merged, 1 files removed, 1 files unresolved + file 'aaa' was deleted in other [destination] but was modified in local [working copy]. + What do you want to do? + use (c)hanged version, (d)elete, or leave (u)nresolved? u + 1 files updated, 0 files merged, 1 files removed, 1 files unresolved use 'hg resolve' to retry unresolved file merges [1] $ hg co -C tip diff --git a/tests/test-copies.t b/tests/test-copies.t --- a/tests/test-copies.t +++ b/tests/test-copies.t @@ -549,9 +549,6 @@ test reflect that for this particular ca Grafting revision 4 on top of revision 2, showing that it respect the rename: -TODO: Make this work with copy info in changesets (probably by writing a -changeset-centric version of copies.mergecopies()) -#if no-changeset $ hg up 2 -q $ hg graft -r 4 --base 3 --hidden grafting 4:af28412ec03c "added d, modified b" (tip) @@ -560,15 +557,14 @@ changeset-centric version of copies.merg $ hg l -l1 -p @ 5 added d, modified b | b1 - ~ diff -r 5a4825cc2926 -r 94a2f1a0e8e2 b1 + ~ diff -r 5a4825cc2926 -r 94a2f1a0e8e2 b1 (no-changeset !) + ~ diff -r f5474f5023a8 -r ef7c02d69f3d b1 (changeset !) --- a/b1 Thu Jan 01 00:00:00 1970 +0000 +++ b/b1 Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +1,2 @@ b +baba -#endif - Test to make sure that fullcopytracing algorithm don't fail when both the merging csets are dirty (a dirty cset is one who is not the descendant of merge base) ------------------------------------------------------------------------------------------------- diff --git a/tests/test-fastannotate-hg.t b/tests/test-fastannotate-hg.t --- a/tests/test-fastannotate-hg.t +++ b/tests/test-fastannotate-hg.t @@ -273,37 +273,10 @@ annotate after merge with -l > EOF $ hg ci -mc -d '3 0' created new head -BROKEN: 'a' was copied to 'b' on both sides. We should not get a merge conflict here $ hg merge merging b - warning: conflicts while merging b! (edit, then use 'hg resolve --mark') - 0 files updated, 0 files merged, 0 files removed, 1 files unresolved - use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon - [1] - $ cat b - <<<<<<< working copy: b80e3e32f75a - test: c - a - z - a - ||||||| base - ======= - a - a - a - b4 - c - b5 - >>>>>>> merge rev: 64afcdf8e29e - test: mergeb - $ cat < b - > a - > z - > a - > b4 - > c - > b5 - > EOF - $ hg resolve --mark -q - $ rm b.orig + 0 files updated, 1 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) $ echo d >> b $ hg ci -mmerge2 -d '4 0' diff --git a/tests/test-graft.t b/tests/test-graft.t --- a/tests/test-graft.t +++ b/tests/test-graft.t @@ -75,6 +75,8 @@ Specifying child as --base revision fail $ hg graft -r 2 --base 3 grafting 2:5c095ad7e90f "2" + note: possible conflict - c was deleted and renamed to: + a note: graft of 2:5c095ad7e90f created no changes to commit Can't continue without starting: @@ -220,6 +222,9 @@ Graft out of order, skipping a merge and committing changelog updating the branch cache grafting 5:97f8bfe72746 "5" + all copies found (* = to merge, ! = divergent, % = renamed and deleted): + src: 'c' -> dst: 'b' + checking for directory renames resolving manifests branchmerge: True, force: True, partial: False ancestor: 4c60f11aa304, local: 6b9e5368ca4e+, remote: 97f8bfe72746 @@ -233,6 +238,9 @@ Graft out of order, skipping a merge and $ HGEDITOR=cat hg graft 4 3 --log --debug scanning for duplicate grafts grafting 4:9c233e8e184d "4" + all copies found (* = to merge, ! = divergent, % = renamed and deleted): + src: 'c' -> dst: 'b' + checking for directory renames resolving manifests branchmerge: True, force: True, partial: False ancestor: 4c60f11aa304, local: 1905859650ec+, remote: 9c233e8e184d @@ -1129,7 +1137,6 @@ and A.3 with a local content change to b grafting 2:f58c7e2b28fa "C0" merging f1e and f1b to f1e merging f2a and f2c to f2c - merging f5b and f5a to f5a Test the cases A.1 (f4x) and A.7 (f3x). diff --git a/tests/test-mv-cp-st-diff.t b/tests/test-mv-cp-st-diff.t --- a/tests/test-mv-cp-st-diff.t +++ b/tests/test-mv-cp-st-diff.t @@ -1688,13 +1688,8 @@ Check debug output for copy tracing Check that merging across the rename works $ echo modified >> renamed -BROKEN: This should propagate the change to 'f' $ hg co -m 4 - file 'renamed' was deleted in other [destination] but was modified in local [working copy]. - What do you want to do? - use (c)hanged version, (d)elete, or leave (u)nresolved? u - 1 files updated, 0 files merged, 0 files removed, 1 files unresolved - use 'hg resolve' to retry unresolved file merges - [1] + merging renamed and f to f + 0 files updated, 1 files merged, 0 files removed, 0 files unresolved $ cd .. diff --git a/tests/test-rename-merge2.t b/tests/test-rename-merge2.t --- a/tests/test-rename-merge2.t +++ b/tests/test-rename-merge2.t @@ -433,6 +433,9 @@ m "um a c" "um x c" " " "10 do merg -------------- test L:nc a b R:up b W: - 12 merge b no ancestor -------------- + all copies found (* = to merge, ! = divergent, % = renamed and deleted): + src: 'a' -> dst: 'b' + checking for directory renames resolving manifests branchmerge: True, force: False, partial: False ancestor: 924404dff337, local: 86a2aa42fc76+, remote: af30c7647fc7 @@ -469,6 +472,9 @@ m "um a c" "um x c" " " "10 do merg -------------- test L:up b R:nm a b W: - 13 merge b no ancestor -------------- + all copies found (* = to merge, ! = divergent, % = renamed and deleted): + src: 'a' -> dst: 'b' + checking for directory renames resolving manifests branchmerge: True, force: False, partial: False ancestor: 924404dff337, local: 59318016310c+, remote: bdb19105162a @@ -506,6 +512,9 @@ m "um a c" "um x c" " " "10 do merg -------------- test L:nc a b R:up a b W: - 14 merge b no ancestor -------------- + all copies found (* = to merge, ! = divergent, % = renamed and deleted): + src: 'a' -> dst: 'b' + checking for directory renames resolving manifests branchmerge: True, force: False, partial: False ancestor: 924404dff337, local: 86a2aa42fc76+, remote: 8dbce441892a @@ -543,6 +552,9 @@ m "um a c" "um x c" " " "10 do merg -------------- test L:up b R:nm a b W: - 15 merge b no ancestor, remove a -------------- + all copies found (* = to merge, ! = divergent, % = renamed and deleted): + src: 'a' -> dst: 'b' + checking for directory renames resolving manifests branchmerge: True, force: False, partial: False ancestor: 924404dff337, local: 59318016310c+, remote: bdb19105162a @@ -580,6 +592,9 @@ m "um a c" "um x c" " " "10 do merg -------------- test L:nc a b R:up a b W: - 16 get a, merge b no ancestor -------------- + all copies found (* = to merge, ! = divergent, % = renamed and deleted): + src: 'a' -> dst: 'b' + checking for directory renames resolving manifests branchmerge: True, force: False, partial: False ancestor: 924404dff337, local: 86a2aa42fc76+, remote: 8dbce441892a @@ -617,6 +632,9 @@ m "um a c" "um x c" " " "10 do merg -------------- test L:up a b R:nc a b W: - 17 keep a, merge b no ancestor -------------- + all copies found (* = to merge, ! = divergent, % = renamed and deleted): + src: 'a' -> dst: 'b' + checking for directory renames resolving manifests branchmerge: True, force: False, partial: False ancestor: 924404dff337, local: 0b76e65c8289+, remote: 4ce40f5aca24 @@ -653,6 +671,9 @@ m "um a c" "um x c" " " "10 do merg -------------- test L:nm a b R:up a b W: - 18 merge b no ancestor -------------- + all copies found (* = to merge, ! = divergent, % = renamed and deleted): + src: 'a' -> dst: 'b' + checking for directory renames resolving manifests branchmerge: True, force: False, partial: False ancestor: 924404dff337, local: 02963e448370+, remote: 8dbce441892a @@ -695,6 +716,9 @@ m "um a c" "um x c" " " "10 do merg -------------- test L:up a b R:nm a b W: - 19 merge b no ancestor, prompt remove a -------------- + all copies found (* = to merge, ! = divergent, % = renamed and deleted): + src: 'a' -> dst: 'b' + checking for directory renames resolving manifests branchmerge: True, force: False, partial: False ancestor: 924404dff337, local: 0b76e65c8289+, remote: bdb19105162a