rewind: add --keep flag that "doesn't modify working directory"
authorAnton Shestakov <av6@dwimlabs.net>
Thu, 25 Jul 2019 18:37:16 +0800
changeset 4821 d8e36e60aea0
parent 4820 d842a4c6fc4a
child 4827 4c6dd20e8cc2
rewind: add --keep flag that "doesn't modify working directory" The actual logic is more complicated than the flag description, but it's sufficiently similar to other --keep flags in action. Unlike strip (or prune), rewind always needs to modify the working directory to commit new revisions that "revive" old ones [1], see _revive_revision() (and rewriteutil.rewrite()). Because of that we don't prevent rewind from modifying wdir, but instead use hg.updaterepo() to update to the old changeset after the "revival" process is complete. Then we rebuild the dirstate based on the commit that rewind would update to without --keep. Since dirstate.rebuild() doesn't restore status of some files (added, removed, also copies and renames), we rely on cmdutil.revert(). It's a fairly crude solution and needs to be removed when implementing the missing copy tracing between oldctx and newctx (which are related only by obsolescence). [1] IOW this means that --keep doesn't allow rewinding if wdir is dirty (unlike e.g. strip).
CHANGELOG
hgext3rd/evolve/rewind.py
tests/test-rewind.t
--- a/CHANGELOG	Tue Jul 23 18:05:40 2019 +0800
+++ b/CHANGELOG	Thu Jul 25 18:37:16 2019 +0800
@@ -1,7 +1,7 @@
 Changelog
 =========
 
-9.1.0 - in progress
+9.2.0 - in progress
 -------------------
 
   * python3: beta support for Python 3.6+
@@ -9,6 +9,7 @@
   * prune: clarify error message when no revision were passed,
   * evolve: avoid possible race conditions bu locking earlier
   * abort: add support for `evolve` and `pick` to `hg abort` (hg-5.1+)
+  * rewind: add --keep flag to preserve working copy
 
 9.1.0 -- 2019-07-29
 -------------------
--- a/hgext3rd/evolve/rewind.py	Tue Jul 23 18:05:40 2019 +0800
+++ b/hgext3rd/evolve/rewind.py	Thu Jul 25 18:37:16 2019 +0800
@@ -32,8 +32,10 @@
      (b'', b'exact', None, _(b"only rewind explicitly selected revisions")),
      (b'', b'from', [],
       _(b"rewind these revisions to their predecessors"), _(b'REV')),
+     (b'k', b'keep', None,
+      _(b"do not modify working directory during rewind")),
      ],
