copy: add experimetal support for unmarking committed copies
authorMartin von Zweigbergk <martinvonz@google.com>
Tue, 28 Jan 2020 14:07:57 -0800
changeset 44365 7c4b98a4e536
parent 44364 8be0c63535b5
child 44366 d8b49bf6cfec
copy: add experimetal support for unmarking committed copies The simplest way I'm aware of to unmark a file as copied after committing is this: hg uncommit --keep <dest> hg forget <dest> hg add <dest> hg amend This patch teaches `hg copy --forget` a `-r` argument to simplify that into: hg copy --forget --at-rev . <dest> In addition to being simpler, it doesn't touch the working copy, so it can easily be used even if the destination file has been modified in the working copy. I'll teach `hg copy` without `--forget` to work with `--at-rev` next. Differential Revision: https://phab.mercurial-scm.org/D8030
mercurial/cmdutil.py
mercurial/commands.py
mercurial/context.py
relnotes/next
tests/test-completion.t
tests/test-copy.t
tests/test-rename-after-merge.t
--- a/mercurial/cmdutil.py	Fri Dec 20 15:50:13 2019 -0800
+++ b/mercurial/cmdutil.py	Tue Jan 28 14:07:57 2020 -0800
@@ -1427,14 +1427,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'at_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(
                     _(
@@ -1442,8 +1461,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'at_rev'):
+        raise error.Abort(_("--at-rev is only supported with --forget"))
+
     def walkpat(pat):
         srcs = []
         m = scmutil.match(ctx, [pat], opts, globbed=True)
--- a/mercurial/commands.py	Fri Dec 20 15:50:13 2019 -0800
+++ b/mercurial/commands.py	Tue Jan 28 14:07:57 2020 -0800
@@ -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'',
+            b'at-rev',
+            b'',
+            _(b'unmark copies in the given revision (EXPERIMENTAL)'),
+            _(b'REV'),
+        ),
+        (
             b'f',
             b'force',
             None,
--- a/mercurial/context.py	Fri Dec 20 15:50:13 2019 -0800
+++ b/mercurial/context.py	Tue Jan 28 14:07:57 2020 -0800
@@ -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
 
--- a/relnotes/next	Fri Dec 20 15:50:13 2019 -0800
+++ b/relnotes/next	Tue Jan 28 14:07:57 2020 -0800
@@ -17,6 +17,9 @@
 
 == New Experimental Features ==
 
+ * Use `hg copy --forget --at-rev REV` to unmark already committed
+   copies.
+
 
 == Bug Fixes  ==
 
--- a/tests/test-completion.t	Fri Dec 20 15:50:13 2019 -0800
+++ b/tests/test-completion.t	Tue Jan 28 14:07:57 2020 -0800
@@ -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, at-rev, force, include, exclude, dry-run
   debugancestor: 
   debugapplystreamclonebundle: 
   debugbuilddag: mergeable-file, overwritten-file, new-file
--- a/tests/test-copy.t	Fri Dec 20 15:50:13 2019 -0800
+++ b/tests/test-copy.t	Tue Jan 28 14:07:57 2020 -0800
@@ -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 --at-rev . 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 ..
--- a/tests/test-rename-after-merge.t	Fri Dec 20 15:50:13 2019 -0800
+++ b/tests/test-rename-after-merge.t	Tue Jan 28 14:07:57 2020 -0800
@@ -120,4 +120,10 @@
   $ hg log -r tip -C -v | grep copies
   copies:      b2 (b1)
 
+Test unmarking copies in merge commit
+
+  $ hg copy --forget --at-rev . b2
+  abort: cannot unmark copy in merge commit
+  [255]
+
   $ cd ..