changeset 44367:9dab3fa64325

copy: add experimental support for marking committed copies The simplest way I'm aware of to mark a file as copied/moved after committing is this: hg uncommit --keep <src> <dest> # <src> needed for move, but not copy hg mv --after <src> <dest> hg amend This patch teaches `hg copy` a `--at-rev` argument to simplify that into: hg copy --after --at-rev . <src> <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. Differential Revision: https://phab.mercurial-scm.org/D8035
author Martin von Zweigbergk <martinvonz@google.com>
date Fri, 20 Dec 2019 13:24:46 -0800
parents d8b49bf6cfec
children 6392bd7c26a8
files mercurial/cmdutil.py mercurial/commands.py relnotes/next tests/test-rename-after-merge.t tests/test-rename-rev.t
diffstat 5 files changed, 130 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/cmdutil.py	Thu Dec 26 14:02:50 2019 -0800
+++ b/mercurial/cmdutil.py	Fri Dec 20 13:24:46 2019 -0800
@@ -1421,17 +1421,23 @@
     forget = opts.get(b"forget")
     after = opts.get(b"after")
     dryrun = opts.get(b"dry_run")
-    ctx = repo[None]
+    rev = opts.get(b'at_rev')
+    if rev:
+        if not forget and not after:
+            # TODO: Remove this restriction and make it also create the copy
+            #       targets (and remove the rename source if rename==True).
+            raise error.Abort(_(b'--at-rev requires --after'))
+        ctx = scmutil.revsingle(repo, rev)
+        if len(ctx.parents()) > 1:
+            raise error.Abort(_(b'cannot mark/unmark copy in merge commit'))
+    else:
+        ctx = repo[None]
+
     pctx = ctx.p1()
 
     uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
 
     if forget:
-        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:
@@ -1484,9 +1490,6 @@
         raise error.Abort(_(b'no destination specified'))
     dest = pats.pop()
 
-    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)
@@ -1517,6 +1520,55 @@
             srcs.append((abs, rel, exact))
         return srcs
 
+    if ctx.rev() is not None:
+        rewriteutil.precheck(repo, [ctx.rev()], b'uncopy')
+        absdest = pathutil.canonpath(repo.root, cwd, dest)
+        if ctx.hasdir(absdest):
+            raise error.Abort(
+                _(b'%s: --at-rev does not support a directory as destination')
+                % uipathfn(absdest)
+            )
+        if absdest not in ctx:
+            raise error.Abort(
+                _(b'%s: copy destination does not exist in %s')
+                % (uipathfn(absdest), ctx)
+            )
+
+        # avoid cycle context -> subrepo -> cmdutil
+        from . import context
+
+        copylist = []
+        for pat in pats:
+            srcs = walkpat(pat)
+            if not srcs:
+                continue
+            for abs, rel, exact in srcs:
+                copylist.append(abs)
+
+        # TODO: Add support for `hg cp --at-rev . foo bar dir` and
+        # `hg cp --at-rev . dir1 dir2`, preferably unifying the code with the
+        # existing functions below.
+        if len(copylist) != 1:
+            raise error.Abort(_(b'--at-rev requires a single source'))
+
+        new_ctx = context.overlayworkingctx(repo)
+        new_ctx.setbase(ctx.p1())
+        mergemod.graft(repo, ctx, wctx=new_ctx)
+
+        new_ctx.markcopied(absdest, copylist[0])
+
+        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'copy', fixphase=True)
+
+        return
+
     # abssrc: hgsep
     # relsrc: ossep
     # otarget: ossep
--- a/mercurial/commands.py	Thu Dec 26 14:02:50 2019 -0800
+++ b/mercurial/commands.py	Fri Dec 20 13:24:46 2019 -0800
@@ -2315,7 +2315,7 @@
             b'',
             b'at-rev',
             b'',
