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', @@ -259,6 +263,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,607 @@ +# 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". +""" + +try: + import cPickle as pickle + pickle.dump # import now +except ImportError: + import pickle +from mercurial.i18n import _ +from mercurial.node import nullid +from mercurial import changegroup, cmdutil, scmutil, phases +from mercurial import error, hg, mdiff, merge, patch, repair, util +from mercurial import templatefilters +from mercurial import lock as lockmod +import errno + +cmdtable = {} +command = cmdutil.command(cmdtable) +testedwith = 'internal' + +class shelvedfile(object): + """Handles common functions on shelve files (.hg/.files/.patch) using + the vfs layer""" + def __init__(self, repo, name, filetype=None): + self.repo = repo + self.name = name + self.vfs = scmutil.vfs(repo.join('shelved')) + if filetype: + self.fname = name + '.' + filetype + else: + self.fname = name + + def exists(self): + return self.vfs.exists(self.fname) + + def filename(self): + return self.vfs.join(self.fname) + + def unlink(self): + util.unlink(self.filename()) + + def stat(self): + return self.vfs.stat(self.fname) + + def opener(self, mode='rb'): + try: + return self.vfs(self.fname, mode) + except IOError, err: + if err.errno != errno.ENOENT: + raise + if mode[0] in 'wa': + try: + self.vfs.mkdir() + return self.vfs(self.fname, mode) + except IOError, err: + if err.errno != errno.EEXIST: + raise + elif mode[0] == 'r': + raise util.Abort(_("shelved change '%s' not found") % + self.name) + +class shelvedstate(object): + """Handles saving and restoring a shelved state. Ensures that different + versions of a shelved state are possible and handles them appropriate""" + _version = 1 + _filename = 'shelvedstate' + + @classmethod + def load(cls, repo): + fp = repo.opener(cls._filename) + (version, name, parents, stripnodes) = pickle.load(fp) + + if version != cls._version: + raise util.Abort(_('this version of shelve is incompatible ' + 'with the version used in this repo')) + + obj = cls() + obj.name = name + obj.parents = parents + obj.stripnodes = stripnodes + + return obj + + @classmethod + def save(cls, repo, name, stripnodes): + fp = repo.opener(cls._filename, 'wb') + pickle.dump((cls._version, name, + repo.dirstate.parents(), + stripnodes), fp) + fp.close() + + @staticmethod + def clear(repo): + util.unlinkpath(repo.join('shelvedstate'), ignoremissing=True) + +def createcmd(ui, repo, pats, opts): + def publicancestors(ctx): + """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() + + wctx = repo[None] + parents = wctx.parents() + if len(parents) > 1: + raise util.Abort(_('cannot shelve while merging')) + parent = parents[0] + + # we never need the user, so we use a generic user for all shelve operations + user = 'shelve@localhost' + label = repo._bookmarkcurrent or parent.branch() or 'default' + + # slashes aren't allowed in filenames, therefore we rename it + origlabel, label = label, label.replace('/', '_') + + def gennames(): + yield label + for i in xrange(1, 100): + yield '%s-%02d' % (label, i) + + shelvedfiles = [] + + def commitfunc(ui, repo, message, match, opts): + # check modified, added, removed, deleted only + for flist in repo.status(match=match)[:4]: + shelvedfiles.extend(flist) + return repo.commit(message, user, opts.get('date'), match) + + if parent.node() != nullid: + desc = parent.description().split('\n', 1)[0] + desc = _('shelved from %s (%s): %s') % (label, str(parent)[:8], desc) + else: + desc = '(empty repository)' + + if not opts['message']: + opts['message'] = desc + + name = opts['name'] + + wlock = lock = tr = None + try: + wlock = repo.wlock() + lock = repo.lock() + + # use an uncommited transaction to generate the bundle to avoid + # pull races. ensure we don't print the abort message to stderr. + tr = repo.transaction('commit', report=lambda x: None) + + if name: + if shelvedfile(repo, name, 'hg').exists(): + raise util.Abort(_("a shelved change named '%s' already exists") + % name) + else: + for n in gennames(): + if not shelvedfile(repo, n, 'hg').exists(): + name = n + break + else: + raise util.Abort(_("too many shelved changes named '%s'") % + label) + + # ensure we are not creating a subdirectory or a hidden file + 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 '.'")) + + 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 + + phases.retractboundary(repo, phases.secret, [node]) + + fp = shelvedfile(repo, name, 'files').opener('wb') + fp.write('\0'.join(shelvedfiles)) + + bases = list(publicancestors(repo[node])) + cg = repo.changegroupsubset(bases, [node], 'shelve') + changegroup.writebundle(cg, shelvedfile(repo, name, 'hg').filename(), + 'HG10UN') + cmdutil.export(repo, [node], + fp=shelvedfile(repo, name, 'patch').opener('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()) + finally: + if tr: + tr.abort() + lockmod.release(lock, wlock) + +def cleanupcmd(ui, repo): + wlock = None + try: + wlock = repo.wlock() + for (name, _) in repo.vfs.readdir('shelved'): + suffix = name.rsplit('.', 1)[-1] + if suffix in ('hg', 'files', 'patch'): + shelvedfile(repo, name).unlink() + 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(): + shelvedfile(repo, name, suffix).unlink() + 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): + try: + names = repo.vfs.readdir('shelved') + 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 = shelvedfile(repo, name).stat() + info.append((st.st_mtime, shelvedfile(repo, pfx).filename())) + 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 = util.split(name)[1] + 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): + fp = shelvedfile(repo, basename, 'files').opener() + return fp.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(): + 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 + for f in ms: + if dirstate[f] == 'm': + dirstate.normallookup(f) + dirstate._pl = (dirstate._pl[0], nullid) + dirstate._dirty = dirstate._dirtypl = True + shelvedstate.clear(repo) + +def unshelvecontinue(ui, repo, state, opts): + # We're finishing off a merge. First parent is our original + # parent, second is the temporary "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'")) + finishmerge(ui, repo, ms, state.stripnodes, state.name, opts) + lock = repo.lock() + repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve') + unshelvecleanup(ui, repo, state.name, opts) + ui.status(_("unshelve of '%s' complete\n") % state.name) + finally: + lockmod.release(lock, wlock) + +@command('unshelve', + [('a', 'abort', None, + _('abort an incomplete unshelve operation')), + ('c', 'continue', None, + _('continue an incomplete unshelve operation')), + ('', 'keep', None, + _('keep shelve after unshelving'))], + _('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 not abortf and not continuef: + cmdutil.checkunfinished(repo) + + 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 = util.split(shelved[0][1])[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 = tr = None + try: + lock = repo.lock() + + tr = repo.transaction('unshelve', report=lambda x: None) + oldtiprev = len(repo) + 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() + + 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), + *revertfiles, no_backup=True) + 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(), *tip.files(), + no_backup=True) + + prevquiet = ui.quiet + ui.quiet = True + try: + repo.rollback(force=True) + finally: + ui.quiet = prevquiet + + unshelvecleanup(ui, repo, basename, opts) + finally: + if tr: + tr.release() + lockmod.release(lock, wlock) + +@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``. + ''' + cmdutil.checkunfinished(repo) + + 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) + +def extsetup(ui): + cmdutil.unfinishedstates.append( + [shelvedstate._filename, False, True, _('unshelve already in progress'), + _("use 'hg unshelve --continue' or 'hg unshelve --abort'")]) diff --git a/tests/run-tests.py b/tests/run-tests.py --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -341,6 +341,7 @@ def createhgrc(path, options): 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-commandserver.py.out b/tests/test-commandserver.py.out --- a/tests/test-commandserver.py.out +++ b/tests/test-commandserver.py.out @@ -73,6 +73,7 @@ testing localhgrc: bundle.mainreporoot=$TESTTMP defaults.backout=-d "0 0" defaults.commit=-d "0 0" +defaults.shelve=--date "0 0" defaults.tag=-d "0 0" ui.slash=True ui.interactive=False @@ -81,6 +82,7 @@ ui.foo=bar runcommand -R foo showconfig ui defaults defaults.backout=-d "0 0" defaults.commit=-d "0 0" +defaults.shelve=--date "0 0" defaults.tag=-d "0 0" ui.slash=True ui.interactive=False 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,420 @@ + $ 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 be possible + + $ hg shelve + (empty repository) + shelved as default + 0 files updated, 0 files merged, 5 files removed, 0 files unresolved + + $ hg unshelve + unshelving change 'default' + adding changesets + 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' + + $ hg shelve + nothing changed + [1] + +create another commit + + $ echo n > n + $ hg add n + $ hg commit n -m second + +shelve a change that we will delete later + + $ echo a >> a/a + $ hg shelve + shelved from default (bb4fec6d): second + 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 (glob) + $ 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 (bb4fec6d): second + 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 (bb4fec6d): second (glob) + default [*] shelved from default (bb4fec6d): second (glob) + + $ hg shelve -l -p default + default [*] shelved from default (bb4fec6d): second (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 8 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 8 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 + 3:7ec047b69dc0 + 2:ceefc37abe1e + $ hg parents -q + 2:ceefc37abe1e + 3:7ec047b69dc0 + $ 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 + (use 'hg unshelve --continue' or 'hg unshelve --abort') + [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 + 2:ceefc37abe1e + $ hg parents + changeset: 2:ceefc37abe1e + 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: 2:ceefc37abe1e + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: second + + $ hg heads -q + 2:ceefc37abe1e + + $ 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 + +set up another conflict between a commit and a shelved change + + $ hg revert -q -C -a + $ echo a >> a/a + $ hg shelve -q + $ echo x >> a/a + $ hg ci -m 'create conflict' + $ hg add foo/foo + +if we resolve a conflict while unshelving, the unshelve should succeed + + $ HGMERGE=true hg unshelve + unshelving change 'default' + adding changesets + adding manifests + 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:be7e79683c99 + $ hg shelve -l + $ hg status + M a/a + A foo/foo + $ cat a/a + a + c + x + +test keep and cleanup + + $ hg shelve + shelved from default (be7e7968): create conflict + shelved as default + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg shelve --list + default [*] shelved from default (be7e7968): create conflict (glob) + $ hg unshelve --keep + unshelving change 'default' + adding changesets + 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 [*] shelved from default (be7e7968): create conflict (glob) + $ hg shelve --cleanup + $ hg shelve --list