[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