[PATCH 4 of 4] shelve: add a shelve extension to save/restore working changes
Augie Fackler
raf at durin42.com
Tue May 28 19:20:05 CDT 2013
On May 28, 2013, at 7:28 PM, Bryan O'Sullivan <bos at serpentine.com> wrote:
> # HG changeset patch
> # User Bryan O'Sullivan <bryano at fb.com>
> # Date 1369783721 25200
> # Tue May 28 16:28:41 2013 -0700
> # Node ID a87ee920b333192898e8ff84e30ddb1ebac515b6
> # Parent 11fce4dc68f060e96cc06cc88da72e2c9da1022b
> shelve: add a shelve extension to save/restore working changes
One code style question.
I wonder if we shouldn't take a step back and have some central registry of things that could be in-progress, since an in-progress histedit, merge, rebase, or graft should abort this, and not all of those will necessarily look like a merge (and the abort message could be confusing).
>
> This extension saves shelved changes using a temporary draft commit,
> and bundles all draft ancestors of the temporary commit. This
> strategy makes it possible to use Mercurial's merge machinery to
> resolve conflicts if necessary when unshelving, even when the
> destination commit or its ancestors have been amended, squashed,
> or evolved.
>
> Although this extension shares its name and some functionality with
> the third party hgshelve extension, it has little else in common.
> Notably, the hgshelve extension shelves changes as unified diffs,
> which makes conflict resolution a matter of finding .rej files and
> cleaning up the mess by hand.
>
> We do not yet allow hunk-level choosing of changes to record.
> Compared to the hgshelve extension, this is a small regression in
> usability, but we hope to integrate that at a later point, once the
> record machinery becomes more reusable and robust.
>
> diff --git a/hgext/color.py b/hgext/color.py
> --- a/hgext/color.py
> +++ b/hgext/color.py
> @@ -63,6 +63,10 @@ Default effects may be overridden from y
> rebase.rebased = blue
> rebase.remaining = red bold
>
> + shelve.age = cyan
> + shelve.newest = green bold
> + shelve.name = blue bold
> +
> histedit.remaining = red bold
>
> The available effects in terminfo mode are 'blink', 'bold', 'dim',
> @@ -260,6 +264,9 @@ except ImportError:
> 'rebase.remaining': 'red bold',
> 'resolve.resolved': 'green bold',
> 'resolve.unresolved': 'red bold',
> + 'shelve.age': 'cyan',
> + 'shelve.newest': 'green bold',
> + 'shelve.name': 'blue bold',
> 'status.added': 'green bold',
> 'status.clean': 'none',
> 'status.copied': 'none',
> diff --git a/hgext/shelve.py b/hgext/shelve.py
> new file mode 100644
> --- /dev/null
> +++ b/hgext/shelve.py
> @@ -0,0 +1,552 @@
> +# shelve.py - save/restore working directory state
> +#
> +# Copyright 2013 Facebook, Inc.
> +#
> +# This software may be used and distributed according to the terms of the
> +# GNU General Public License version 2 or any later version.
> +
> +'''save and restore changes to the working directory
> +
> +The "hg shelve" command saves changes made to the working directory
> +and reverts those changes, resetting the working directory to a clean
> +state.
> +
> +Later on, the "hg unshelve" command restores the changes saved by "hg
> +shelve". Changes can be restored even after updating to a different
> +parent, in which case Mercurial's merge machinery will resolve any
> +conflicts if necessary.
> +
> +You can have more than one shelved change outstanding at a time; each
> +shelved change has a distinct name. For details, see the help for "hg
> +shelve".
> +'''
> +
> +from mercurial.i18n import _
> +from mercurial.node import nullid
> +from mercurial import changegroup, cmdutil, commands, extensions, pvec, scmutil
> +from mercurial import error, hg, mdiff, merge, node, patch, repair, util
> +from mercurial import templatefilters
> +from mercurial import lock as lockmod
> +import errno, os, time
> +
> +cmdtable = {}
> +command = cmdutil.command(cmdtable)
> +testedwith = 'internal'
> +
> +def shelvedfilename(repo, name, filetype):
> + return os.path.join(repo.join('shelved'), name + '.' + filetype)
> +
> +def shelvedfile(repo, name, filetype, mode='rb'):
> + '''Open a file used for storing data associated with a shelved change.'''
> + try:
> + return open(shelvedfilename(repo, name, filetype), mode)
> + except IOError, err:
> + if err.errno == errno.ENOENT:
> + if mode[0] in 'wa':
> + try:
> + repo.vfs.mkdir(repo.join('shelved'))
> + return open(shelvedfilename(repo, name, filetype), mode)
> + except IOError, err:
> + if err.errno != errno.EEXIST:
> + raise
> + elif mode[0] =='r':
> + raise util.Abort(_("shelved change '%s' not found") % name)
> + raise
> +
> +class shelvedstate(object):
> + @classmethod
> + def load(cls, repo):
> + fp = repo.opener('shelvedstate')
> + try:
> + lines = fp.read().splitlines()
> + finally:
> + fp.close()
> + lines.reverse()
> +
> + version = lines.pop()
> + obj = cls()
> + obj.name = lines.pop()
> + obj.parents = [node.bin(n) for n in lines.pop().split()]
> + obj.stripnodes = [node.bin(n) for n in lines]
> + return obj
> +
> + @staticmethod
> + def save(repo, name, oldtiprev):
> + fp = repo.opener('shelvedstate', 'wb')
> + fp.write('1\n')
> + fp.write(name + '\n')
> + fp.write(' '.join(node.hex(n) for n in repo.dirstate.parents()) + '\n')
> + # save revs that need to be stripped when we are done
> + for rev in xrange(oldtiprev, len(repo)):
> + fp.write(node.hex(repo.changelog.node(rev)) + '\n')
> + fp.close()
> +
> + @staticmethod
> + def clear(repo):
> + util.unlinkpath(repo.join('shelvedstate'), ignoremissing=True)
> +
> +def createcmd(ui, repo, pats, opts):
> + def publicancestors(ctx):
why is this function nested?
> + '''Compute the heads of the public ancestors of a commit.
> +
> + Much faster than the revset heads(ancestors(ctx) - draft())'''
> + seen = set()
> + visit = util.deque()
> + visit.append(ctx)
> + while visit:
> + ctx = visit.popleft()
> + for parent in ctx.parents():
> + rev = parent.rev()
> + if rev not in seen:
> + seen.add(rev)
> + if parent.mutable():
> + visit.append(parent)
> + else:
> + yield parent.node()
> +
> + try:
> + shelvedstate.load(repo)
> + raise util.Abort(_('unshelve already in progress'))
> + except IOError, err:
> + if err.errno != errno.ENOENT:
> + raise
> +
> + wctx = repo[None]
> + parents = wctx.parents()
> + if len(parents) > 1:
> + raise util.Abort(_('cannot shelve while merging'))
> + parent = parents[0]
> + if parent.node() == nullid:
> + raise util.Abort(_('cannot shelve - repo has no history'))
> +
> + try:
> + user = repo.ui.username()
> + except util.Abort:
> + user = 'shelve at localhost'
> +
> + label = repo._bookmarkcurrent or parent.branch()
> +
> + def gennames():
> + yield label
> + for i in xrange(1, 100):
> + yield '%s-%02d' % (label, i)
> +
> + shelvedfiles = []
> +
> + def commitfunc(ui, repo, message, match, opts):
> + for flist in repo.status(match=match)[:4]:
> + shelvedfiles.extend(flist)
> + return repo.commit(message, user, opts.get('date'), match)
> +
> + desc = parent.description().split('\n', 1)[0]
> + desc = _('shelved from %s (%s): %s') % (label, str(parent)[:8], desc)
> +
> + if not opts['message']:
> + opts['message'] = desc
> +
> + name = opts['name']
> + if '/' in name or '\\' in name:
> + raise util.Abort(_('shelved change names may not contain slashes'))
> + if name.startswith('.'):
> + raise util.Abort(_("shelved change names may not start with '.'"))
> + if name:
> + if os.path.exists(shelvedfilename(repo, name, 'hg')):
> + raise util.Abort(_("a shelved change named '%s' already exists")
> + % name)
> +
> + wlock = lock = None
> + try:
> + wlock = repo.wlock()
> + lock = repo.lock()
> +
> + if not name:
> + for name in gennames():
> + if not os.path.exists(shelvedfilename(repo, name, 'hg')):
> + break
> + else:
> + raise util.Abort(_("too many shelved changes named '%s'") %
> + label)
> +
> + node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
> +
> + if not node:
> + stat = repo.status(match=scmutil.match(repo[None], pats, opts))
> + if stat[3]:
> + ui.status(_("nothing changed (%d missing files, see "
> + "'hg status')\n") % len(stat[3]))
> + else:
> + ui.status(_("nothing changed\n"))
> + return 1
> +
> + shelvedfile(repo, name, 'files', 'wb').write('\0'.join(shelvedfiles))
> +
> + bases = list(publicancestors(repo[node]))
> + cg = repo.changegroupsubset(bases, [node], 'shelve')
> + changegroup.writebundle(cg, shelvedfilename(repo, name, 'hg'),
> + 'HG10UN')
> + cmdutil.export(repo, [node],
> + fp=shelvedfile(repo, name, 'patch', 'wb'),
> + opts=mdiff.diffopts(git=True))
> +
> + if ui.formatted():
> + desc = util.ellipsis(desc, ui.termwidth())
> + ui.status(desc + '\n')
> + ui.status(_('shelved as %s\n') % name)
> + hg.update(repo, parent.node())
> + repair.strip(ui, repo, [node], backup='none', topic='shelve')
> + finally:
> + lockmod.release(lock, wlock)
> +
> +def cleanupcmd(ui, repo):
> + path = repo.join('shelved')
> + wlock = None
> + try:
> + wlock = repo.wlock()
> + for name in os.listdir(path):
> + suffix = name.rsplit('.', 1)[-1]
> + if suffix in ('hg', 'files', 'patch'):
> + os.unlink(os.path.join(path, name))
> + finally:
> + lockmod.release(wlock)
> +
> +def deletecmd(ui, repo, pats):
> + if not pats:
> + raise util.Abort(_('no shelved changes specified!'))
> + wlock = None
> + try:
> + wlock = repo.wlock()
> + try:
> + for name in pats:
> + for suffix in 'hg files patch'.split():
> + os.unlink(shelvedfilename(repo, name, suffix))
> + except OSError, err:
> + if err.errno != errno.ENOENT:
> + raise
> + raise util.Abort(_("shelved change '%s' not found") % name)
> + finally:
> + lockmod.release(wlock)
> +
> +def listshelves(repo):
> + path = repo.join('shelved')
> + try:
> + names = os.listdir(path)
> + except OSError, err:
> + if err.errno != errno.ENOENT:
> + raise
> + return []
> + info = []
> + for name in names:
> + pfx, sfx = name.rsplit('.', 1)
> + if not pfx or sfx != 'patch':
> + continue
> + st = os.lstat(os.path.join(path, name))
> + info.append((st.st_mtime, os.path.join(path, pfx)))
> + return sorted(info, reverse=True)
> +
> +def listcmd(ui, repo, pats, opts):
> + pats = set(pats)
> + width = 80
> + if not ui.plain():
> + width = ui.termwidth()
> + namelabel = 'shelve.newest'
> + for mtime, name in listshelves(repo):
> + sname = os.path.basename(name)
> + if pats and sname not in pats:
> + continue
> + ui.write(sname, label=namelabel)
> + namelabel = 'shelve.name'
> + if ui.quiet:
> + ui.write('\n')
> + continue
> + ui.write(' ' * (16 - len(sname)))
> + used = 16
> + age = '[%s]' % templatefilters.age(util.makedate(mtime))
> + ui.write(age, label='shelve.age')
> + ui.write(' ' * (18 - len(age)))
> + used += 18
> + fp = open(name + '.patch', 'rb')
> + try:
> + while True:
> + line = fp.readline()
> + if not line:
> + break
> + if not line.startswith('#'):
> + desc = line.rstrip()
> + if ui.formatted():
> + desc = util.ellipsis(desc, width - used)
> + ui.write(desc)
> + break
> + ui.write('\n')
> + if not (opts['patch'] or opts['stat']):
> + continue
> + difflines = fp.readlines()
> + if opts['patch']:
> + for chunk, label in patch.difflabel(iter, difflines):
> + ui.write(chunk, label=label)
> + if opts['stat']:
> + for chunk, label in patch.diffstatui(difflines, width=width,
> + git=True):
> + ui.write(chunk, label=label)
> + finally:
> + fp.close()
> +
> +def readshelvedfiles(repo, basename):
> + return shelvedfile(repo, basename, 'files').read().split('\0')
> +
> +def checkparents(repo, state):
> + if state.parents != repo.dirstate.parents():
> + raise util.Abort(_('working directory parents do not match unshelve '
> + 'state'))
> +
> +def unshelveabort(ui, repo, state, opts):
> + wlock = repo.wlock()
> + lock = None
> + try:
> + checkparents(repo, state)
> + 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],
> + *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
> + 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 unshelvecleanup(ui, repo, name, opts):
> + if not opts['keep']:
> + for filetype in 'hg files patch'.split():
> + os.unlink(shelvedfilename(repo, name, filetype))
> +
> +def unshelvecontinue(ui, repo, state, opts):
> + # We're finishing off a merge. First parent is our original
> + # parent, second is the fake commit we're unshelving.
> + wlock = repo.wlock()
> + lock = None
> + try:
> + checkparents(repo, state)
> + ms = merge.mergestate(repo)
> + if [f for f in ms if ms[f] == 'u']:
> + raise util.Abort(
> + _("unresolved conflicts, can't continue"),
> + hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
> + dirstate = repo.dirstate
> + for f in ms:
> + if dirstate[f] == 'm':
> + dirstate.normallookup(f)
> + dirstate._pl = (dirstate._pl[0], nullid)
> + dirstate._dirty = dirstate._dirtypl = True
> + lock = repo.lock()
> + 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:
> + lockmod.release(lock, wlock)
> +
> + at command('unshelve',
> + [('a', 'abort', None,
> + _('abort an incomplete unshelve operation')),
> + ('c', 'continue', None,
> + _('continue an incomplete unshelve operation')),
> + ('', 'keep', None,
> + _('save shelved change'))],
> + _('hg unshelve [SHELVED]'))
> +def unshelve(ui, repo, *shelved, **opts):
> + '''restore a shelved change to the working directory
> +
> + This command accepts an optional name of a shelved change to
> + restore. If none is given, the most recent shelved change is used.
> +
> + If a shelved change is applied successfully, the bundle that
> + contains the shelved changes is deleted afterwards.
> +
> + Since you can restore a shelved change on top of an arbitrary
> + commit, it is possible that unshelving will result in a conflict
> + between your changes and the commits you are unshelving onto. If
> + this occurs, you must resolve the conflict, then use
> + ``--continue`` to complete the unshelve operation. (The bundle
> + will not be deleted until you successfully complete the unshelve.)
> +
> + (Alternatively, you can use ``--abort`` to abandon an unshelve
> + that causes a conflict. This reverts the unshelved changes, and
> + does not delete the bundle.)
> + '''
> + abortf = opts['abort']
> + continuef = opts['continue']
> + if abortf or continuef:
> + if abortf and continuef:
> + raise util.Abort(_('cannot use both abort and continue'))
> + if shelved:
> + raise util.Abort(_('cannot combine abort/continue with '
> + 'naming a shelved change'))
> + try:
> + state = shelvedstate.load(repo)
> + except IOError, err:
> + if err.errno != errno.ENOENT:
> + raise
> + raise util.Abort(_('no unshelve operation underway'))
> +
> + if abortf:
> + return unshelveabort(ui, repo, state, opts)
> + elif continuef:
> + return unshelvecontinue(ui, repo, state, opts)
> + elif len(shelved) > 1:
> + raise util.Abort(_('can only unshelve one change at a time'))
> + elif not shelved:
> + shelved = listshelves(repo)
> + if not shelved:
> + raise util.Abort(_('no shelved changes to apply!'))
> + basename = os.path.basename(shelved[0][1])
> + ui.status(_("unshelving change '%s'\n") % basename)
> + else:
> + basename = shelved[0]
> +
> + 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 = None
> + try:
> + lock = repo.lock()
> +
> + oldtiprev = len(repo)
> + try:
> + fp = shelvedfile(repo, basename, 'hg')
> + gen = changegroup.readbundle(fp, fp.name)
> + modheads = repo.addchangegroup(gen, 'unshelve', 'bundle:' + fp.name)
> + finally:
> + fp.close()
> +
> + tip = repo['tip']
> + wctx = repo['.']
> + ancestor = tip.ancestor(wctx)
> +
> + wlock = repo.wlock()
> +
> + if ancestor.node() != wctx.node():
> + conflicts = hg.merge(repo, tip.node(), force=True)
> + if conflicts:
> + cl = repo.changelog
> + shelvedstate.save(repo, basename, oldtiprev)
> + # Fix up the dirstate entries of files from the second
> + # parent as if we were not merging, except for those
> + # with unresolved conflicts.
> + ms = merge.mergestate(repo)
> + parents = repo.parents()
> + revertfiles = set(parents[1].files()).difference(ms)
> + cmdutil.revert(ui, repo, parents[1],
> + (parents[0].node(), nullid),
> + *revertfiles, no_backup=True)
> + raise error.InterventionRequired(
> + _("unresolved conflicts (see 'hg resolve', then "
> + "'hg unshelve --continue')"))
> + else:
> + parent = tip.parents()[0]
> + hg.update(repo, parent.node())
> + cmdutil.revert(ui, repo, tip, repo.dirstate.parents(), *tip.files(),
> + no_backup=True)
> + #repair.strip(ui, repo, [tip.node()], backup='none', topic='shelve')
> +
> + try:
> + prevquiet = ui.quiet
> + ui.quiet = True
> + repo.rollback(force=True)
> + finally:
> + ui.quiet = prevquiet
> +
> + unshelvecleanup(ui, repo, basename, opts)
> + finally:
> + lockmod.release(lock, wlock)
> +
> + at command('shelve',
> + [('A', 'addremove', None,
> + _('mark new/missing files as added/removed before shelving')),
> + ('', 'cleanup', None,
> + _('delete all shelved changes')),
> + ('', 'date', '',
> + _('shelve with the specified commit date'), _('DATE')),
> + ('d', 'delete', None,
> + _('delete the named shelved change(s)')),
> + ('l', 'list', None,
> + _('list current shelves')),
> + ('m', 'message', '',
> + _('use text as shelve message'), _('TEXT')),
> + ('n', 'name', '',
> + _('use the given name for the shelved commit'), _('NAME')),
> + ('p', 'patch', None,
> + _('show patch')),
> + ('', 'stat', None,
> + _('output diffstat-style summary of changes'))],
> + _('hg shelve'))
> +def shelvecmd(ui, repo, *pats, **opts):
> + '''save and set aside changes from the working directory
> +
> + Shelving takes files that "hg status" reports as not clean, saves
> + the modifications to a bundle (a shelved change), and reverts the
> + files so that their state in the working directory becomes clean.
> +
> + To restore these changes to the working directory, using "hg
> + unshelve"; this will work even if you switch to a different
> + commit.
> +
> + When no files are specified, "hg shelve" saves all not-clean
> + files. If specific files or directories are named, only changes to
> + those files are shelved.
> +
> + Each shelved change has a name that makes it easier to find later.
> + The name of a shelved change defaults to being based on the active
> + bookmark, or if there is no active bookmark, the current named
> + branch. To specify a different name, use ``--name``.
> +
> + To see a list of existing shelved changes, use the ``--list``
> + option. For each shelved change, this will print its name, age,
> + and description; use ``--patch`` or ``--stat`` for more details.
> +
> + To delete specific shelved changes, use ``--delete``. To delete
> + all shelved changes, use ``--cleanup``.
> + '''
> + def checkopt(opt, incompatible):
> + if opts[opt]:
> + for i in incompatible.split():
> + if opts[i]:
> + raise util.Abort(_("options '--%s' and '--%s' may not be "
> + "used together") % (opt, i))
> + return True
> + if checkopt('cleanup', 'addremove delete list message name patch stat'):
> + if pats:
> + raise util.Abort(_("cannot specify names when using '--cleanup'"))
> + return cleanupcmd(ui, repo)
> + elif checkopt('delete', 'addremove cleanup list message name patch stat'):
> + return deletecmd(ui, repo, pats)
> + elif checkopt('list', 'addremove cleanup delete message name'):
> + return listcmd(ui, repo, pats, opts)
> + else:
> + for i in ('patch', 'stat'):
> + if opts[i]:
> + raise util.Abort(_("option '--%s' may not be "
> + "used when shelving a change") % (i,))
> + return createcmd(ui, repo, pats, opts)
> diff --git a/tests/run-tests.py b/tests/run-tests.py
> --- a/tests/run-tests.py
> +++ b/tests/run-tests.py
> @@ -889,6 +889,7 @@ def runone(options, test):
> hgrc.write('[defaults]\n')
> hgrc.write('backout = -d "0 0"\n')
> hgrc.write('commit = -d "0 0"\n')
> + hgrc.write('shelve = --date "0 0"\n')
> hgrc.write('tag = -d "0 0"\n')
> if options.inotify:
> hgrc.write('[extensions]\n')
> diff --git a/tests/test-shelve.t b/tests/test-shelve.t
> new file mode 100644
> --- /dev/null
> +++ b/tests/test-shelve.t
> @@ -0,0 +1,354 @@
> + $ echo "[extensions]" >> $HGRCPATH
> + $ echo "shelve=" >> $HGRCPATH
> + $ echo "[defaults]" >> $HGRCPATH
> + $ echo "diff = --nodates --git" >> $HGRCPATH
> +
> + $ hg init repo
> + $ cd repo
> + $ mkdir a b
> + $ echo a > a/a
> + $ echo b > b/b
> + $ echo c > c
> + $ echo d > d
> + $ echo x > x
> + $ hg addremove -q
> +
> +shelving in an empty repo should bail
> +
> + $ hg shelve
> + abort: cannot shelve - repo has no history
> + [255]
> +
> + $ hg commit -q -m 'initial commit'
> +
> + $ hg shelve
> + nothing changed
> + [1]
> +
> +shelve a change that we will delete later
> +
> + $ echo a >> a/a
> + $ hg shelve
> + shelved from default (cc01e2b0): initial commit
> + shelved as default
> + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
> +
> +set up some more complex changes to shelve
> +
> + $ echo a >> a/a
> + $ hg mv b b.rename
> + moving b/b to b.rename/b
> + $ hg cp c c.copy
> + $ hg status -C
> + M a/a
> + A b.rename/b
> + b/b
> + A c.copy
> + c
> + R b/b
> +
> +prevent some foot-shooting
> +
> + $ hg shelve -n foo/bar
> + abort: shelved change names may not contain slashes
> + [255]
> + $ hg shelve -n .baz
> + abort: shelved change names may not start with '.'
> + [255]
> +
> +the common case - no options or filenames
> +
> + $ hg shelve
> + shelved from default (cc01e2b0): initial commit
> + shelved as default-01
> + 2 files updated, 0 files merged, 2 files removed, 0 files unresolved
> + $ hg status -C
> +
> +ensure that our shelved changes exist
> +
> + $ hg shelve -l
> + default-01 [*] shelved from default (cc01e2b0): initial commit (glob)
> + default [*] shelved from default (cc01e2b0): initial commit (glob)
> +
> + $ hg shelve -l -p default
> + default [*] shelved from default (cc01e2b0): initial commit (glob)
> +
> + diff --git a/a/a b/a/a
> + --- a/a/a
> + +++ b/a/a
> + @@ -1,1 +1,2 @@
> + a
> + +a
> +
> +delete our older shelved change
> +
> + $ hg shelve -d default
> +
> +local edits should prevent a shelved change from applying
> +
> + $ echo e>>a/a
> + $ hg unshelve
> + 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]
> +
> + $ hg revert -C a/a
> +
> +apply it and make sure our state is as expected
> +
> + $ hg unshelve
> + unshelving change 'default-01'
> + adding changesets
> + adding manifests
> + adding file changes
> + added 1 changesets with 3 changes to 7 files
> + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
> + $ hg status -C
> + M a/a
> + A b.rename/b
> + b/b
> + A c.copy
> + c
> + R b/b
> + $ hg shelve -l
> +
> + $ hg unshelve
> + abort: no shelved changes to apply!
> + [255]
> + $ hg unshelve foo
> + abort: shelved change 'foo' not found
> + [255]
> +
> +named shelves, specific filenames, and "commit messages" should all work
> +
> + $ hg status -C
> + M a/a
> + A b.rename/b
> + b/b
> + A c.copy
> + c
> + R b/b
> + $ hg shelve -q -n wibble -m wat a
> +
> +expect "a" to no longer be present, but status otherwise unchanged
> +
> + $ hg status -C
> + A b.rename/b
> + b/b
> + A c.copy
> + c
> + R b/b
> + $ hg shelve -l --stat
> + wibble [*] wat (glob)
> + a/a | 1 +
> + 1 files changed, 1 insertions(+), 0 deletions(-)
> +
> +and now "a/a" should reappear
> +
> + $ hg unshelve -q wibble
> + $ hg status -C
> + M a/a
> + A b.rename/b
> + b/b
> + A c.copy
> + c
> + R b/b
> +
> +cause unshelving to result in a merge with 'a' conflicting
> +
> + $ hg shelve -q
> + $ echo c>>a/a
> + $ hg commit -m second
> + $ hg tip --template '{files}\n'
> + a/a
> +
> +add an unrelated change that should be preserved
> +
> + $ mkdir foo
> + $ echo foo > foo/foo
> + $ hg add foo/foo
> +
> +force a conflicted merge to occur
> +
> + $ hg unshelve
> + unshelving change 'default'
> + adding changesets
> + adding manifests
> + adding file changes
> + added 1 changesets with 3 changes to 7 files (+1 heads)
> + 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
> + 2:99fa200422e2
> + 1:71743bbd8fc8
> + $ hg parents -q
> + 1:71743bbd8fc8
> + 2:99fa200422e2
> + $ hg status
> + M a/a
> + M b.rename/b
> + M c.copy
> + A foo/foo
> + R b/b
> + ? a/a.orig
> + $ hg diff
> + diff --git a/a/a b/a/a
> + --- a/a/a
> + +++ b/a/a
> + @@ -1,2 +1,6 @@
> + a
> + +<<<<<<< local
> + c
> + +=======
> + +a
> + +>>>>>>> other
> + diff --git a/b.rename/b b/b.rename/b
> + --- /dev/null
> + +++ b/b.rename/b
> + @@ -0,0 +1,1 @@
> + +b
> + diff --git a/b/b b/b/b
> + deleted file mode 100644
> + --- a/b/b
> + +++ /dev/null
> + @@ -1,1 +0,0 @@
> + -b
> + diff --git a/c.copy b/c.copy
> + --- /dev/null
> + +++ 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
> +
> + $ hg shelve
> + abort: unshelve already in progress
> + [255]
> +
> +abort the unshelve and be happy
> +
> + $ hg status
> + M a/a
> + M b.rename/b
> + M c.copy
> + A foo/foo
> + R b/b
> + ? a/a.orig
> + $ hg unshelve -a
> + unshelve of 'default' aborted
> + $ hg heads -q
> + 1:71743bbd8fc8
> + $ hg parents
> + changeset: 1:71743bbd8fc8
> + tag: tip
> + user: test
> + date: Thu Jan 01 00:00:00 1970 +0000
> + summary: second
> +
> + $ hg resolve -l
> + $ hg status
> + A foo/foo
> + ? a/a.orig
> +
> +try to continue with no unshelve underway
> +
> + $ hg unshelve -c
> + abort: no unshelve operation underway
> + [255]
> + $ hg status
> + A foo/foo
> + ? a/a.orig
> +
> +redo the unshelve to get a conflict
> +
> + $ hg unshelve -q
> + warning: conflicts during merge.
> + merging a/a incomplete! (edit conflicts, then use 'hg resolve --mark')
> + unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
> + [1]
> +
> +attempt to continue
> +
> + $ hg unshelve -c
> + abort: unresolved conflicts, can't continue
> + (see 'hg resolve', then 'hg unshelve --continue')
> + [255]
> +
> + $ hg revert -r . a/a
> + $ hg resolve -m a/a
> +
> + $ hg unshelve -c
> + unshelve of 'default' complete
> +
> +ensure the repo is as we hope
> +
> + $ hg parents
> + changeset: 1:71743bbd8fc8
> + tag: tip
> + user: test
> + date: Thu Jan 01 00:00:00 1970 +0000
> + summary: second
> +
> + $ hg heads -q
> + 1:71743bbd8fc8
> +
> + $ hg status -C
> + M a/a
> + M b.rename/b
> + b/b
> + M c.copy
> + c
> + A foo/foo
> + R b/b
> + ? a/a.orig
> +
> +there should be no shelves left
> +
> + $ hg shelve -l
> +
> + $ hg commit -m whee a/a
> +
> +#if execbit
> +
> +ensure that metadata-only changes are shelved
> +
> + $ chmod +x a/a
> + $ hg shelve -q -n execbit a/a
> + $ hg status a/a
> + $ hg unshelve -q execbit
> + $ hg status a/a
> + M a/a
> + $ hg revert a/a
> +
> +#endif
> +
> +#if symlink
> +
> + $ rm a/a
> + $ ln -s foo a/a
> + $ hg shelve -q -n symlink a/a
> + $ hg status a/a
> + $ hg unshelve -q symlink
> + $ hg status a/a
> + M a/a
> + $ hg revert a/a
> +
> +#endif
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel at selenic.com
> http://selenic.com/mailman/listinfo/mercurial-devel
More information about the Mercurial-devel
mailing list