D8030: uncopy: add support for unmarking committed copies

martinvonz (Martin von Zweigbergk) phabricator at mercurial-scm.org
Tue Jan 28 23:54:59 UTC 2020


martinvonz created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  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 uncopy` a `-r` argument to simplify that into:
  
    hg uncopy -r . <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.

REPOSITORY
  rHG Mercurial

BRANCH
  default

REVISION DETAIL
  https://phab.mercurial-scm.org/D8030

AFFECTED FILES
  mercurial/cmdutil.py
  mercurial/commands.py
  mercurial/context.py
  relnotes/5.3
  relnotes/next
  tests/test-completion.t
  tests/test-copy.t
  tests/test-rename-after-merge.t

CHANGE DETAILS

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 uncopy -r . b2
+  abort: cannot unmark copy in merge commit
+  [255]
+
   $ cd ..
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 uncopy -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-completion.t b/tests/test-completion.t
--- a/tests/test-completion.t
+++ b/tests/test-completion.t
@@ -357,7 +357,7 @@
   tags: template
   tip: patch, git, style, template
   unbundle: update
-  uncopy: include, exclude
+  uncopy: rev, include, exclude
   unshelve: abort, continue, interactive, keep, name, tool, date
   update: clean, check, merge, date, rev, tool
   verify: full
diff --git a/relnotes/next b/relnotes/next
--- a/relnotes/next
+++ b/relnotes/next
@@ -1,6 +1,7 @@
 == New Features ==
 
- * `hg uncopy` can be used to unmark a file as copied.
+ * `hg uncopy` can be used to unmark a file as copied. Use `hg uncopy -r REV`
+   to unmark already committed copies.
 
 
 == New Experimental Features ==
diff --git a/relnotes/5.3 b/relnotes/5.3
--- a/relnotes/5.3
+++ b/relnotes/5.3
@@ -2,7 +2,8 @@
 
  * Windows will process hgrc files in %PROGRAMDATA%\Mercurial\hgrc.d.
 
- * `hg uncopy` can be used to unmark a file as copied.
+ * `hg uncopy` can be used to unmark a file as copied. Use `hg uncopy -r REV`
+   to unmark already committed copies.
 
 == New Experimental Features ==
 
diff --git a/mercurial/context.py b/mercurial/context.py
--- a/mercurial/context.py
+++ b/mercurial/context.py
@@ -2488,6 +2488,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/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -7493,7 +7493,8 @@
 
 @command(
     b'uncopy',
-    walkopts,
+    [(b'r', b'rev', b'', _(b'unmark copies in the given revision'), _(b'REV'))]
+    + walkopts,
     _(b'[OPTION]... DEST...'),
     helpcategory=command.CATEGORY_FILE_CONTENTS,
 )
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -1695,7 +1695,23 @@
 
 
 def uncopy(ui, repo, pats, opts):
-    ctx = repo[None]
+    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)
 
@@ -1705,13 +1721,24 @@
     uipathfn = scmutil.getuipathfn(repo)
     for f in ctx.walk(match):
         if f in current_copies:
-            ctx[f].markcopied(None)
+            new_ctx[f].markcopied(None)
         elif match.exact(f):
             ui.warn(
                 _(b'%s: not uncopying - file is not marked as copied\n')
                 % 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)
+
 
 ## facility to let extension process additional data into an import patch
 # list of identifier to be executed in order



To: martinvonz, #hg-reviewers
Cc: mercurial-devel


More information about the Mercurial-devel mailing list