Patchwork D6479: unshelve: first prototype of restoring unresolved changes

login
register
mail settings
Submitter phabricator
Date June 5, 2019, 7:05 p.m.
Message ID <differential-rev-PHID-DREV-xvpnr6v2u2pltuobvyqh-req@phab.mercurial-scm.org>
Download mbox | patch
Permalink /patch/40325/
State New
Headers show

Comments

phabricator - June 5, 2019, 7:05 p.m.
navaneeth.suresh created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  This patch adds an `--unresolved` flag to `unshelve`.
  
  This will get the user back to the old unresolved merge by the following
  steps:
  
  step 1: If the user has committed new changesets after shelving the changes,
  they must update the working directory to one of the merge parents.
  
  step 2: Internally, merge `p1` with `p2` with the merge tool `:fail`.
  This will not update the contents of the files with conflicting changes.
  Instead, it will mark them as unresolved.
  
  step 3: This internal merge will also mark the files which are already
  resolved by the user in the unresolved shelve changeset as unresolved. But,
  we will move the contents of `$HGRCPATH/merge-unresolved/<basename>/`
  to `$HGRCPATH/merge/` so that we can restore the partially resolved states.
  
  step 4: We now have a state in which files marked as resolved might have
  conflicts. But, we will apply the changes in shelve on the top of this so that we
  can get our old unresolved merge again by the usual `unshelve` mechanism.
  The usual rebase step is avoided on unresolved shelve changesets.
  
  `$ hg unshelve --unresolved` will abort when:
  
  1. The working directory is dirty.
  2. If there is an ongoing merge.
  3. If the working directory is not at either p1 or p2.
  
  (p1, p2 are the parents of the unresolved shelve changeset)

REPOSITORY
  rHG Mercurial

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

AFFECTED FILES
  hgext/shelve.py
  tests/test-shelve-unresolved.t

CHANGE DETAILS




To: navaneeth.suresh, #hg-reviewers
Cc: mercurial-devel
phabricator - June 10, 2019, 2:40 p.m.
pulkit added a comment.


  > step 2: Internally, merge p1 with p2 with the merge tool :fail.
  >  This will not update the contents of the files with conflicting changes.
  >  Instead, it will mark them as unresolved.
  
  Can we prevent merging again? Since we are getting content from old unresolved commit, copying mergestate from previously stored one, what purpose does this merging serves?
  
  > step 3: This internal merge will also mark the files which are already
  >  resolved by the user in the unresolved shelve changeset as unresolved. But,
  >  we will move the contents of $HGRCPATH/merge-unresolved/<basename>/
  >  to $HGRCPATH/merge/ so that we can restore the partially resolved states.
  > 
  > step 4: We now have a state in which files marked as resolved might have
  >  conflicts. But, we will apply the changes in shelve on the top of this so that we
  >  can get our old unresolved merge again by the usual unshelve mechanism.
  >  The usual rebase step is avoided on unresolved shelve changesets.

REPOSITORY
  rHG Mercurial

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

To: navaneeth.suresh, #hg-reviewers
Cc: pulkit, mercurial-devel
phabricator - June 10, 2019, 3:48 p.m.
navaneeth.suresh added a comment.


  > Can we prevent merging again? Since we are getting content from old unresolved commit, copying mergestate from previously stored one, what purpose does this merging serves?
  
  The usual unshelve mechanism restores the content of the files in the mergestate. However, we'll be still in a state with only one parent. This merge will help to recreate the exact same state with two parents.

REPOSITORY
  rHG Mercurial

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

To: navaneeth.suresh, #hg-reviewers
Cc: pulkit, mercurial-devel
phabricator - June 10, 2019, 3:56 p.m.
pulkit added a comment.


  In https://phab.mercurial-scm.org/D6479#94555, @navaneeth.suresh wrote:
  
  > > Can we prevent merging again? Since we are getting content from old unresolved commit, copying mergestate from previously stored one, what purpose does this merging serves?
  >
  > The usual unshelve mechanism restores the content of the files in the mergestate. However, we'll be still in a state with only one parent. This merge will help to recreate the exact same state with two parents.
  
  
  There are other ways to set the second parent, like dirstate.setparents() I think. It is worth investigating to find a way to do it without merging again.

REPOSITORY
  rHG Mercurial

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

To: navaneeth.suresh, #hg-reviewers
Cc: pulkit, mercurial-devel
phabricator - June 10, 2019, 4 p.m.
navaneeth.suresh added a comment.


  In https://phab.mercurial-scm.org/D6479#94556, @pulkit wrote:
  
  > In https://phab.mercurial-scm.org/D6479#94555, @navaneeth.suresh wrote:
  >
  > > > Can we prevent merging again? Since we are getting content from old unresolved commit, copying mergestate from previously stored one, what purpose does this merging serves?
  > >
  > > The usual unshelve mechanism restores the content of the files in the mergestate. However, we'll be still in a state with only one parent. This merge will help to recreate the exact same state with two parents.
  >
  >
  > There are other ways to set the second parent, like dirstate.setparents() I think. It is worth investigating to find a way to do it without merging again.
  
  
  Interesting. Then, I shall investigate it and update the diff.

