[PATCH RFC/WIP] introduce stash command

Idan Kamara idankk86 at gmail.com
Sat Jun 2 08:25:11 CDT 2012


# HG changeset patch
# User Idan Kamara <idankk86 at gmail.com>
# Date 1338643506 -10800
# Node ID 6e5f3f08d11199f5893b501dfa8096979675314f
# Parent  357e6bcfb61973478bfbe4cf5652026a6bda7ef7
introduce stash command

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -17,7 +17,7 @@
 import minirst, revset, fileset
 import dagparser, context, simplemerge
 import random, setdiscovery, treediscovery, dagutil, pvec
-import phases
+import phases, stash
 
 table = {}
 
@@ -5159,6 +5159,104 @@
                      ui.configsource(section, name, untrusted))
             ui.write('%s=%s\n' % (sectname, value))
 
+ at command("stash",
+        [('l', 'list', False, _('list all stashes')),
+         ('f', 'force', False, _('unstash with dirty working dir')),
+         ('a', 'apply', False, _('apply the named or most recent stash')),
+         ('', 'pop', False,
+             _('apply and pop the named or most recent stash')),
+         ('p', 'patch', False,
+             _('show patch of the named or most recent stash')),
+         ('d', 'delete', False, _('delete the named or most recent stash')),
+         ('A', 'addremove', None,
+             _('mark new/missing files as added/removed before stashing')),
+         ('m', 'message', '', _('use text as stash message'), _('TEXT'))]
+         + walkopts + mergetoolopts,
+         _('hg stash [-l|-a|--pop|-p|-d] [-f] [NAME]'))
+def stash_(ui, repo, name=None, pop=False, apply=False,
+           delete=False, patch=False, **opts):
+    """stash all changes in the working directory
+
+    Saves the state of the working directory so it can be
+    restored at a later time.
+
+    Stashes are unnamed (by default) and can be listed
+    using --list and inspected with -p/--patch.
+
+    Referring to stashes is done either by index (as
+    shown in --list) or name (if one was given). If neither
+    is specified, the most recent stash is chosen.
+
+    A stash is saved as a regular commit in the repository and
+    is identified by a bookmark. It is hidden from the user
+    (not yet). When a stash is popped or deleted, it is removed
+    from the repository.
+
+    .. container:: verbose
+
+      Examples:
+
+      - stash changes in a named stash::
+
+          hg stash wip
+
+      - unstash (but don't delete) a named stash::
+
+          hg stash -a wip
+
+      - unstash and remove previous stash::
+
+          hg stash --pop
+
+      - show previous stash::
+
+          hg stash -p
+
+      - list all stashes
+
+          hg stash -l
+    """
+    if opts.get('list'):
+        for i, (name, ctx) in enumerate(sorted(stash.list_(repo).iteritems(),
+                                key=lambda k: k[1].rev())):
+            name = name[6:]
+            if name.startswith('unnamed'):
+                name = 'unnamed'
+            ui.write("%d %s@%d:%s - %s\n" % (i, name, ctx.p1().rev(),
+                     short(ctx.p1().node()), ctx.description()))
+        return
+
+    if name:
+        try:
+            name = int(name)
+        except ValueError:
+            name = 'stash/' + name
+
+    if pop or apply:
+        name, ctx = stash.get(repo, name)
+        stats = stash.apply(repo, ctx, **opts)
+        if stats and stats[3] > 0:
+            if pop:
+                raise util.Abort(_('unresolved conflicts, not popping'),
+                                 hint=_('use hg resolve and '
+                                        'hg stash --delete'))
+            else:
+                raise util.Abort(_('unresolved conflicts'),
+                                 hint=_('use hg resolve'))
+        if pop:
+            stash.delete(ui, repo, name)
+    elif delete:
+        name, ctx = stash.get(repo, name)
+        stash.delete(ui, repo, name)
+    elif patch:
+        name, ctx = stash.get(repo, name)
+        diff(ui, repo, change=ctx.rev())
+    else:
+        ret = stash.create(ui, repo, name, **opts)
+        if ret:
+            ui.write(_("stashed working dir "
+                       "(hg stash --pop to unstash)\n"))
+
 @command('^status|st',
     [('A', 'all', None, _('show status of all files')),
     ('m', 'modified', None, _('show only modified files')),
diff --git a/mercurial/stash.py b/mercurial/stash.py
new file mode 100644
--- /dev/null
+++ b/mercurial/stash.py
@@ -0,0 +1,122 @@
+from node import hex, bin, nullid, nullrev, short
+from i18n import _, gettext
+import hg, context, bookmarks, cmdutil, util, repair
+import merge as mergemod
+
+def create(ui, repo, name=None, **opts):
+    # stashes are marked using a bookmark with a special 'stash/' prefix
+    #
+    # we might want to have a more generic 'hg/' namespace for future
+    # use (so stashes will be saved at hg/stash/*)
+    stashes = list_(repo)
+    if name is None:
+        counter = -1
+        for name, ctx in stashes.iteritems():
+            name = name[13:]
+            try:
+                i = int(name)
+                if i > counter:
+                    counter = i
+            except ValueError:
+                pass
+        name = 'stash/unnamed%d' % (counter+1)
+    elif name:
+        if name in stashes:
+            raise util.Abort(_('stash %s aleady exists') % name)
+
+    # this should be at the top, but the circular import is iffy
+    import commands
+    commitopts = dict(opts)
+    if not commitopts['message']:
+        commitopts['message'] = _('Stashed working directory')
+
+    ret = commands.commit(ui, repo, **commitopts)
+    if ret:
+        return
+    ctx = repo['.']
+
+    # silence update
+    repo.ui.pushbuffer()
+    ret = hg.update(repo, node=ctx.p1().node())
+    assert not ret
+    repo.ui.popbuffer()
+
+    if ctx in set(stashes.values()):
+        raise util.Abort(_('same changes already stashed'))
+    marks = repo._bookmarks
+    marks[name] = ctx.node()
+    bookmarks.write(repo)
+
+    return name, ctx
+
+def list_(repo):
+    d = {}
+    for mark, n in repo._bookmarks.iteritems():
+        if mark.startswith('stash/'):
+            d[mark] = repo[n]
+    return d
+
+def get(repo, id_=None):
+    """
+    if id_ = None, get the most recent stash (the one whose revision
+    is maximal (XXX is this true all the time?)). when it's an int,
+    sort the stashes by revision and use it as an index. otherwise,
+    find a named stash with that name.
+    """
+    stashes = list_(repo)
+    if id_ is None:
+        if stashes:
+            return max(stashes.iteritems(), key=lambda kv: kv[1].rev())
+        else:
+            raise util.Abort(_('there are no stashes'))
+    else:
+        if isinstance(id_, int):
+            if id_ >= len(stashes):
+                raise util.Abort(_("stash index out of range"))
+            return sorted(stashes.iteritems(),
+                          key=lambda kv: kv[1].rev())[id_]
+        else:
+            try:
+                return id_, stashes[id_]
+            except KeyError:
+                raise util.Abort(_("stash %s doesn't exist") % id_[6:])
+
+def apply(repo, ctx, force=False, **opts):
+    wctx = repo[None]
+    if not force:
+        if wctx.dirty(branch=False):
+            raise util.Abort(_('cannot apply with a dirty working directory'),
+                             hint=_('use -f to force'))
+    #if wctx.p1() != ctx.p1():
+    #    repo.ui.warn(_('stash parent and working directory parent'
+    #                   ' are different\n'))
+
+    wlock = repo.wlock()
+    try:
+        repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
+        try:
+            # ui.forcemerge is an internal variable, do not document
+            stats = mergemod.update(repo, ctx.node(), True, True,
+                                    False, ctx.p1().node(), True)
+        finally:
+            repo.ui.setconfig('ui', 'forcemerge', '')
+        # drop the second merge parent
+        repo.setparents(wctx.p1().node(), nullid)
+        # fix up dirstate for copies and renames
+        cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
+
+        return stats
+    finally:
+        wlock.release()
+
+def delete(ui, repo, name):
+    node = repo._bookmarks[name]
+    if repo[node].children():
+        raise util.Abort(_('stash has children changesets, cannot delete'))
+    lock = repo.lock()
+    try:
+        del repo._bookmarks[name]
+        bookmarks.write(repo)
+        repair.strip(ui, repo, [node], topic='unstash-backup')
+    finally:
+        lock.release()
diff --git a/tests/test-stash.t b/tests/test-stash.t
new file mode 100644
--- /dev/null
+++ b/tests/test-stash.t
@@ -0,0 +1,154 @@
+  $ printf '[extensions]\ngraphlog=\n' >> $HGRCPATH
+  $ printf '[defaults]\nglog = --template "{rev}:{node|short} {bookmarks} {desc}\\n"\n' >> $HGRCPATH
+  $ hg init
+
+basic setup and testing:
+
+  $ touch a
+  $ hg ci -qAm.
+  $ hg stash
+  nothing changed
+  $ hg stash -l
+  $ hg stash -d
+  abort: there are no stashes
+  [255]
+  $ hg stash -a
+  abort: there are no stashes
+  [255]
+  $ hg stash -p
+  abort: there are no stashes
+  [255]
+  $ hg stash -d foo
+  abort: stash foo doesn't exist
+  [255]
+  $ hg stash -a foo
+  abort: stash foo doesn't exist
+  [255]
+  $ hg stash -p foo
+  abort: stash foo doesn't exist
+  [255]
+
+stash a modification:
+
+  $ echo a >> a
+  $ hg stash
+  stashed working dir (hg stash --pop to unstash)
+  $ hg stash -l
+  0 unnamed at 0:7f6b67e0497a - Stashed working directory
+  $ hg stash -p
+  diff -r 7f6b67e0497a -r * a (glob)
+  --- a/a	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a	* (glob)
+  @@ -0,0 +1,1 @@
+  +a
+
+apply it:
+
+  $ hg stash -a
+  $ hg diff --nodates
+  diff -r 7f6b67e0497a a
+  --- a/a
+  +++ b/a
+  @@ -0,0 +1,1 @@
+  +a
+  $ hg up -C .
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo b >> b
+  $ hg ci -qAm.
+
+pop it (different parents):
+
+  $ hg stash --pop
+  saved backup bundle to $TESTTMP/.hg/strip-backup/*-unstash-backup.hg (glob)
+  $ hg stash -l
+  $ hg diff --nodates
+  diff -r 901c2d3ce0fb a
+  --- a/a
+  +++ b/a
+  @@ -0,0 +1,1 @@
+  +a
+
+named stash:
+
+  $ echo c > c
+  $ echo a >> a
+  $ hg stash -A foo
+  adding c
+  stashed working dir (hg stash --pop to unstash)
+  $ hg stash -l
+  0 foo at 1:901c2d3ce0fb - Stashed working directory
+  $ hg stash -p foo
+  diff -r 901c2d3ce0fb -r * a (glob)
+  --- a/a	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/a	* (glob)
+  @@ -0,0 +1,2 @@
+  +a
+  +a
+  diff -r 901c2d3ce0fb -r * c (glob)
+  --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+  +++ b/c	* (glob)
+  @@ -0,0 +1,1 @@
+  +c
+  $ hg stash -a
+  $ hg stash -d foo
+  saved backup bundle to $TESTTMP/.hg/strip-backup/*-unstash-backup.hg (glob)
+
+create several unnamed stashes:
+
+  $ echo a >> a
+  $ hg stash
+  stashed working dir (hg stash --pop to unstash)
+  $ echo aa >> a
+  $ hg stash -m wip
+  created new head
+  stashed working dir (hg stash --pop to unstash)
+  $ echo aaa >> a
+  $ hg stash
+  created new head
+  stashed working dir (hg stash --pop to unstash)
+  $ hg stash -l
+  0 unnamed at 1:901c2d3ce0fb - Stashed working directory
+  1 unnamed at 1:901c2d3ce0fb - wip
+  2 unnamed at 1:901c2d3ce0fb - Stashed working directory
+
+apply them with a conflict:
+
+  $ hg stash --pop 1
+  saved backup bundle to $TESTTMP/.hg/strip-backup/*-unstash-backup.hg (glob)
+  $ hg stash -l
+  0 unnamed at 1:901c2d3ce0fb - Stashed working directory
+  1 unnamed at 1:901c2d3ce0fb - Stashed working directory
+  $ hg stash -f -a
+  merging a
+  warning: conflicts during merge.
+  merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
+  abort: unresolved conflicts
+  (use hg resolve)
+  [255]
+
+use mergetool when applying:
+
+  $ mv a.orig a
+  $ hg stash -f --pop --tool "internal:other"
+  saved backup bundle to $TESTTMP/.hg/strip-backup/*-unstash-backup.hg (glob)
+  $ hg diff --nodates
+  diff -r 901c2d3ce0fb a
+  --- a/a
+  +++ b/a
+  @@ -0,0 +1,1 @@
+  +aaa
+  $ hg stash -d
+  saved backup bundle to $TESTTMP/.hg/strip-backup/*-unstash-backup.hg (glob)
+
+test that renames are preserved:
+
+  $ hg up -qC .
+  $ hg mv b c
+  $ hg stash
+  stashed working dir (hg stash --pop to unstash)
+  $ hg stash --pop
+  saved backup bundle to $TESTTMP/.hg/strip-backup/*-unstash-backup.hg (glob)
+  $ hg st --copies
+  M c
+    b
+  R b


More information about the Mercurial-devel mailing list