diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py --- a/mercurial/cmdutil.py +++ b/mercurial/cmdutil.py @@ -1426,14 +1426,33 @@ uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True) if forget: - match = scmutil.match(wctx, pats, opts) - - current_copies = wctx.p1copies() - current_copies.update(wctx.p2copies()) - - for f in wctx.walk(match): + rev = opts[b'rev'] + if rev: + ctx = scmutil.revsingle(repo, rev) + else: + ctx = repo[None] + if ctx.rev() is None: + new_ctx = ctx + else: + if len(ctx.parents()) > 1: + raise error.Abort(_(b'cannot unmark copy in merge commit')) + # avoid cycle context -> subrepo -> cmdutil + from . import context + + rewriteutil.precheck(repo, [ctx.rev()], b'uncopy') + new_ctx = context.overlayworkingctx(repo) + new_ctx.setbase(ctx.p1()) + mergemod.graft(repo, ctx, wctx=new_ctx) + + match = scmutil.match(ctx, pats, opts) + + current_copies = ctx.p1copies() + current_copies.update(ctx.p2copies()) + + uipathfn = scmutil.getuipathfn(repo) + for f in ctx.walk(match): if f in current_copies: - wctx[f].markcopied(None) + new_ctx[f].markcopied(None) elif match.exact(f): ui.warn( _( @@ -1441,8 +1460,25 @@ ) % uipathfn(f) ) + + if ctx.rev() is not None: + with repo.lock(): + mem_ctx = new_ctx.tomemctx_for_amend(ctx) + new_node = mem_ctx.commit() + + if repo.dirstate.p1() == ctx.node(): + with repo.dirstate.parentchange(): + scmutil.movedirstate(repo, repo[new_node]) + replacements = {ctx.node(): [new_node]} + scmutil.cleanupnodes( + repo, replacements, b'uncopy', fixphase=True + ) + return + if opts.get(b'rev'): + raise error.Abort(_("--rev is only supported with --forget")) + def walkpat(pat): srcs = [] if after: diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -2312,6 +2312,13 @@ (b'', b'forget', None, _(b'unmark a file as copied')), (b'A', b'after', None, _(b'record a copy that has already occurred')), ( + b'r', + b'rev', + b'', + _(b'unmark copies in the given revision'), + _(b'REV'), + ), + ( b'f', b'force', None, diff --git a/mercurial/context.py b/mercurial/context.py --- a/mercurial/context.py +++ b/mercurial/context.py @@ -2487,6 +2487,17 @@ editor=editor, ) + def tomemctx_for_amend(self, precursor): + extra = precursor.extra().copy() + extra[b'amend_source'] = precursor.hex() + return self.tomemctx( + text=precursor.description(), + branch=precursor.branch(), + extra=extra, + date=precursor.date(), + user=precursor.user(), + ) + def isdirty(self, path): return path in self._cache diff --git a/relnotes/next b/relnotes/next --- a/relnotes/next +++ b/relnotes/next @@ -12,7 +12,8 @@ commits that are being merged, when there are conflicts. Also works for conflicts caused by e.g. `hg graft`. - * `hg copy --forget` can be used to unmark a file as copied. + * `hg copy --forget` can be used to unmark a file as copied. Use `hg + copy --forget -r REV` to unmark already committed copies. == New Experimental Features == diff --git a/tests/test-completion.t b/tests/test-completion.t --- a/tests/test-completion.t +++ b/tests/test-completion.t @@ -257,7 +257,7 @@ commit: addremove, close-branch, amend, secret, edit, force-close-branch, interactive, include, exclude, message, logfile, date, user, subrepos config: untrusted, edit, local, global, template continue: dry-run - copy: forget, after, force, include, exclude, dry-run + copy: forget, after, rev, force, include, exclude, dry-run debugancestor: debugapplystreamclonebundle: debugbuilddag: mergeable-file, overwritten-file, new-file diff --git a/tests/test-copy.t b/tests/test-copy.t --- a/tests/test-copy.t +++ b/tests/test-copy.t @@ -319,5 +319,56 @@ A dir2/bar A dir2/foo ? dir2/untracked +# Clean up for next test + $ hg forget dir2 + removing dir2/bar + removing dir2/foo + $ rm -r dir2 + +Test uncopy on committed copies + +# Commit some copies + $ hg cp bar baz + $ hg cp bar qux + $ hg ci -m copies + $ hg st -C --change . + A baz + bar + A qux + bar + $ base=$(hg log -r '.^' -T '{rev}') + $ hg log -G -T '{rev}:{node|short} {desc}\n' -r $base: + @ 5:a612dc2edfda copies + | + o 4:4800b1f1f38e add dir/ + | + ~ +# Add a dirty change on top to show that it's unaffected + $ echo dirty >> baz + $ hg st + M baz + $ cat baz + bleah + dirty + $ hg copy --forget -r . baz + saved backup bundle to $TESTTMP/part2/.hg/strip-backup/a612dc2edfda-e36b4448-uncopy.hg +# The unwanted copy is no longer recorded, but the unrelated one is + $ hg st -C --change . + A baz + A qux + bar +# The old commit is gone and we have updated to the new commit + $ hg log -G -T '{rev}:{node|short} {desc}\n' -r $base: + @ 5:c45090e5effe copies + | + o 4:4800b1f1f38e add dir/ + | + ~ +# Working copy still has the uncommitted change + $ hg st + M baz + $ cat baz + bleah + dirty $ cd .. diff --git a/tests/test-rename-after-merge.t b/tests/test-rename-after-merge.t --- a/tests/test-rename-after-merge.t +++ b/tests/test-rename-after-merge.t @@ -120,4 +120,10 @@ $ hg log -r tip -C -v | grep copies copies: b2 (b1) +Test unmarking copies in merge commit + + $ hg copy --forget -r . b2 + abort: cannot unmark copy in merge commit + [255] + $ cd ..