REPOSITORY
  rHG Mercurial

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

To: navaneeth.suresh, #hg-reviewers
Cc: pulkit, mercurial-devel
phabricator - June 10, 2019, 4:36 p.m.
pulkit added inline comments.

INLINE COMMENTS

> shelve.py:722
> +            repo.dirstate.setparents(p1.node(), p2.node())
> +            repo.dirstate.write(repo.currenttransaction())
> +

why do we need to do this?

> shelve.py:724
> +
> +    if not os.path.exists(repo.vfs.join('merge-unresolved')):
> +        util.makedir(repo.vfs.join('merge-unresolved'), False)

if merge-unresolved/ does not exists, it means there are no unresolved shelves, right? we should not be creating it then.

> shelve.py:1039
>          raise error.Abort(_("shelved change '%s' not found") % basename)
> +    if unresolved:
> +        cmdutil.bailifchanged(repo)

we can do this check by looking into shelvectx extras below.

REPOSITORY
  rHG Mercurial

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

To: navaneeth.suresh, #hg-reviewers
Cc: pulkit, mercurial-devel
phabricator - June 10, 2019, 5:39 p.m.
pulkit added inline comments.

INLINE COMMENTS

> shelve.py:724
> +    # restore the status of resolved files in shelvectx.
> +    util.rename(repo.vfs.join('merge-unresolved/%s/' % basename),
> +                repo.vfs.join('merge'))

`vfs.rename`  should be used here.

REPOSITORY
  rHG Mercurial

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

To: navaneeth.suresh, #hg-reviewers
Cc: pulkit, mercurial-devel
phabricator - June 10, 2019, 10:48 p.m.
pulkit added inline comments.

INLINE COMMENTS

> shelve.py:717
>  
> +def restoreunresolvedshelve(repo, ui, basename, pctx, shelvectx):
> +    p1, p2 = shelvectx.parents()

the functions needs documentation.

> shelve.py:722
> +
> +    # Replace `merge/` with `merge-unresolved/<basename>/` to
> +    # restore the status of resolved files in shelvectx.

this comment can be a part of function documentation.

REPOSITORY
  rHG Mercurial

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

To: navaneeth.suresh, #hg-reviewers
Cc: pulkit, mercurial-devel

Patch

diff --git a/tests/test-shelve-unresolved.t b/tests/test-shelve-unresolved.t
--- a/tests/test-shelve-unresolved.t
+++ b/tests/test-shelve-unresolved.t
@@ -1,3 +1,17 @@ 
+  $ addunresolvedmerge() {
+  >   echo A >> $1
+  >   echo A >> $2
+  >   hg ci -m A
+  >   echo B >> $1
+  >   echo B >> $2
+  >   hg ci -m B
+  >   hg up $3
+  >   echo C >> $1
+  >   echo C >> $2
+  >   hg ci -m C
+  >   hg merge -r $(($3+1))
+  > }
+
 Test shelve with unresolved mergestate
 
   $ cat >> $HGRCPATH <<EOF
@@ -156,3 +170,283 @@ 
   +=======
   +B
   +>>>>>>> merge rev:    fd9a4049234b - test: B
