Patchwork D8035: copy: add support for marking committed copies

login
register
mail settings
Submitter phabricator
Date Jan. 29, 2020, 11:41 p.m.
Message ID <c3c54cf138c553c0dc2eae457f9154a9@localhost.localdomain>
Download mbox | patch
Permalink /patch/44757/
State Not Applicable
Headers show

Comments

phabricator - Jan. 29, 2020, 11:41 p.m.
martinvonz updated this revision to Diff 19697.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D8035?vs=19671&id=19697

BRANCH
  default

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D8035/new/

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

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

CHANGE DETAILS




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

Patch

diff --git a/tests/test-rename.t b/tests/test-rename-rev.t
copy from tests/test-rename.t
copy to tests/test-rename-rev.t
--- a/tests/test-rename.t
+++ b/tests/test-rename-rev.t
@@ -6,693 +6,51 @@ 
   $ 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 "1"
-
-rename a single file
-
-  $ hg rename d1/d11/a1 d2/c
-  $ hg --config ui.portablefilenames=abort rename d1/a d1/con.xml
-  abort: filename contains 'con', which is reserved on Windows: d1/con.xml
-  [255]
-  $ hg sum
-  parent: 0:9b4b6e7b2c26 tip
-   1
-  branch: default
-  commit: 1 renamed
-  update: (current)
-  phases: 1 draft
-  $ hg status -C
-  A d2/c
-    d1/d11/a1
-  R d1/d11/a1
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d2/c
-
-rename a single file using absolute paths
-
-  $ hg rename `pwd`/d1/d11/a1 `pwd`/d2/c
-  $ hg status -C
-  A d2/c
-    d1/d11/a1
-  R d1/d11/a1
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d2/c
+  $ hg commit -m "intial"
 
-rename --after a single file
 
-  $ mv d1/d11/a1 d2/c
-  $ hg rename --after d1/d11/a1 d2/c
-  $ hg status -C
-  A d2/c
-    d1/d11/a1
-  R d1/d11/a1
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d2/c
-
-rename --after a single file when src and tgt already tracked
-
-  $ mv d1/d11/a1 d2/c
-  $ hg addrem -s 0
-  removing d1/d11/a1
-  adding d2/c
-  $ hg rename --after d1/d11/a1 d2/c
-  $ hg status -C
-  A d2/c
-    d1/d11/a1
-  R d1/d11/a1
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d2/c
-
-rename --after a single file to a nonexistent target filename
-
-  $ hg rename --after d1/a dummy
-  d1/a: not recording move - dummy does not exist
-  [1]
-
-move a single file to an existing directory
+Test single file
 
