--- a/hgext/shelve.py Fri Oct 25 01:14:18 2013 +0900
+++ b/hgext/shelve.py Wed Oct 23 13:12:48 2013 -0700
@@ -27,6 +27,7 @@
from mercurial import error, hg, mdiff, merge, patch, repair, util
from mercurial import templatefilters
from mercurial import lock as lockmod
+from hgext import rebase
import errno
cmdtable = {}
@@ -95,25 +96,35 @@
raise util.Abort(_('this version of shelve is incompatible '
'with the version used in this repo'))
name = fp.readline().strip()
+ wctx = fp.readline().strip()
+ pendingctx = fp.readline().strip()
parents = [bin(h) for h in fp.readline().split()]
stripnodes = [bin(h) for h in fp.readline().split()]
+ unknownfiles = fp.readline()[:-1].split('\0')
finally:
fp.close()
obj = cls()
obj.name = name
+ obj.wctx = repo[bin(wctx)]
+ obj.pendingctx = repo[bin(pendingctx)]
obj.parents = parents
obj.stripnodes = stripnodes
+ obj.unknownfiles = unknownfiles
return obj
@classmethod
- def save(cls, repo, name, stripnodes):
+ def save(cls, repo, name, originalwctx, pendingctx, stripnodes,
+ unknownfiles):
fp = repo.opener(cls._filename, 'wb')
fp.write('%i\n' % cls._version)
fp.write('%s\n' % name)
+ fp.write('%s\n' % hex(originalwctx.node()))
+ fp.write('%s\n' % hex(pendingctx.node()))
fp.write('%s\n' % ' '.join([hex(p) for p in repo.dirstate.parents()]))
fp.write('%s\n' % ' '.join([hex(n) for n in stripnodes]))
+ fp.write('%s\n' % '\0'.join(unknownfiles))
fp.close()
@classmethod
@@ -368,44 +379,55 @@
lock = None
try:
checkparents(repo, state)
+
+ util.rename(repo.join('unshelverebasestate'),
+ repo.join('rebasestate'))
+ try:
+ rebase.rebase(ui, repo, **{
+ 'abort' : True
+ })
+ except Exception:
+ util.rename(repo.join('rebasestate'),
+ repo.join('unshelverebasestate'))
+ raise
+
lock = repo.lock()
- merge.mergestate(repo).reset()
- if opts['keep']:
- repo.setparents(repo.dirstate.parents()[0])
- else:
- revertfiles = readshelvedfiles(repo, state.name)
- wctx = repo.parents()[0]
- cmdutil.revert(ui, repo, wctx, [wctx.node(), nullid],
- *pathtofiles(repo, revertfiles),
- **{'no_backup': True})
- # fix up the weird dirstate states the merge left behind
- mf = wctx.manifest()
- dirstate = repo.dirstate
- for f in revertfiles:
- if f in mf:
- dirstate.normallookup(f)
- else:
- dirstate.drop(f)
- dirstate._pl = (wctx.node(), nullid)
- dirstate._dirty = True
+
+ mergefiles(ui, repo, state.wctx, state.pendingctx, state.unknownfiles)
+
repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve')
shelvedstate.clear(repo)
ui.warn(_("unshelve of '%s' aborted\n") % state.name)
finally:
lockmod.release(lock, wlock)
+def mergefiles(ui, repo, wctx, shelvectx, unknownfiles):
+ """updates to wctx and merges the changes from shelvectx into the
+ dirstate. drops any files in unknownfiles from the dirstate."""
+ oldquiet = ui.quiet
+ try:
+ ui.quiet = True
+ hg.update(repo, wctx.node())
+ files = []
+ files.extend(shelvectx.files())
+ files.extend(shelvectx.parents()[0].files())
+ cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents(),
+ *pathtofiles(repo, files),
+ **{'no_backup': True})
+ finally:
+ ui.quiet = oldquiet
+
+ # Send untracked files back to being untracked
+ dirstate = repo.dirstate
+ for f in unknownfiles:
+ dirstate.drop(f)
+
def unshelvecleanup(ui, repo, name, opts):
"""remove related files after an unshelve"""
if not opts['keep']:
for filetype in 'hg files patch'.split():
shelvedfile(repo, name, filetype).unlink()
-def finishmerge(ui, repo, ms, stripnodes, name, opts):
- # Reset the working dir so it's no longer in a merge state.
- dirstate = repo.dirstate
- dirstate.setparents(dirstate._pl[0])
- shelvedstate.clear(repo)
-
def unshelvecontinue(ui, repo, state, opts):
"""subcommand to continue an in-progress unshelve"""
# We're finishing off a merge. First parent is our original
@@ -419,9 +441,30 @@
raise util.Abort(
_("unresolved conflicts, can't continue"),
hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
- finishmerge(ui, repo, ms, state.stripnodes, state.name, opts)
+
lock = repo.lock()
+
+ util.rename(repo.join('unshelverebasestate'),
+ repo.join('rebasestate'))
+ try:
+ rebase.rebase(ui, repo, **{
+ 'continue' : True
+ })
+ except Exception:
+ util.rename(repo.join('rebasestate'),
+ repo.join('unshelverebasestate'))
+ raise
+
+ shelvectx = repo['tip']
+ if not shelvectx in state.pendingctx.children():
+ # rebase was a no-op, so it produced no child commit
+ shelvectx = state.pendingctx
+
+ mergefiles(ui, repo, state.wctx, shelvectx, state.unknownfiles)
+
+ state.stripnodes.append(shelvectx.node())
repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve')
+ shelvedstate.clear(repo)
unshelvecleanup(ui, repo, state.name, opts)
ui.status(_("unshelve of '%s' complete\n") % state.name)
finally:
@@ -491,71 +534,96 @@
shelvedfiles = readshelvedfiles(repo, basename)
- m, a, r, d = repo.status()[:4]
- unsafe = set(m + a + r + d).intersection(shelvedfiles)
- if unsafe:
- ui.warn(_('the following shelved files have been modified:\n'))
- for f in sorted(unsafe):
- ui.warn(' %s\n' % f)
- ui.warn(_('you must commit, revert, or shelve your changes before you '
- 'can proceed\n'))
- raise util.Abort(_('cannot unshelve due to local changes\n'))
-
wlock = lock = tr = None
try:
lock = repo.lock()
+ wlock = repo.wlock()
tr = repo.transaction('unshelve', report=lambda x: None)
oldtiprev = len(repo)
+
+ wctx = repo['.']
+ tmpwctx = wctx
+ # The goal is to have a commit structure like so:
+ # ...-> wctx -> tmpwctx -> shelvectx
+ # where tmpwctx is an optional commit with the user's pending changes
+ # and shelvectx is the unshelved changes. Then we merge it all down
+ # to the original wctx.
+
+ # Store pending changes in a commit
+ m, a, r, d, u = repo.status(unknown=True)[:5]
+ if m or a or r or d or u:
+ def commitfunc(ui, repo, message, match, opts):
+ hasmq = util.safehasattr(repo, 'mq')
+ if hasmq:
+ saved, repo.mq.checkapplied = repo.mq.checkapplied, False
+
+ try:
+ return repo.commit(message, 'shelve@localhost',
+ opts.get('date'), match)
+ finally:
+ if hasmq:
+ repo.mq.checkapplied = saved
+
+ tempopts = {}
+ tempopts['message'] = "pending changes temporary commit"
+ tempopts['addremove'] = True
+ oldquiet = ui.quiet
+ try:
+ ui.quiet = True
+ node = cmdutil.commit(ui, repo, commitfunc, None, tempopts)
+ finally:
+ ui.quiet = oldquiet
+ tmpwctx = repo[node]
+
try:
fp = shelvedfile(repo, basename, 'hg').opener()
gen = changegroup.readbundle(fp, fp.name)
repo.addchangegroup(gen, 'unshelve', 'bundle:' + fp.name)
nodes = [ctx.node() for ctx in repo.set('%d:', oldtiprev)]
phases.retractboundary(repo, phases.secret, nodes)
- tr.close()
finally:
fp.close()
- tip = repo['tip']
- wctx = repo['.']
- ancestor = tip.ancestor(wctx)
-
- wlock = repo.wlock()
+ shelvectx = repo['tip']
- if ancestor.node() != wctx.node():
- conflicts = hg.merge(repo, tip.node(), force=True, remind=False)
- ms = merge.mergestate(repo)
- stripnodes = [repo.changelog.node(rev)
- for rev in xrange(oldtiprev, len(repo))]
- if conflicts:
- shelvedstate.save(repo, basename, stripnodes)
- # Fix up the dirstate entries of files from the second
- # parent as if we were not merging, except for those
- # with unresolved conflicts.
- parents = repo.parents()
- revertfiles = set(parents[1].files()).difference(ms)
- cmdutil.revert(ui, repo, parents[1],
- (parents[0].node(), nullid),
- *pathtofiles(repo, revertfiles),
- **{'no_backup': True})
+ # If the shelve is not immediately on top of the commit
+ # we'll be merging with, rebase it to be on top.
+ if tmpwctx.node() != shelvectx.parents()[0].node():
+ try:
+ rebase.rebase(ui, repo, **{
+ 'rev' : [shelvectx.rev()],
+ 'dest' : str(tmpwctx.rev()),
+ 'keep' : True,
+ })
+ except error.InterventionRequired:
+ tr.close()
+
+ stripnodes = [repo.changelog.node(rev)
+ for rev in xrange(oldtiprev, len(repo))]
+ shelvedstate.save(repo, basename, wctx, tmpwctx, stripnodes, u)
+
+ util.rename(repo.join('rebasestate'),
+ repo.join('unshelverebasestate'))
raise error.InterventionRequired(
_("unresolved conflicts (see 'hg resolve', then "
"'hg unshelve --continue')"))
- finishmerge(ui, repo, ms, stripnodes, basename, opts)
- else:
- parent = tip.parents()[0]
- hg.update(repo, parent.node())
- cmdutil.revert(ui, repo, tip, repo.dirstate.parents(),
- *pathtofiles(repo, tip.files()),
- **{'no_backup': True})
+
+ # refresh ctx after rebase completes
+ shelvectx = repo['tip']
+
+ if not shelvectx in tmpwctx.children():
+ # rebase was a no-op, so it produced no child commit
+ shelvectx = tmpwctx
- prevquiet = ui.quiet
- ui.quiet = True
- try:
- repo.rollback(force=True)
- finally:
- ui.quiet = prevquiet
+ mergefiles(ui, repo, wctx, shelvectx, u)
+ shelvedstate.clear(repo)
+
+ # The transaction aborting will strip all the commits for us,
+ # but it doesn't update the inmemory structures, so addchangegroup
+ # hooks still fire and try to operate on the missing commits.
+ # Clean up manually to prevent this.
+ repo.changelog.strip(oldtiprev, tr)
unshelvecleanup(ui, repo, basename, opts)
finally:
--- a/tests/test-shelve.t Fri Oct 25 01:14:18 2013 +0900
+++ b/tests/test-shelve.t Wed Oct 23 13:12:48 2013 -0700
@@ -27,7 +27,6 @@
adding manifests
adding file changes
added 1 changesets with 5 changes to 5 files
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg commit -q -m 'initial commit'
@@ -100,19 +99,19 @@
$ hg shelve -d default
$ hg qfinish -a -q
-local edits should prevent a shelved change from applying
+local edits should not prevent a shelved change from applying
- $ echo e>>a/a
- $ hg unshelve
+ $ printf "z\na\n" > a/a
+ $ hg unshelve --keep
unshelving change 'default-01'
- the following shelved files have been modified:
- a/a
- you must commit, revert, or shelve your changes before you can proceed
- abort: cannot unshelve due to local changes
-
- [255]
+ adding changesets
+ adding manifests
+ adding file changes
+ added 1 changesets with 3 changes to 8 files (+1 heads)
+ merging a/a
- $ hg revert -C a/a
+ $ hg revert --all -q
+ $ rm a/a.orig b.rename/b c.copy
apply it and make sure our state is as expected
@@ -122,7 +121,6 @@
adding manifests
adding file changes
added 1 changesets with 3 changes to 8 files
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg status -C
M a/a
A b.rename/b
@@ -201,24 +199,21 @@
merging a/a
warning: conflicts during merge.
merging a/a incomplete! (edit conflicts, then use 'hg resolve --mark')
- 2 files updated, 0 files merged, 1 files removed, 1 files unresolved
- use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
[1]
ensure that we have a merge with unresolved conflicts
- $ hg heads -q
- 4:cebf2b8de087
- 3:2e69b451d1ea
- $ hg parents -q
- 3:2e69b451d1ea
- 4:cebf2b8de087
+ $ hg heads -q --template '{rev}\n'
+ 5
+ 4
+ $ hg parents -q --template '{rev}\n'
+ 4
+ 5
$ hg status
M a/a
M b.rename/b
M c.copy
- A foo/foo
R b/b
? a/a.orig
$ hg diff
@@ -248,12 +243,6 @@
+++ b/c.copy
@@ -0,0 +1,1 @@
+c
- diff --git a/foo/foo b/foo/foo
- new file mode 100644
- --- /dev/null
- +++ b/foo/foo
- @@ -0,0 +1,1 @@
- +foo
$ hg resolve -l
U a/a
@@ -268,10 +257,10 @@
M a/a
M b.rename/b
M c.copy
- A foo/foo
R b/b
? a/a.orig
$ hg unshelve -a
+ rebase aborted
unshelve of 'default' aborted
$ hg heads -q
3:2e69b451d1ea
@@ -330,9 +319,9 @@
3:2e69b451d1ea
$ hg status -C
- M b.rename/b
+ A b.rename/b
b/b
- M c.copy
+ A c.copy
c
A foo/foo
R b/b
@@ -372,6 +361,7 @@
set up another conflict between a commit and a shelved change
$ hg revert -q -C -a
+ $ rm a/a.orig b.rename/b c.copy
$ echo a >> a/a
$ hg shelve -q
$ echo x >> a/a
@@ -387,7 +377,6 @@
adding file changes
added 1 changesets with 1 changes to 6 files (+1 heads)
merging a/a
- 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
$ hg parents -q
4:33f7f61e6c5e
$ hg shelve -l
@@ -411,7 +400,6 @@
adding manifests
adding file changes
added 1 changesets with 1 changes to 7 files
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg shelve --list
default (*) create conflict (glob)
$ hg shelve --cleanup
@@ -433,7 +421,6 @@
adding manifests
adding file changes
added 1 changesets with 1 changes to 7 files
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg bookmark
* test 4:33f7f61e6c5e
@@ -450,7 +437,6 @@
adding manifests
adding file changes
added 1 changesets with 1 changes to 7 files
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
shelve should leave dirstate clean (issue 4055)
@@ -479,8 +465,52 @@
adding manifests
adding file changes
added 2 changesets with 2 changes to 2 files (+1 heads)
- 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg status
M z
$ cd ..
+
+shelve should only unshelve pending changes (issue 4068)
+
+ $ hg init onlypendingchanges
+ $ cd onlypendingchanges
+ $ touch a
+ $ hg ci -Aqm a
+ $ touch b
+ $ hg ci -Aqm b
+ $ hg up -q 0
+ $ touch c
+ $ hg ci -Aqm c
+
+ $ touch d
+ $ hg add d
+ $ hg shelve
+ shelved as default
+ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+ $ hg up -q 1
+ $ hg unshelve
+ unshelving change 'default'
+ adding changesets
+ adding manifests
+ adding file changes
+ added 1 changesets with 1 changes to 3 files
+ $ hg status
+ A d
+
+unshelve should work on an ancestor of the original commit
+
+ $ hg shelve
+ shelved as default
+ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+ $ hg up 0
+ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+ $ hg unshelve
+ unshelving change 'default'
+ adding changesets
+ adding manifests
+ adding file changes
+ added 1 changesets with 1 changes to 3 files
+ $ hg status
+ A d
+
+ $ cd ..