changeset 5512:2df762b9c0c5 stable

dirstate: replace _uncommitdirstate() by core's movedirstate() IIRC, when _uncommitdirstate() was upstreamed, it got cleaned up a lot. Let's switch to the simpler upstream version. This fixes the brokeness in `hg uncommit` demonstrated by the previous patch. The problem was that _uncommitdirstate() had not been properly updated to support uncommitting to another commit. If you replace `oldctx.p1()` by `ctx` there, you would get this same diff as from this patch (the `interactive` code path had been fixed this way already).
author Martin von Zweigbergk <martinvonz@google.com>
date Wed, 26 Aug 2020 00:05:52 -0700
parents 80b5d4b85a52
children 13e589fb61f5
files CHANGELOG hgext3rd/evolve/cmdrewrite.py hgext3rd/evolve/evolvecmd.py tests/test-uncommit.t
diffstat 4 files changed, 46 insertions(+), 169 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGELOG	Thu Sep 03 09:21:58 2020 -0700
+++ b/CHANGELOG	Wed Aug 26 00:05:52 2020 -0700
@@ -11,6 +11,7 @@
     into more than one commit
 
   * revset: no longer changeset without topic when running `topic(REVSET)`
+  * uncommit: fix situation where added file would be left in a wrong state
 
 10.0.1 -- 2020-07-31
 --------------------
--- a/hgext3rd/evolve/cmdrewrite.py	Thu Sep 03 09:21:58 2020 -0700
+++ b/hgext3rd/evolve/cmdrewrite.py	Wed Aug 26 00:05:52 2020 -0700
@@ -341,100 +341,49 @@
     newid = repo.commitctx(new)
     return newid
 