-  $ hg rename d1/d11/a1 d2
-  $ hg status -C
-  A d2/a1
-    d1/d11/a1
-  R d1/d11/a1
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d2/a1
-
-move --after a single file to an existing directory
-
-  $ mv d1/d11/a1 d2
-  $ hg rename --after d1/d11/a1 d2
-  $ hg status -C
-  A d2/a1
-    d1/d11/a1
-  R d1/d11/a1
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d2/a1
-
-rename a file using a relative path
-
-  $ (cd d1/d11; hg rename ../../d2/b e)
-  $ hg status -C
-  A d1/d11/e
-    d2/b
-  R d2/b
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d1/d11/e
-
-rename --after a file using a relative path
-
-  $ (cd d1/d11; mv ../../d2/b e; hg rename --after ../../d2/b e)
-  $ hg status -C
-  A d1/d11/e
-    d2/b
-  R d2/b
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d1/d11/e
-
-rename directory d1 as d3
-
-  $ hg rename d1/ d3
-  moving d1/a to d3/a
-  moving d1/b to d3/b
-  moving d1/ba to d3/ba
-  moving d1/d11/a1 to d3/d11/a1
-  $ hg status -C
-  A d3/a
-    d1/a
-  A d3/b
-    d1/b
-  A d3/ba
-    d1/ba
-  A d3/d11/a1
-    d1/d11/a1
-  R d1/a
-  R d1/b
-  R d1/ba
-  R d1/d11/a1
-  $ hg update -C
-  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d3
-
-rename --after directory d1 as d3
-
-  $ mv d1 d3
-  $ hg rename --after d1 d3
-  moving d1/a to d3/a
-  moving d1/b to d3/b
-  moving d1/ba to d3/ba
-  moving d1/d11/a1 to d3/d11/a1
-  $ hg status -C
-  A d3/a
-    d1/a
-  A d3/b
+# 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 d3/ba
-    d1/ba
-  A d3/d11/a1
-    d1/d11/a1
-  R d1/a
-  R d1/b
-  R d1/ba
-  R d1/d11/a1
-  $ hg update -C
-  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d3
-
-move a directory using a relative path
-
-  $ (cd d2; mkdir d3; hg rename ../d1/d11 d3)
-  moving ../d1/d11/a1 to d3/d11/a1
-  $ hg status -C
-  A d2/d3/d11/a1
-    d1/d11/a1
-  R d1/d11/a1
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d2/d3
-
-move --after a directory using a relative path
-
-  $ (cd d2; mkdir d3; mv ../d1/d11 d3; hg rename --after ../d1/d11 d3)
-  moving ../d1/d11/a1 to d3/d11/a1
-  $ hg status -C
-  A d2/d3/d11/a1
-    d1/d11/a1
-  R d1/d11/a1
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d2/d3
-
-move directory d1/d11 to an existing directory d2 (removes empty d1)
-
-  $ hg rename d1/d11/ d2
-  moving d1/d11/a1 to d2/d11/a1
-  $ hg status -C
-  A d2/d11/a1
-    d1/d11/a1
-  R d1/d11/a1
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d2/d11
-
-move directories d1 and d2 to a new directory d3
-
-  $ mkdir d3
-  $ hg rename d1 d2 d3
-  moving d1/a to d3/d1/a
-  moving d1/b to d3/d1/b
-  moving d1/ba to d3/d1/ba
-  moving d1/d11/a1 to d3/d1/d11/a1
-  moving d2/b to d3/d2/b
-  $ hg status -C
-  A d3/d1/a
-    d1/a
-  A d3/d1/b
+  A d1/d
+# Errors out without --after for now
+  $ hg cp -r . d1/b d1/d
+  abort: --rev requires --after
+  [255]
+# Errors out with non-existent destination
+  $ hg cp -A -r . d1/b d1/non-existent
+  abort: d1/non-existent: copy destination does not exist in 8a9d70fa20c9
+  [255]
+# Successful invocation
+  $ hg cp -A -r . 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 d3/d1/ba
-    d1/ba
-  A d3/d1/d11/a1
-    d1/d11/a1
-  A d3/d2/b
-    d2/b
-  R d1/a
-  R d1/b
-  R d1/ba
-  R d1/d11/a1
-  R d2/b
-  $ hg update -C
-  5 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d3
-
-move --after directories d1 and d2 to a new directory d3
-
-  $ mkdir d3
-  $ mv d1 d2 d3
-  $ hg rename --after d1 d2 d3
-  moving d1/a to d3/d1/a
-  moving d1/b to d3/d1/b
-  moving d1/ba to d3/d1/ba
-  moving d1/d11/a1 to d3/d1/d11/a1
-  moving d2/b to d3/d2/b
-  $ hg status -C
-  A d3/d1/a
-    d1/a
-  A d3/d1/b
+  A d1/d
     d1/b
-  A d3/d1/ba
-    d1/ba
-  A d3/d1/d11/a1
-    d1/d11/a1
-  A d3/d2/b
-    d2/b
-  R d1/a
-  R d1/b
-  R d1/ba
-  R d1/d11/a1
-  R d2/b
-  $ hg update -C
-  5 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d3
 
-move everything under directory d1 to existing directory d2, do not
-overwrite existing files (d2/b)
+Test using directory as destination
 
