[PATCH 2 of 2 V3] switch: add a switch extension to easily move between branches (issue4420)

liscju piotr.listkiewicz at gmail.com
Wed Mar 9 04:37:05 EST 2016


# HG changeset patch
# User liscju <piotr.listkiewicz at gmail.com>
# Date 1456130672 -3600
#      Mon Feb 22 09:44:32 2016 +0100
# Node ID b78377e2f386bd13aa617c257c11e69c1aec34a7
# Parent  99d42ff5175af8e03eedb8515395d89c0132319c
switch: add a switch extension to easily move between branches (issue4420)

This extension allows to switch between branches easily, with
saving and restoring working changes using shelve extension.
So far this extension is a first  proposal of solution and i am looking
forward to opinions,code review and help.

Without argument switch command shows possible switch destination -
other branches. With one argument it shelves current changes, updates
to given branch and try to unshelve changes. If there are merge
conflicts while unshelving, it saves current switch state, gives user
opportunity to resolve conflicts and tries again when user invoke
switch --continue.

It also gives user chance to abort this operation with switch --abort,
that aborts shelve operation, updates to previous node and unshelves
last saved shelve. This operation relies on current behaviour of shelve
abort that doesn't do any cleanup of old shelves, so last shelved file
is always available after running shelve --abort.

This implementation marks new/missing files as added/removed before
shelving - this behaviour seems resonable to me but im looking forward
to opinions about it, especially because it is preserving *.orig files.
I have doubts if switch should clean them, and if should then
is cleaning all files with orig extension save. Im looking forward to
opinions about it.

This solution also needs adding locking and transactions, i need
help in those issues because i have very little understanding how
locking/transactions works.

Another thing is if a new extension is really needed to perform switch
operation, so far i have seen proposals to implement this as a part of
shelve cmd or update cmd.