+
+-- now, fix an urgent bug
+  $ echo fixed >> bug
+  $ ls
+  bar
+  bug
+  file1
+  file1.orig
+  file2
+  file2.orig
+  $ hg add bug
+  $ hg ci -m "fix bug"
+  $ hg log -G
+  @  changeset:   3:a53a9a7475b3
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     fix bug
+  |
+  o  changeset:   2:69004294ad57
+  |  parent:      0:c32ef6121744
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     C
+  |
+  | o  changeset:   1:fd9a4049234b
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     B
+  |
+  o  changeset:   0:c32ef6121744
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     A
+  
+-- let's get back to the old mergestate
+-- we need to update to one of the merge parents. otherwise, abort.
+  $ hg unshelve --unresolved
+  unshelving change 'default'
+  abort: dirstate is not on either of the merge parents.
+  use hg update to one of the merge parents.
+  [255]
+  $ hg up 2
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ cat file2
+  A
+  C
+
+-- flag --unshelve is not passed. but, the last shelve is unresolved
+  $ hg unshelve
+  unshelving change 'default'
+  abort: default is an unresolved shelve, use --unresolved to unshelve it
+  [255]
+
+  $ hg unshelve --unresolved
+  unshelving change 'default'
+  $ hg log -G
+  o  changeset:   3:a53a9a7475b3
+  |  tag:         tip
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     fix bug
+  |
+  @  changeset:   2:69004294ad57
+  |  parent:      0:c32ef6121744
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     C
+  |
+  | @  changeset:   1:fd9a4049234b
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     B
+  |
+  o  changeset:   0:c32ef6121744
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     A
+  
+  $ cat file1
+  A
+  B
+  C
+  $ hg diff
+  diff -r 69004294ad57 file1
+  --- a/file1	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/file1	Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,2 +1,3 @@
+   A
+  +B
+   C
+  diff -r 69004294ad57 file2
+  --- a/file2	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/file2	Thu Jan 01 00:00:00 1970 +0000
+  @@ -1,2 +1,6 @@
+   A
+  +<<<<<<< working copy: 69004294ad57 - test: C
+   C
+  +=======
+  +B
+  +>>>>>>> merge rev:    fd9a4049234b - test: B
+  $ cat file2
+  A
+  <<<<<<< working copy: 69004294ad57 - test: C
+  C
+  =======
+  B
+  >>>>>>> merge rev:    fd9a4049234b - test: B
+  $ hg resolve -l
+  R file1
+  U file2
+
+-- flag --unresolved is passed but the top most shelve is not unresolved
+  $ hg shelve --unresolved
+  shelved as default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo garbage >> bug
+  $ hg st
+  ? bar
+  ? bug
+  ? file1.orig
+  ? file2.orig
+  $ hg add bug
+  $ hg shelve
+  shelved as default-01
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg unshelve --unresolved
+  unshelving change 'default-01'
+  abort: default-01 is not an unresolved shelve
+  
+  [255]
+
+-- now, unshelve default
+  $ hg unshelve -n default --unresolved
+
+-- commit the merge after completing conflict resolution
+  $ cat >> file2 <<EOF
+  > A
+  > B
+  > C
+  > EOF
+  $ hg resolve -m file2
+  (no more unresolved files)
+  $ hg ci -m merge
+  $ hg verify
+  checking changesets
+  checking manifests
+  crosschecking files in changesets and manifests
+  checking files
+  checked 5 changesets with 9 changes to 3 files
+  $ hg log -G
+  @    changeset:   4:745dca2ee1f1
+  |\   tag:         tip
+  | |  parent:      2:69004294ad57
+  | |  parent:      1:fd9a4049234b
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     merge
+  | |
+  | | o  changeset:   3:a53a9a7475b3
+  | |/   user:        test
+  | |    date:        Thu Jan 01 00:00:00 1970 +0000
+  | |    summary:     fix bug
+  | |
+  | o  changeset:   2:69004294ad57
+  | |  parent:      0:c32ef6121744
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     C
+  | |
+  o |  changeset:   1:fd9a4049234b
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     B
+  |
+  o  changeset:   0:c32ef6121744
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     A
+  
+
+-- flag --unsresolved is passed but we don’t have any unresolved shelve
+  $ hg unshelve --unresolved
+  unshelving change 'default-01'
+  abort: default-01 is not an unresolved shelve
+  
+  [255]
+
+-- when working directory is dirty
+  $ addunresolvedmerge file1 file2 5
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  created new head
+  merging file1
+  merging file2
+  warning: conflicts while merging file1! (edit, then use 'hg resolve --mark')
+  warning: conflicts while merging file2! (edit, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 2 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
+  [1]
+  $ hg shelve --unresolved
+  shelved as default
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo dirt >> bar
+  $ hg add bar
+  $ hg unshelve --unresolved
+  unshelving change 'default'
+  abort: uncommitted changes
+  [255]
+
+--- unshelve --unresolved when there is another merge going on
+  $ hg ci -m dirt
+  $ hg up 7
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ echo foo >> bug
+  $ hg add bug
+  $ hg ci -m foo2
+  created new head
+  $ hg log -G
+  @  changeset:   9:c74a624102ed
+  |  tag:         tip
+  |  parent:      7:974ec4298b79
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     foo2
+  |
+  | o  changeset:   8:4acf09fb3a59
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     dirt
+  |
+  o  changeset:   7:974ec4298b79
+  |  parent:      5:db68c6c84fe6
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     C
+  |
+  | o  changeset:   6:e236d497f76b
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     B
+  |
+  o  changeset:   5:db68c6c84fe6
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     A
+  |
+  o    changeset:   4:745dca2ee1f1
+  |\   parent:      2:69004294ad57
+  | |  parent:      1:fd9a4049234b
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     merge
+  | |
+  | | o  changeset:   3:a53a9a7475b3
+  | |/   user:        test
+  | |    date:        Thu Jan 01 00:00:00 1970 +0000
+  | |    summary:     fix bug
+  | |
+  | o  changeset:   2:69004294ad57
+  | |  parent:      0:c32ef6121744
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     C
+  | |
+  o |  changeset:   1:fd9a4049234b
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     B
+  |
+  o  changeset:   0:c32ef6121744
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     A
+  
+  $ hg merge -r 8
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg unshelve --unresolved
+  abort: cannot unshelve while merging
+  [255]
diff --git a/hgext/shelve.py b/hgext/shelve.py
--- a/hgext/shelve.py
+++ b/hgext/shelve.py
@@ -26,6 +26,7 @@ 
 import errno
 import itertools
 import os
+import shutil
 import stat
 
 from mercurial.i18n import _
@@ -714,6 +715,29 @@ 
         ui.status(_('marked working directory as branch %s\n')
                   % branchtorestore)
 
+def restoreunresolvedshelve(repo, ui, basename, pctx, shelvectx):
+    p1, p2 = shelvectx.parents()
+    overrides = {('ui', 'forcemerge'): ':fail'}
+    # Rather than attempting to merge files that were modified on
+    # both branches, `:fail` marks them as unresolved.
+    # This will also mark resolved files in unresolved shelvectx as
+    # unresolved. We'll restore their status and content later.
+    with ui.configoverride(overrides, 'unshelve'):
+        # dirstate can be either p1 or p2.
+        targetctx = p2
+        if pctx.node() == p2.node():
+            targetctx = p1
+        merge.update(repo, targetctx, branchmerge=True, mergeforce=False,
+                     force=True)
+
+    if not os.path.exists(repo.vfs.join('merge-unresolved')):
+        util.makedir(repo.vfs.join('merge-unresolved'), False)
+    shutil.rmtree(repo.vfs.join('merge'))
+    # Replace `merge/` with `merge-unresolved/<basename>/` to
+    # restore the status of resolved files in shelvectx.
+    util.rename(repo.vfs.join('merge-unresolved/%s/' % basename),
+                repo.vfs.join('merge'))
+
 def unshelvecleanup(ui, repo, name, opts):
     """remove related files after an unshelve"""
     if not opts.get('keep'):
@@ -918,7 +942,9 @@ 
            _('restore shelved change with given name'), _('NAME')),
           ('t', 'tool', '', _('specify merge tool')),
           ('', 'date', '',
-           _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
+           _('set date for temporary commits (DEPRECATED)'), _('DATE')),
+          ('', 'unresolved', None,
+           _('unshelve mergestate with unresolved files'))],
          _('hg unshelve [[-n] SHELVED]'),
          helpcategory=command.CATEGORY_WORKING_DIRECTORY)
 def unshelve(ui, repo, *shelved, **opts):
