[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 18:04:59 UTC 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