[PATCH 2 of 5] shelve: add a shelve extension to save/restore working changes

Pierre-Yves David pierre-yves.david at ens-lyon.org
Wed Sep 18 13:04:59 CDT 2013


On 09/17/2013 05:55 PM, David Soria Parra wrote:
> # HG changeset patch
> # User David Soria Parra<dsp at experimentalworks.net>
> # Date 1377793333 25200
> #      Thu Aug 29 09:22:13 2013 -0700
> # Node ID f533657af87051e0a7a3d6ffc30a9dd7d2415d5a
> # Parent  4732ba61dd562a85f2517a634b67f49ea3229f2e
> shelve: add a shelve extension to save/restore working changes
>
> This extension saves shelved changes using a temporary draft commit,
> and bundles the temporary commit and its draft ancestors, then
> strips them.
>
> This strategy makes it possible to use Mercurial's bundle and merge
> machinery to resolve conflicts if necessary when unshelving, even
> when the destination commit or its ancestors have been amended,
> squashed, or evolved. (Once a change has been unshelved, its
> associated unbundled commits are either rolled back or stripped.)
>
> Storing the shelved change as a bundle also avoids the difficulty
> that hidden commits would cause, of making it impossible to amend
> the parent if it is a draft commits (a common scenario).
>
> 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
> conflict markers, 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.

Note: have you checked the Sean Farley progress on this topic ?

>
> diff --git a/hgext/color.py b/hgext/color.py
> --- a/hgext/color.py
> +++ b/hgext/color.py
> @@ -63,6 +63,10 @@
>     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 @@
>              '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,587 @@
> +# 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, 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
> +
> +cmdtable = {}
> +command = cmdutil.command(cmdtable)
> +testedwith = 'internal'
> +
> +class shelvedfile(object):
> +    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:
> +                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)
> +            raise

small nitch:

I prefer

   if err.errno != errno.ENOENT:
       raise
   big chunk of code

over the current

   if err.errno == errno.ENOENT:
       big chunk of code
   raise

The first one is actually used more often in the rest of the file.

> +
> +class shelvedstate(object):

This class could use a few line of documentation explaining its goal and 
the disc format used.
> +    _version = '1'
> +
> +    @classmethod
> +    def load(cls, repo):
> +        fp = repo.opener('shelvedstate')
> +        try:
> +            lines = fp.read().splitlines()
> +        finally:
> +            fp.close()
> +        lines.reverse()
> +
> +        version = lines.pop()
> +        if version != cls._version:
> +            raise util.Abort(_('this version of shelve is incompatible '
> +                               'with the version used in this repo'))
> +        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

I had some terrible experience with attribute that appears within some 
other code.  Can we have a __init__ that initialises all known attribute 
(possibility to None is that help)?

> +
> +    @classmethod
> +    def save(cls, repo, name, stripnodes):
> +        fp = repo.opener('shelvedstate', 'wb')
> +        fp.write(cls._version + '\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 n in stripnodes:
> +            fp.write(node.hex(n) + '\n')
> +        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()
> +
> +    try:
> +        shelvedstate.load(repo)
> +        raise util.Abort(_('unshelve already in progress'))

Don't we just get a nice a shinny API to check multi-step operation ?

> +    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'))

Why is the lack of history an issue for shelve ?
> +
> +    try:
> +        user = repo.ui.username()
> +    except util.Abort:
> +        user = 'shelve at localhost'

elsewhere in Mercurial, the usename is a strong requirement ? Why isn't 
it the case here ?
(and additional question, why shelve have no -u argument ?)

> +
> +    label = repo._bookmarkcurrent or parent.branch()
> +
> +    # 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)
> +
> +    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']
> +
> +    wlock = lock = None
> +    try:
> +        wlock = repo.wlock()
> +        lock = repo.lock()
> +
> +        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)
> +
> +        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 '.'"))

Is that for filename validity ? I believe there is other constrains out 
there (like no ":" on Mac etc). Do we have a generic function in core to 
handle that ?

> +
> +        node = cmdutil.commit(ui, repo, commitfunc, pats, opts)

Question: could we use a non-commited transaction to avoid the strip and 
a possible race condition with pull ?

> +
> +        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
> +
> +        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())
> +        repair.strip(ui, repo, [node], backup='none', topic='shelve')
> +    finally:
> +        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)
> +
> + at command('unshelve',
> +         [('a', 'abort', None,
> +           _('abort an incomplete unshelve operation')),
> +          ('c', 'continue', None,
> +           _('continue an incomplete unshelve operation')),
> +          ('', 'keep', None,
> +           _('save shelved change'))],

This help text is a bit obscur. What about:

"do not delete the 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.

Note: The latest unshelved changed could be kept under a special name. 
That would ease undoing error.

> +
> +    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 = 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'))

really ? It very very impractical that local changes prevent unshelving.
It that a temporary technical limitiation or a real UI choice ?
As we allows it for file not impacted by the shelve this seems 
inconsistent. We should either allow it or deny it all the time.
I would prefer to allow it at all time and we have the technologie for 
it (see merge --force)

> +    wlock = lock = None
> +    try:
> +        lock = repo.lock()
> +
> +        oldtiprev = len(repo)
> +        try:
> +            fp = shelvedfile(repo, basename, 'hg').opener()
> +            gen = changegroup.readbundle(fp, fp.name)
> +            repo.addchangegroup(gen, 'unshelve', 'bundle:' + fp.name)
> +        finally:
> +            fp.close()

Could we force the unbundled content as secret to prevent pull race ?

> +
> +        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)
> +
> +        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
> @@ -341,6 +341,7 @@
>       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 @@
>   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 @@
>    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,401 @@
> +  $ 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]
> +
> +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

note: the "update" line is confusing. I can see why it is here but we 
should probably improves that in the future.

> +
> +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:da6db56b46f7
> +  2:ceefc37abe1e
> +  $ hg parents -q
> +  2:ceefc37abe1e
> +  3:da6db56b46f7
> +  $ 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
> +  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 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 shelve --cleanup
> +  $ hg shelve --list
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel at selenic.com
> http://selenic.com/mailman/listinfo/mercurial-devel



More information about the Mercurial-devel mailing list