-def _uncommitdirstate(repo, oldctx, match, interactive):
-    """Fix the dirstate after switching the working directory from
-    oldctx to a copy of oldctx not containing changed files matched by
-    match.
+# TODO: call core's version once we've dropped support for hg <= 4.9
+def movedirstate(repo, newctx, match=None):
+    """Move the dirstate to newctx and adjust it as necessary.
+
+    A matcher can be provided as an optimization. It is probably a bug to pass
+    a matcher that doesn't match all the differences between the parent of the
+    working copy and newctx.
     """
-    ctx = repo[b'.']
+    oldctx = repo[b'.']
     ds = repo.dirstate
-    copies = dict(ds.copies())
-    if interactive:
-        # In interactive cases, we will find the status between oldctx and ctx
-        # and considering only the files which are changed between oldctx and
-        # ctx, and the status of what changed between oldctx and ctx will help
-        # us in defining the exact behavior
-        st = repo.status(oldctx, ctx, match=match)
-        for f in st.modified:
-            # These are files which are modified between oldctx and ctx which
-            # contains two cases: 1) Were modified in oldctx and some
-            # modifications are uncommitted
-            # 2) Were added in oldctx but some part is uncommitted (this cannot
-            # contain the case when added files are uncommitted completely as
-            # that will result in status as removed not modified.)
-            # Also any modifications to a removed file will result the status as
-            # added, so we have only two cases. So in either of the cases, the
-            # resulting status can be modified or clean.
-            if ds[f] == b'r':
-                # But the file is removed in the working directory, leaving that
-                # as removed
-                continue
+    dscopies = dict(ds.copies())
+    ds.setparents(newctx.node(), node.nullid)
+    s = newctx.status(oldctx, match=match)
+    for f in s.modified:
+        if ds[f] == b'r':
+            # modified + removed -> removed
+            continue
+        ds.normallookup(f)
+
+    for f in s.added:
+        if ds[f] == b'r':
+            # added + removed -> unknown
+            ds.drop(f)
+        elif ds[f] != b'a':
+            ds.add(f)
+
+    for f in s.removed:
+        if ds[f] == b'a':
+            # removed + added -> normal
             ds.normallookup(f)
-
-        for f in st.added:
-            # These are the files which are added between oldctx and ctx(new
-            # one), which means the files which were removed in oldctx
-            # but uncommitted completely while making the ctx
-            # This file should be marked as removed if the working directory
-            # does not adds it back. If it's adds it back, we do a normallookup.
-            # The file can't be removed in working directory, because it was
-            # removed in oldctx
-            if ds[f] == b'a':
-                ds.normallookup(f)
-                continue
+        elif ds[f] != b'r':
             ds.remove(f)
 
-        for f in st.removed:
-            # These are files which are removed between oldctx and ctx, which
-            # means the files which were added in oldctx and were completely
-            # uncommitted in ctx. If a added file is partially uncommitted, that
-            # would have resulted in modified status, not removed.
-            # So a file added in a commit, and uncommitting that addition must
-            # result in file being stated as unknown.
-            if ds[f] == b'r':
-                # The working directory say it's removed, so lets make the file
-                # unknown
-                ds.drop(f)
-                continue
-            ds.add(f)
-    else:
-        st = repo.status(oldctx.p1(), oldctx, match=match)
-        for f in st.modified:
-            if ds[f] == b'r':
-                # modified + removed -> removed
-                continue
-            ds.normallookup(f)
-
-        for f in st.added:
-            if ds[f] == b'r':
-                # added + removed -> unknown
-                ds.drop(f)
-            elif ds[f] != b'a':
-                ds.add(f)
-
-        for f in st.removed:
-            if ds[f] == b'a':
-                # removed + added -> normal
-                ds.normallookup(f)
-            elif ds[f] != b'r':
-                ds.remove(f)
-
     # Merge old parent and old working dir copies
-    oldcopies = {}
-    if interactive:
-        # Interactive had different meaning of the variables so restoring the
-        # original meaning to use them
-        st = repo.status(oldctx.p1(), oldctx, match=match)
-    for f in (st.modified + st.added):
-        src = oldctx[f].renamed()
-        if src:
-            oldcopies[f] = src[0]
-    oldcopies.update(copies)
-    copies = dict((dst, oldcopies.get(src, src))
-                  for dst, src in oldcopies.items())
+    oldcopies = copies.pathcopies(newctx, oldctx, match)
+    oldcopies.update(dscopies)
+    newcopies = {
+        dst: oldcopies.get(src, src)
+        for dst, src in oldcopies.items()
+    }
     # Adjust the dirstate copies
-    for dst, src in copies.items():
-        if (src not in ctx or dst in ctx or ds[dst] != b'a'):
+    for dst, src in newcopies.items():
+        if src not in newctx or dst in newctx or ds[dst] != b'a':
             src = None
         ds.copy(src, dst)
 
@@ -567,8 +516,7 @@
             hg.updaterepo(repo, newid, True)
         else:
             with repo.dirstate.parentchange(), compat.parentchange(repo):
-                repo.dirstate.setparents(newid, node.nullid)
-                _uncommitdirstate(repo, old, match, interactive)
+                movedirstate(repo, repo[newid], match)
         if not repo[newid].files():
             ui.warn(_(b"new changeset is empty\n"))
             ui.status(_(b"(use 'hg prune .' to remove it)\n"))
--- a/hgext3rd/evolve/evolvecmd.py	Thu Sep 03 09:21:58 2020 -0700
+++ b/hgext3rd/evolve/evolvecmd.py	Wed Aug 26 00:05:52 2020 -0700
@@ -642,8 +642,7 @@
         otherdiv = repo[othernode]
 
         with repo.dirstate.parentchange(), compat.parentchange(repo):
-            repo.dirstate.setparents(publicnode, nodemod.nullid)
-            dirstatedance(repo, divergent, publicnode, None)
+            cmdrewrite.movedirstate(repo, repo[publicnode])
         # check if node to be committed has changes same as public one
         s = publicdiv.status()
         if not (s.added or s.removed or s.deleted or s.modified):
@@ -655,9 +654,7 @@
             return (True, publicnode)
 
     with repo.dirstate.parentchange(), compat.parentchange(repo):
-        repo.dirstate.setparents(resparent, nodemod.nullid)
-
-    dirstatedance(repo, divergent, resparent, None)
+        cmdrewrite.movedirstate(repo, repo[resparent])
 
     # merge the branches
     mergebranches(repo, divergent, other, base)
@@ -798,73 +795,6 @@
                              metadata=metadata, ui=repo.ui)
         repo.filteredrevcache.clear()
 
-def dirstatedance(repo, oldparent, newparent, match):
-    """utility function to fix the dirstate when we change parents from
-    oldparent to newparent with a dirty working directory using
-    repo.dirstate.setparents()
-
-    Lets refer oldparent as Pold
-               newparent as Pnew
-
-    Now when we are on oldparent with a dirty working directory, there are three
-    types of files which we are concerned about. They are files having modified,
-    added and removed status.
-
-    Lets refer modified files as Fm
-               added files as Fa
-               removed files as Fr
-
-    Now, between Pold and Pnew, files can be modified, files can be added, files
-    can be removed.
-
-    Lets refer modification of a file between Pold to Pnew as Cm
-               addition of a file between Pold to Pnew as Ca
-               removal of a file between Pold to Pnew as Cr
-
-    Now let's play combinations and permutations:
-
-    |---------------------------------------------------------------|
-    | Type of file |  Changes between |   End status with Pnew as   |
-    |   in wdir    |    Pold -> Pnew  |       wdir parent           |
-    |--------------|------------------|-----------------------------|
-    |              |                  |                             |
-    |   Fm         |      Cm          |       Modified or clean     |
-    |--------------|------------------|-----------------------------|
-    |   Fm         |      Cr          |           Added             |
-    |--------------|------------------|-----------------------------|
-    |   Fm         |      Ca          |       Not possible (1)      |
-    |--------------|------------------|-----------------------------|
-    |   Fa         |      Ca          |       Modified or clean     |
-    |--------------|------------------|-----------------------------|
-    |   Fa         |      Cm          |       Not possible (2)      |
-    |--------------|------------------|-----------------------------|
-    |   Fa         |      Cr          |       Not possible (2)      |
-    |--------------|------------------|-----------------------------|
-    |   Fr         |      Cr          | File should be untracked (3)|
-    |--------------|------------------|-----------------------------|
-    |   Fr         |      Ca          |       Not possible (4)      |
-    |--------------|------------------|-----------------------------|
-    |   Fr         |      Cm          |           Removed           |
-    |--------------|------------------|-----------------------------|
-
-    (1): File is modified in wdir, it means file was present in Pold, so
-         addition of that file between Pold to Pnew is not possible
-
-    (2): File was added in wdir, it means file was not present in Pold, so
-         deletion or modification of that file from Pold to Pnew is not possible
-
-    (3): File should be dropped from the dirstate, Pnew has it removed, so no
-         need to mark that removed again
-
-    (4): File was removed in wdir, it means file was present in Pold, so
-         addition of that file between Pold to Pnew is not possible
-
-    """
-
-    # falling back to an existing function, in future we should have logic in
-    # this function only
-    cmdrewrite._uncommitdirstate(repo, oldparent, match, True)
-
 def mergehook(repo, base, divergent, other):
     """function which extensions can wrap and merge data introduced by them
     while resolving content-divergence"""
--- a/tests/test-uncommit.t	Thu Sep 03 09:21:58 2020 -0700
+++ b/tests/test-uncommit.t	Wed Aug 26 00:05:52 2020 -0700
@@ -364,10 +364,9 @@
   $ hg cat b --rev .
   b: no such file in rev 5b27f6b17da2
   [1]
-BROKEN: 'b' is no longer in the parent commit, so it should be marked 'A'
   $ hg status
-  M b
   A aa
+  A b
 
 Test uncommiting predecessors
 
@@ -427,9 +426,9 @@
   R m
   R n
   $ hg status
+  M b
   M d
   A aa
-  R b
   $ hg amend --extract j
   $ hg status --change .
   M o
@@ -444,10 +443,10 @@
   R m
   R n
   $ hg status
+  M b
   M d
   M j
   A aa
-  R b
 
 (with all)
 
@@ -456,6 +455,7 @@
   (use 'hg prune .' to remove it)
   $ hg status --change .
   $ hg status
+  M b
   M d
   M j
   M o
@@ -465,7 +465,6 @@
   A h
   A k
   A l
-  R b
   R c
   R f
   R g
@@ -478,11 +477,10 @@
 
   $ hg amend
   $ hg status
-  ? b
 
   $ hg diff -c . --stat
    aa |  1 +
-   b  |  1 -
+   b  |  1 +
    c  |  1 -
    d  |  1 +
    e  |  1 +
@@ -496,7 +494,7 @@
    m  |  1 -
    n  |  1 -
    o  |  1 +
-   15 files changed, 9 insertions(+), 6 deletions(-)
+   15 files changed, 10 insertions(+), 5 deletions(-)
   $ hg uncommit --revert --all
   new changeset is empty
   (use 'hg prune .' to remove it)