diff -r 99d42ff5175a -r b78377e2f386 hgext/switch.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/switch.py	Mon Feb 22 09:44:32 2016 +0100
@@ -0,0 +1,229 @@
+# switch.py - switch feature for mercurial
+#
+# Copyright 2016 Piotr Listkiewicz <piotr.listkiewicz at gmail dot com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+''' Switches to branch, save and restore changes to the working directory
+'''
+from __future__ import absolute_import
+import errno
+import operator
+from mercurial import (
+    cmdutil,
+    error,
+    hg as hgmod,
+    merge,
+    node as nodemod,
+    util,
+)
+from mercurial.i18n import _
+from . import shelve
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+# Note for extension authors: ONLY specify testedwith = 'internal' for
+# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
+# be specifying the version(s) of Mercurial they are tested with, or
+# leave the attribute unspecified.
+testedwith = 'internal'
+
+switchshelvedstatefile = 'switchshelvedstate'
+origshelvedstatefile = 'shelvedstate'
+
+class switchstate(object):
+    _version = 1
+    _switchstatefile = 'switchstate'
+
+    @classmethod
+    def load(cls, repo):
+        fp = repo.vfs(cls._switchstatefile)
+        try:
+            version = int(fp.readline().strip())
+            if version != cls._version:
+                raise error.Abort(_('this version of switch is incompatible '
+                                   'with the version used in this repo'))
+            originalnode = nodemod.bin(fp.readline().strip())
+            destbranch = fp.readline().strip()
+        finally:
+            fp.close()
+
+        obj = cls()
+        obj.originalnode = originalnode
+        obj.destbranch = destbranch
+
+        return obj
+
+    @classmethod
+    def save(cls, repo, originalnode, destbranch):
+        fp = repo.vfs(cls._switchstatefile, 'wb')
+        fp.write('%i\n' % cls._version)
+        fp.write('%s\n' % nodemod.hex(originalnode))
+        fp.write('%s\n' % destbranch)
+        fp.close()
+
+    @classmethod
+    def clear(cls, repo):
+        util.unlinkpath(repo.join(cls._switchstatefile), ignoremissing=True)
+
+def extsetup(ui):
+    cmdutil.unfinishedstates.append(
+        [switchstate._switchstatefile, False, False,
+         _('switch already in progress'),
+         _("use 'hg switch --continue' or 'hg switch --abort'")])
+    cmdutil.afterresolvedstates.append(
+        [switchstate._switchstatefile, _('hg switch --continue')])
+
+ at command('switch',
+         [('a', 'abort', None,
+           _('abort an incomplete switch operation')),
+          ('c', 'continue', None,
+           _('continue an incomplete switch operation'))],
+         _('hg switch [OPTION] [BRANCH]'))
+def switch(ui, repo, *branch, **opts):
+    """switches to given branch or show possible switch destination
+
+    With no argument show possible switch destination. With one
+    argument saves changes made to the working directory, updates to
+    given branch and restores saved changes. Before switching to other
+    branch it marks new/missing files as added/removed respectively.
+    """
+    if not branch and not opts.get('abort') and not opts.get('continue'):
+        _showbranches(ui, repo)
+        return
+    with repo.wlock():
+        _doswitch(ui, repo, *branch, **opts)
+
+def _doswitch(ui, repo, *destbranch, **opts):
+    abortf = opts.get('abort', None)
+    continuef = opts.get('continue', None)
+
+    if not abortf and not continuef:
+        cmdutil.checkunfinished(repo)
+
+    if abortf or continuef:
+        if abortf and continuef:
+            raise error.Abort(_('cannot use both abort and continue'))
+        if destbranch:
+            raise error.Abort(_('cannot combine abort/continue with '
+                               'selecting a branch to switch'))
+
+        try:
+            state = switchstate.load(repo)
+        except IOError as err:
+            if err.errno != errno.ENOENT:
+                raise
+            raise error.Abort(_('no switch operation underway'))
+
+        if abortf:
+            return switchabort(ui, repo, state)
+        elif continuef:
+            return switchcontinue(ui, repo, state)
+
+    if len(destbranch) > 1:
+        raise error.Abort(_('can only switch to one branch at a time'))
+    elif not destbranch[0] in repo.branchmap():
+        raise error.Abort(_('can only switch between branches'))
+    else:
+        destbranch = destbranch[0]
+
+    originalnode = repo['.'].node()
+    sourcebranch = repo.dirstate.branch()
+
+    if sourcebranch == destbranch:
+        return
+    elif _iscleanwc(repo):
+        hgmod.update(repo, destbranch)
+        return
+
+    oldquiet = ui.quiet
+    try:
+        ui.quiet = True
+
+        shelve.shelvecmd(ui, repo, **{
+            'addremove': True,
+        })
+        hgmod.update(repo, destbranch)
+        shelve.unshelve(ui, repo)
+    except error.InterventionRequired:
+        switchstate.save(repo, originalnode, destbranch)
+        util.rename(repo.join(origshelvedstatefile),
+                    repo.join(switchshelvedstatefile))
+        raise error.InterventionRequired(
+                    _("unresolved conflicts (see 'hg resolve', then "
+                      "'hg switch --continue')"))
+
+    finally:
+        ui.quiet = oldquiet
+
+def switchabort(ui, repo, state):
+    with repo.lock():
+        oldquiet = ui.quiet
+        try:
+            ui.quiet = True
+
+            util.rename(repo.join(switchshelvedstatefile),
+                        repo.join(origshelvedstatefile))
+            try:
+                shelvedstate_ = shelve.shelvedstate.load(repo)
+                shelve.unshelve(ui, repo, **{
+                    'abort': True
+                })
+            except Exception:
+                util.rename(repo.join(origshelvedstatefile),
+                            repo.join(switchshelvedstatefile))
+                raise
+            switchstate.clear(repo)
+            _restorewctx(repo, ui, state.originalnode, shelvedstate_.name)
+        finally:
+            ui.quiet = oldquiet
+            switchstate.clear(repo)
+            ui.warn(_("switch to '%s' aborted\n") % state.destbranch)
+
+def switchcontinue(ui, repo, state):
+    with repo.lock():
+        if _isunresolvedconflict(repo):
+            raise error.Abort(
+                _("unresolved conflicts, can't continue"),
+                hint=_("see 'hg resolve', then 'hg switch --continue'"))
+
+        util.rename(repo.join(switchshelvedstatefile),
+                    repo.join(origshelvedstatefile))
+
+        oldquiet = ui.quiet
+        try:
+            ui.quiet = True
+            shelve.unshelve(ui, repo, **{
+                'continue': True
+            })
+        except Exception:
+            util.rename(repo.join(origshelvedstatefile),
+                        repo.join(switchshelvedstatefile))
+            raise
+        finally:
+            ui.quiet = oldquiet
+
+        switchstate.clear(repo)
+        ui.status(_("switch to branch '%s' complete\n") % state.destbranch)
+
+def _showbranches(ui, repo):
+    currentbranch = repo.dirstate.branch()
+    sortedbranches = sorted(repo.branchmap().iterbranches(),
+                            key=operator.itemgetter(0))
+    for branchname, _, _, _ in sortedbranches:
+        if branchname != currentbranch:
+            ui.write("%s\n" % branchname)
+
+def _iscleanwc(repo):
+    return not repo[None].dirty(missing=True)
+
+def _restorewctx(repo, ui, originalnode, shelvedname):
+    hgmod.update(repo, originalnode)
+    shelve.unshelve(ui, repo, shelvedname)
+
+def _isunresolvedconflict(repo):
+    ms = merge.mergestate.read(repo)
+    if [f for f in ms if ms[f] == 'u']:
+        return True
+    return False
diff -r 99d42ff5175a -r b78377e2f386 tests/test-switch.t
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-switch.t	Mon Feb 22 09:44:32 2016 +0100
@@ -0,0 +1,220 @@
+  $ cat <<EOF >> $HGRCPATH
+  > [extensions]
+  > switch =
+  > purge = 
+  > EOF
+
+Switch has a help message
+
+  $ hg switch -h
+  hg switch [OPTION] [BRANCH]
+  
+  switches to given branch or show possible switch destination
+  
+      With no argument show possible switch destination. With one argument saves
+      changes made to the working directory, updates to given branch and
+      restores saved changes. Before switching to other branch it marks
+      new/missing files as added/removed respectively.
+  
+  options:
+  
+   -a --abort    abort an incomplete switch operation
+   -c --continue continue an incomplete switch operation
+  
+  (some details hidden, use --verbose to show complete help)
+
+Create repository for testing simple cases
+
+  $ hg init switch-simple-cases
+  $ cd switch-simple-cases
+  $ echo "AA" >> a
+  $ hg add a
+  $ hg commit -m "a"
+  $ hg branch default2
+  marked working directory as branch default2
+  (branches are permanent and global, did you want a bookmark?)
+  $ echo "BB" >> b
+  $ hg add b
+  $ hg commit -m "b"
+  $ hg branch default3
+  marked working directory as branch default3
+  $ echo "CC" >> c
+  $ hg add c
+  $ hg commit -m "c"
+  $ hg branches
+  default3                       2:8150990960f6
+  default2                       1:e3ef0d1503d2 (inactive)
+  default                        0:b66327a525ed (inactive)
+  $ hg branch
+  default3
+
+Switch without arguments should show reasonable switch destination - all
+branches except current branch.
+
+  $ hg switch
+  default
+  default2
+
+Switch to the revision shows error
+
+  $ hg switch 2
+  abort: can only switch between branches
+  [255]
+
+Switch to the same branch should preserve everything in working directory
+
+  $ hg branch
+  default3
+  $ ls
+  a
+  b
+  c
+  $ hg status
+  $ touch e
+  $ echo "B2B2" >> b
+  $ hg status
+  M b
+  ? e
+  $ hg switch default3
+  $ hg status
+  M b
+  ? e
+
+Switch to the other branch should recreate changes in working directory
+
+  $ hg branch
+  default3
+  $ hg status
+  M b
+  ? e
+  $ rm a
+  $ hg status
+  M b
+  ! a
+  ? e
+  $ hg switch default2
+  $ hg status
+  M b
+  A e
+  R a
+
+Switch to the other branch should show information about conflicts and
+after resolving conflicts and running switch --continue it should
+succesfully switch to the other branch
+
+  $ hg update -C -r .
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg purge
+  $ hg status
+  $ hg branch
+  default2
+
+It adds file b to branch default to make it later conflicts with default2
+
+  $ hg update -r default
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ ls
+  a
+  $ echo "B2B2" >> b
+  $ hg add b
+  $ hg commit -m "b2"
+
+We go back to branch default2 to make changes to b and switch with conflicts
+to branch default
+
+  $ hg update -r default2
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ ls
+  a
+  b
+  $ echo "ZZ" >> b
+  $ hg status
+  M b
+  $ hg switch default
+  merging b
+  warning: conflicts while merging b! (edit, then use 'hg resolve --mark')
+  unresolved conflicts (see 'hg resolve', then 'hg switch --continue')
+  [1]
+  $ hg status
+  M b
+  ? b.orig
+  $ rm b.orig
+
+When we want to make another operation while switch is in progress it should
+abort it
+
+  $ hg switch default3
+  abort: switch already in progress
+  (use 'hg switch --continue' or 'hg switch --abort')
+  [255]
+
+When we resolve conflict and run hg switch --continue we should have uncommited
+changes
+
+  $ hg resolve --mark b
+  (no more unresolved files)
+  continue: hg switch --continue
+  $ hg switch --continue
+  switch to branch 'default' complete
+  $ hg branch
+  default
+  $ hg status
+  M b
+
+Running hg switch --continue with no switch operation underway
+should abort
+
+  $ hg switch --continue
+  abort: no switch operation underway
+  [255]
+
+We discard changes in working directory and go back to branch default
+
+  $ hg status
+  M b
+  $ hg update -C default2
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg branch
+  default2
+  $ hg status
+  $ ls
+  a
+  b
+
+We make changes to b that will be part of merge conflict while switching
+
+  $ echo "ZZ" >> b
+  $ echo "C" >> d
+  $ hg add d
+  $ rm a
+  $ hg rm a
+  $ hg status
+  M b
+  A d
+  R a
+  $ hg switch default
+  merging b
+  warning: conflicts while merging b! (edit, then use 'hg resolve --mark')
+  unresolved conflicts (see 'hg resolve', then 'hg switch --continue')
+  [1]
+  $ hg status
+  M b
+  M d
+  R a
+  ? b.orig
+  $ rm b.orig
+
+Now we gonna abort changes to see if it recreates changes in default2
+
+  $ hg branch
+  default
+  $ hg switch --abort
+  rebase aborted
+  unshelve of 'default2' aborted
+  switch to 'default' aborted
+  $ hg branch
+  default2
+  $ hg status
+  M b
+  A d
+  R a


More information about the Mercurial-devel mailing list