-  $ hg rename d1/* d2
-  d2/b: not overwriting - file already committed
-  ('hg rename --force' to replace the file by recording a rename)
-  moving d1/d11/a1 to d2/d11/a1
-  [1]
-  $ hg status -C
-  A d2/a
-    d1/a
-  A d2/ba
-    d1/ba
-  A d2/d11/a1
-    d1/d11/a1
-  R d1/a
-  R d1/ba
-  R d1/d11/a1
-  $ diff -u d1/b d2/b
-  --- d1/b	* (glob)
-  +++ d2/b	* (glob)
-  @@ * (glob)
-  -d1/b
-  +d2/b
-  [1]
-  $ hg update -C
-  3 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d2/a d2/ba d2/d11/a1
-
-attempt to move one file into a non-existent directory
-
-  $ hg rename d1/a dx/
-  abort: destination dx/ is not a directory
-  [255]
-  $ hg status -C
-  $ hg update -C
-  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
-
-attempt to move potentially more than one file into a non-existent directory
-
-  $ hg rename 'glob:d1/**' dx
-  abort: with multiple sources, destination must be an existing directory
+  $ 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 -r . d1 d3
+  abort: d3: --rev does not support a directory as destination
   [255]
 
-move every file under d1 to d2/d21
-
-  $ mkdir d2/d21
-  $ hg rename 'glob:d1/**' d2/d21
-  moving d1/a to d2/d21/a
-  moving d1/b to d2/d21/b
-  moving d1/ba to d2/d21/ba
-  moving d1/d11/a1 to d2/d21/a1
-  $ hg status -C
-  A d2/d21/a
-    d1/a
-  A d2/d21/a1
-    d1/d11/a1
-  A d2/d21/b
-    d1/b
-  A d2/d21/ba
-    d1/ba
-  R d1/a
-  R d1/b
-  R d1/ba
-  R d1/d11/a1
-  $ hg update -C
-  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d2/d21
-
-move --after some files under d1 to d2/d21
-
-  $ mkdir d2/d21
-  $ mv d1/a d1/d11/a1 d2/d21
-  $ hg rename --after 'glob:d1/**' d2/d21
-  moving d1/a to d2/d21/a
-  d1/b: not recording move - d2/d21/b does not exist
-  d1/ba: not recording move - d2/d21/ba does not exist
-  moving d1/d11/a1 to d2/d21/a1
-  [1]
-  $ hg status -C
-  A d2/d21/a
-    d1/a
-  A d2/d21/a1
-    d1/d11/a1
-  R d1/a
-  R d1/d11/a1
-  $ hg update -C
-  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d2/d21
-
-move every file under d1 starting with an 'a' to d2/d21 (regexp)
-
-  $ mkdir d2/d21
-  $ hg rename 're:d1/([^a][^/]*/)*a.*' d2/d21
-  moving d1/a to d2/d21/a
-  moving d1/d11/a1 to d2/d21/a1
-  $ hg status -C
-  A d2/d21/a
-    d1/a
-  A d2/d21/a1
-    d1/d11/a1
-  R d1/a
-  R d1/d11/a1
-  $ hg update -C
-  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d2/d21
-
-attempt to overwrite an existing file
-
-  $ echo "ca" > d1/ca
-  $ hg rename d1/ba d1/ca
-  d1/ca: not overwriting - file exists
-  ('hg rename --after' to record the rename)
-  [1]
-  $ hg status -C
-  ? d1/ca
-  $ hg update -C
-  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
-
-forced overwrite of an existing file
-
-  $ echo "ca" > d1/ca
-  $ hg rename --force d1/ba d1/ca
-  $ hg status -C
-  A d1/ca
-    d1/ba
-  R d1/ba
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d1/ca
-
-attempt to overwrite an existing broken symlink
-
-#if symlink
-  $ ln -s ba d1/ca
-  $ hg rename --traceback d1/ba d1/ca
-  d1/ca: not overwriting - file exists
-  ('hg rename --after' to record the rename)
-  [1]
-  $ hg status -C
-  ? d1/ca
-  $ hg update -C
-  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d1/ca
-
-replace a symlink with a file
-
-  $ ln -s ba d1/ca
-  $ hg rename --force d1/ba d1/ca
-  $ hg status -C
-  A d1/ca
-    d1/ba
-  R d1/ba
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d1/ca
-#endif
-
-do not copy more than one source file to the same destination file
-
-  $ mkdir d3
-  $ hg rename d1/* d2/* d3
-  moving d1/d11/a1 to d3/d11/a1
-  d3/b: not overwriting - d2/b collides with d1/b
-  [1]
-  $ hg status -C
-  A d3/a
-    d1/a
-  A d3/b
-    d1/b
-  A d3/ba
-    d1/ba
-  A d3/d11/a1
-    d1/d11/a1
-  R d1/a
-  R d1/b
-  R d1/ba
-  R d1/d11/a1
-  $ hg update -C
-  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d3
-
-move a whole subtree with "hg rename ."
-
-  $ mkdir d3
-  $ (cd d1; hg rename . ../d3)
-  moving a to ../d3/d1/a
-  moving b to ../d3/d1/b
-  moving ba to ../d3/d1/ba
-  moving d11/a1 to ../d3/d1/d11/a1
-  $ hg status -C
-  A d3/d1/a
-    d1/a
-  A d3/d1/b
-    d1/b
-  A d3/d1/ba
-    d1/ba
-  A d3/d1/d11/a1
-    d1/d11/a1
-  R d1/a
-  R d1/b
-  R d1/ba
-  R d1/d11/a1
-  $ hg update -C
-  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d3
-
-move a whole subtree with "hg rename --after ."
-
-  $ mkdir d3
-  $ mv d1/* d3
-  $ (cd d1; hg rename --after . ../d3)
-  moving a to ../d3/a
-  moving b to ../d3/b
-  moving ba to ../d3/ba
-  moving d11/a1 to ../d3/d11/a1
-  $ hg status -C
-  A d3/a
-    d1/a
-  A d3/b
-    d1/b
-  A d3/ba
-    d1/ba
-  A d3/d11/a1
-    d1/d11/a1
-  R d1/a
-  R d1/b
-  R d1/ba
-  R d1/d11/a1
-  $ hg update -C
-  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d3
-
-move the parent tree with "hg rename .."
-
-  $ (cd d1/d11; hg rename .. ../../d3)
-  moving ../a to ../../d3/a
-  moving ../b to ../../d3/b
-  moving ../ba to ../../d3/ba
-  moving a1 to ../../d3/d11/a1
-  $ hg status -C
-  A d3/a
-    d1/a
-  A d3/b
-    d1/b
-  A d3/ba
-    d1/ba
-  A d3/d11/a1
-    d1/d11/a1
-  R d1/a
-  R d1/b
-  R d1/ba
-  R d1/d11/a1
-  $ hg update -C
-  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d3
-
-skip removed files
-
-  $ hg remove d1/b
-  $ hg rename d1 d3
-  moving d1/a to d3/a
-  moving d1/ba to d3/ba
-  moving d1/d11/a1 to d3/d11/a1
-  $ hg status -C
-  A d3/a
-    d1/a
-  A d3/ba
-    d1/ba
-  A d3/d11/a1
-    d1/d11/a1
-  R d1/a
-  R d1/b
-  R d1/ba
-  R d1/d11/a1
-  $ hg update -C
-  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm -rf d3
-
-transitive rename
-
-  $ hg rename d1/b d1/bb
-  $ hg rename d1/bb d1/bc
-  $ hg status -C
-  A d1/bc
-    d1/b
-  R d1/b
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d1/bc
-
-transitive rename --after
-
-  $ hg rename d1/b d1/bb
-  $ mv d1/bb d1/bc
-  $ hg rename --after d1/bb d1/bc
-  $ hg status -C
-  A d1/bc
-    d1/b
-  R d1/b
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d1/bc
-
-  $ echo "# idempotent renames (d1/b -> d1/bb followed by d1/bb -> d1/b)"
-  # idempotent renames (d1/b -> d1/bb followed by d1/bb -> d1/b)
-  $ hg rename d1/b d1/bb
-  $ echo "some stuff added to d1/bb" >> d1/bb
-  $ hg rename d1/bb d1/b
-  $ hg status -C
-  M d1/b
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-
-overwriting with renames (issue1959)
-
-  $ hg rename d1/a d1/c
-  $ hg rename d1/b d1/a
-  $ hg status -C
-  M d1/a
-    d1/b
-  A d1/c
-    d1/a
-  R d1/b
-  $ hg diff --git
-  diff --git a/d1/a b/d1/a
-  --- a/d1/a
-  +++ b/d1/a
-  @@ -1,1 +1,1 @@
-  -d1/a
-  +d1/b
-  diff --git a/d1/b b/d1/b
-  deleted file mode 100644
-  --- a/d1/b
-  +++ /dev/null
-  @@ -1,1 +0,0 @@
-  -d1/b
-  diff --git a/d1/a b/d1/c
-  copy from d1/a
-  copy to d1/c
-  $ hg update -C
-  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm d1/c # The file was marked as added, so 'hg update' action  was 'forget'
-
-check illegal path components
-
-  $ hg rename d1/d11/a1 .hg/foo
-  abort: path contains illegal component: .hg/foo
-  [255]
-  $ hg status -C
-  $ hg rename d1/d11/a1 ../foo
-  abort: ../foo not under root '$TESTTMP'
-  [255]
-  $ hg status -C
-
-  $ mv d1/d11/a1 .hg/foo
-  $ hg rename --after d1/d11/a1 .hg/foo
-  abort: path contains illegal component: .hg/foo
-  [255]
-  $ hg status -C
-  ! d1/d11/a1
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm .hg/foo
-
-  $ hg rename d1/d11/a1 .hg
-  abort: path contains illegal component: .hg/a1
-  [255]
-  $ hg --config extensions.largefiles= rename d1/d11/a1 .hg
-  The fsmonitor extension is incompatible with the largefiles extension and has been disabled. (fsmonitor !)
-  abort: path contains illegal component: .hg/a1
-  [255]
-  $ hg status -C
-  $ hg rename d1/d11/a1 ..
-  abort: ../a1 not under root '$TESTTMP'
-  [255]
-  $ hg --config extensions.largefiles= rename d1/d11/a1 ..
-  The fsmonitor extension is incompatible with the largefiles extension and has been disabled. (fsmonitor !)
-  abort: ../a1 not under root '$TESTTMP'
-  [255]
-  $ hg status -C
-
-  $ mv d1/d11/a1 .hg
-  $ hg rename --after d1/d11/a1 .hg
-  abort: path contains illegal component: .hg/a1
-  [255]
-  $ hg status -C
-  ! d1/d11/a1
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm .hg/a1
-
-  $ (cd d1/d11; hg rename ../../d2/b ../../.hg/foo)
-  abort: path contains illegal component: .hg/foo
-  [255]
-  $ hg status -C
-  $ (cd d1/d11; hg rename ../../d2/b ../../../foo)
-  abort: ../../../foo not under root '$TESTTMP'
-  [255]
-  $ hg status -C
-
-check that stat information such as mtime is preserved on rename - it's unclear
-whether the `touch` and `stat` commands are portable, so we mimic them using
-python.  Not all platforms support precision of even one-second granularity, so
-we allow a rather generous fudge factor here; 1234567890 is 2009, and the
-primary thing we care about is that it's not the machine's current time;
-hopefully it's really unlikely for a machine to have such a broken clock that
-this test fails. :)
-
-  $ mkdir mtime
-Create the file (as empty), then update its mtime and atime to be 1234567890.
-  >>> import os
-  >>> filename = "mtime/f"
-  >>> mtime = 1234567890
-  >>> open(filename, "w").close()
-  >>> os.utime(filename, (mtime, mtime))
-  $ hg ci -qAm 'add mtime dir'
-"hg cp" does not preserve the mtime, so it should be newer than the 2009
-timestamp.
-  $ hg cp -q mtime mtime_cp
-  >>> from __future__ import print_function
-  >>> import os
-  >>> filename = "mtime_cp/f"
-  >>> print(os.stat(filename).st_mtime < 1234567999)
-  False
-"hg mv" preserves the mtime, so it should be ~equal to the 2009 timestamp
-(modulo some fudge factor due to not every system supporting 1s-level
-precision).
-  $ hg mv -q mtime mtime_mv
-  >>> from __future__ import print_function
-  >>> import os
-  >>> filename = "mtime_mv/f"
-  >>> print(os.stat(filename).st_mtime < 1234567999)
-  True
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
@@ -126,4 +126,8 @@ 
   abort: cannot unmark copy in merge commit
   [255]
 
+  $ hg copy --after -r . b1 b2
+  abort: cannot mark copy in merge commit
+  [255]
+
   $ cd ..
diff --git a/tests/test-completion.t b/tests/test-completion.t
--- a/tests/test-completion.t
+++ b/tests/test-completion.t
@@ -256,7 +256,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: after, force, include, exclude, dry-run
+  copy: after, force, rev, include, exclude, dry-run
   debugancestor: 
   debugapplystreamclonebundle: 
   debugbuilddag: mergeable-file, overwritten-file, new-file
diff --git a/relnotes/next b/relnotes/next
--- a/relnotes/next
+++ b/relnotes/next
@@ -1,5 +1,9 @@ 
 == New Features ==
 
+ * `hg copy` now supports a `--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).
+
  * `hg uncopy` can be used to unmark a file as copied. Use `hg uncopy -r REV`
    to unmark already committed copies.
 
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -2316,6 +2316,13 @@ 
             None,
             _(b'forcibly copy over an existing managed file'),
         ),
+        (
+            b'r',
+            b'rev',
+            '',
+            _(b'mark copied in the specified revision'),
+            _(b'REV'),
+        ),
     ]
     + walkopts
     + dryrunopts,
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -1418,7 +1418,18 @@ 
     targets = {}
     after = opts.get(b"after")
     dryrun = opts.get(b"dry_run")
-    ctx = repo[None]
+    rev = opts.get(b'rev')
+    if rev:
+        if 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'--rev requires --after'))
+        ctx = scmutil.revsingle(repo, rev)
+        if len(ctx.parents()) > 1:
+            raise error.Abort(_(b'cannot mark copy in merge commit'))
+    else:
+        ctx = repo[None]
+
     pctx = ctx.p1()
 
     pats = scmutil.expandpats(pats)
@@ -1460,6 +1471,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: --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 -r . foo bar dir` and
+        # `hg cp -r . dir1 dir2`, preferably unifying the code with the existing
+        # functions below.
+        if len(copylist) != 1:
+            raise error.Abort(_(b'--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