@@ -964,6 +990,7 @@ 
     opts = pycompat.byteskwargs(opts)
     abortf = opts.get('abort')
     continuef = opts.get('continue')
+    unresolved = opts.get('unresolved')
     if not abortf and not continuef:
         cmdutil.checkunfinished(repo)
     shelved = list(shelved)
@@ -1019,6 +1046,14 @@ 
 
     if not shelvedfile(repo, basename, patchextension).exists():
         raise error.Abort(_("shelved change '%s' not found") % basename)
+    if unresolved:
+        cmdutil.bailifchanged(repo)
+        if not os.path.exists(repo.vfs.join('merge-unresolved/%s' % basename)):
+            raise error.Abort(_('%s is not an unresolved shelve\n') %
+                                basename)
+    elif os.path.exists(repo.vfs.join('merge-unresolved/%s' % basename)):
+        raise error.Abort(_('%s is an unresolved shelve,'
+                            ' use --unresolved to unshelve it') % basename)
 
     repo = repo.unfiltered()
     lock = tr = None
@@ -1044,13 +1079,22 @@ 
         if shelvectx.branch() != shelvectx.p1().branch():
             branchtorestore = shelvectx.branch()
 
-        shelvectx = _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev,
-                                          basename, pctx, tmpwctx,
-                                          shelvectx, branchtorestore,
-                                          activebookmark)
+        if unresolved:
+            p1, p2 = shelvectx.parents()
+            if pctx.node() not in [p1.node(), p2.node()]:
+                raise error.Abort(_('dirstate is not on either of the merge'
+                                    ' parents.\nuse hg update to one of the'
+                                    ' merge parents.'))
+        else:
+            shelvectx = _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev,
+                                            basename, pctx, tmpwctx,
+                                            shelvectx, branchtorestore,
+                                            activebookmark)
         overrides = {('ui', 'forcemerge'): opts.get('tool', '')}
         with ui.configoverride(overrides, 'unshelve'):
             mergefiles(ui, repo, pctx, shelvectx)
+        if unresolved:
+            restoreunresolvedshelve(repo, ui, basename, pctx, shelvectx)
         restorebranch(ui, repo, branchtorestore)
         _forgetunknownfiles(repo, shelvectx, addedbefore)