-    _(b'[--as-divergence] [--exact] [--to REV]... [--from REV]...'),
+    _(b'[--as-divergence] [--exact] [--keep] [--to REV]... [--from REV]...'),
     helpbasic=True)
 def rewind(ui, repo, **opts):
     """rewind a stack of changesets to a previous state
@@ -99,6 +101,8 @@
 
         # Check that we can rewind these changesets
         with repo.transaction(b'rewind'):
+            oldctx = repo[b'.']
+
             for rev in sorted(rewinded):
                 ctx = unfi[rev]
                 rewindmap[ctx.node()] = _revive_revision(unfi, rev, rewindmap)
@@ -115,12 +119,44 @@
                     update_target = newdest[-1]
             obsolete.createmarkers(unfi, relationships, operation=b'rewind')
             if update_target is not None:
-                hg.updaterepo(repo, update_target, False)
+                if opts.get('keep'):
+                    hg.updaterepo(repo, oldctx, True)
+
+                    # This is largely the same as the implementation in
+                    # strip.stripcmd() and cmdrewrite.cmdprune().
+
+                    # only reset the dirstate for files that would actually
+                    # change between the working context and the revived cset
+                    newctx = repo[update_target]
+                    changedfiles = []
+                    for ctx in [oldctx, newctx]:
+                        # blindly reset the files, regardless of what actually
+                        # changed
+                        changedfiles.extend(ctx.files())
+
+                    # reset files that only changed in the dirstate too
+                    dirstate = repo.dirstate
+                    dirchanges = [f for f in dirstate if dirstate[f] != 'n']
+                    changedfiles.extend(dirchanges)
+                    repo.dirstate.rebuild(newctx.node(), newctx.manifest(),
+                                          changedfiles)
+
+                    # TODO: implement restoration of copies/renames
+                    # Ideally this step should be handled by dirstate.rebuild
+                    # or scmutil.movedirstate, but right now there's no copy
+                    # tracing across obsolescence relation (oldctx <-> newctx).
+                    revertopts = {'no_backup': True, 'all': True,
+                                  'rev': oldctx.node()}
+                    with ui.configoverride({(b'ui', b'quiet'): True}):
+                        cmdutil.revert(repo.ui, repo, oldctx,
+                                       repo.dirstate.parents(), **revertopts)
+                else:
+                    hg.updaterepo(repo, update_target, False)
 
     repo.ui.status(_(b'rewinded to %d changesets\n') % len(rewinded))
     if relationships:
         repo.ui.status(_(b'(%d changesets obsoleted)\n') % len(relationships))
-    if update_target is not None:
+    if update_target is not None and not opts.get('keep'):
         ui.status(_(b'working directory is now at %s\n') % repo[b'.'])
 
 def _select_rewinded(repo, opts):
--- a/tests/test-rewind.t	Tue Jul 23 18:05:40 2019 +0800
+++ b/tests/test-rewind.t	Thu Jul 25 18:37:16 2019 +0800
@@ -970,3 +970,83 @@
   ~
 
   $ cd ..
+
+Rewind --keep
+=============
+
+  $ hg init rewind-keep
+  $ cd rewind-keep
+  $ echo root > root
+  $ hg ci -qAm 'root'
+
+  $ echo apple > a
+  $ echo banana > b
+  $ hg ci -qAm initial
+
+  $ hg rm b
+  $ echo apricot > a
+  $ echo coconut > c
+  $ hg add c
+  $ hg status
+  M a
+  A c
+  R b
+  $ hg amend -m amended
+  $ hg glf --hidden
+  @  2: amended (a c)
+  |
+  | x  1: initial (a b)
+  |/
+  o  0: root (root)
+  
+
+Clean wdir
+
+  $ hg rewind --keep --to 'desc("initial")' --hidden
+  rewinded to 1 changesets
+  (1 changesets obsoleted)
+  $ hg obslog
+  @    b4c97fddc16a (3) initial
+  |\
+  x |  2ea5be2f8751 (2) amended
+  |/     rewritten(description, meta, content) as b4c97fddc16a using rewind by test (Thu Jan 01 00:00:06 1970 +0000)
+  |
+  x  30704102d912 (1) initial
+       rewritten(description, content) as 2ea5be2f8751 using amend by test (Thu Jan 01 00:00:06 1970 +0000)
+       rewritten(meta) as b4c97fddc16a using rewind by test (Thu Jan 01 00:00:06 1970 +0000)
+  
+  $ hg glf --hidden
+  @  3: initial (a b)
+  |
+  | x  2: amended (a c)
+  |/
+  | x  1: initial (a b)
+  |/
+  o  0: root (root)
+  
+  $ hg st
+  M a
+  A c
+  R b
+
+Making wdir even more dirty
+
+  $ echo avocado > a
+  $ echo durian > d
+  $ hg st
+  M a
+  A c
+  R b
+  ? d
+
+No rewinding without --keep
+
+  $ hg rewind --to 'desc("amended")' --hidden
+  abort: uncommitted changes
+  [255]
+
+XXX: Unfortunately, even with --keep it's not allowed
+
+  $ hg rewind --keep --to 'desc("amended")' --hidden
+  abort: uncommitted changes
+  [255]