-            _(b'unmark copies in the given revision (EXPERIMENTAL)'),
+            _(b'(un)mark copies in the given revision (EXPERIMENTAL)'),
             _(b'REV'),
         ),
         (
@@ -2345,7 +2345,7 @@
     all given (positional) arguments are unmarked as copies. The destination
     file(s) will be left in place (still tracked).
 
-    This command takes effect with the next commit.
+    This command takes effect with the next commit by default.
 
     Returns 0 on success, 1 if errors are encountered.
     """
--- a/relnotes/next	Thu Dec 26 14:02:50 2019 -0800
+++ b/relnotes/next	Fri Dec 20 13:24:46 2019 -0800
@@ -14,8 +14,12 @@
 
  * `hg copy --forget` can be used to unmark a file as copied.
 
+== New Experimental Features ==
 
-== New Experimental Features ==
+ * `hg copy` now supports a `--at-rev` argument to mark files as
+   copied in the specified commit. It only works with `--after` for
+   now (i.e., it's only useful for marking files copied using non-hg
+   `cp` as copied).
 
  * Use `hg copy --forget --at-rev REV` to unmark already committed
    copies.
--- a/tests/test-rename-after-merge.t	Thu Dec 26 14:02:50 2019 -0800
+++ b/tests/test-rename-after-merge.t	Fri Dec 20 13:24:46 2019 -0800
@@ -120,10 +120,14 @@
   $ hg log -r tip -C -v | grep copies
   copies:      b2 (b1)
 
-Test unmarking copies in merge commit
+Test marking/unmarking copies in merge commit
 
   $ hg copy --forget --at-rev . b2
-  abort: cannot unmark copy in merge commit
+  abort: cannot mark/unmark copy in merge commit
+  [255]
+
+  $ hg copy --after --at-rev . b1 b2
+  abort: cannot mark/unmark copy in merge commit
   [255]
 
   $ cd ..
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-rename-rev.t	Fri Dec 20 13:24:46 2019 -0800
@@ -0,0 +1,56 @@
+  $ hg init
+  $ mkdir d1 d1/d11 d2
+  $ echo d1/a > d1/a
+  $ echo d1/ba > d1/ba
+  $ echo d1/a1 > d1/d11/a1
+  $ echo d1/b > d1/b
+  $ echo d2/b > d2/b
+  $ hg add d1/a d1/b d1/ba d1/d11/a1 d2/b
+  $ hg commit -m "intial"
+
+
+Test single file
+
+# One recoded copy, one copy to record after commit
+  $ hg cp d1/b d1/c
+  $ cp d1/b d1/d
+  $ hg add d1/d
+  $ hg ci -m 'copy d1/b to d1/c and d1/d'
+  $ hg st -C --change .
+  A d1/c
+    d1/b
+  A d1/d
+# Errors out without --after for now
+  $ hg cp --at-rev . d1/b d1/d
+  abort: --at-rev requires --after
+  [255]
+# Errors out with non-existent destination
+  $ hg cp -A --at-rev . d1/b d1/non-existent
+  abort: d1/non-existent: copy destination does not exist in 8a9d70fa20c9
+  [255]
+# Successful invocation
+  $ hg cp -A --at-rev . d1/b d1/d
+  saved backup bundle to $TESTTMP/.hg/strip-backup/8a9d70fa20c9-973ae357-copy.hg
+# New copy is recorded, and previously recorded copy is also still there
+  $ hg st -C --change .
+  A d1/c
+    d1/b
+  A d1/d
+    d1/b
+
+Test using directory as destination
+
+  $ hg co 0
+  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+  $ cp -R d1 d3
+  $ hg add d3
+  adding d3/a
+  adding d3/b
+  adding d3/ba
+  adding d3/d11/a1
+  $ hg ci -m 'copy d1/ to d3/'
+  created new head
+  $ hg cp -A --at-rev . d1 d3
+  abort: d3: --at-rev does not support a directory as destination
+  [255]
+