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

liscju piotr.listkiewicz at gmail.com
Tue Mar 1 15:10:32 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 faf19a7078eddcb14a4272908c53990f88fa096a
# Parent  46461ba496c9fd763f6e47217ba8b6de8b8ca1d1
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.

diff -r 46461ba496c9 -r faf19a7078ed 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,220 @@
+# 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 mercurial.i18n import _
+from mercurial import cmdutil, error, util, merge, repair
+from mercurial.hg import update
+from mercurial.node import bin, hex
+from hgext.shelve import shelvecmd, unshelve, shelvedstate
+from operator import itemgetter
+import errno
+
+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 = 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' % 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.
+    """
+    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 not destbranch:
+        _showbranches(ui, repo)
+        return
+    elif len(destbranch) > 1:
+        raise error.Abort(_('can only switch to one branch at a time'))
+    else:
+        destbranch = destbranch[0]
+
+    originalnode = repo['.'].node()
+    sourcebranch = repo.dirstate.branch()
+
+    if sourcebranch == destbranch:
+        return
+    elif _iscleanwc(repo):
+        update(repo, destbranch)
+        return
+
+    oldquiet = ui.quiet
+    try:
+        ui.quiet = True
+
+        shelvecmd(ui, repo, **{
+            'addremove': True,
+        })
+        update(repo, destbranch)
+        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_ = shelvedstate.load(repo)
+                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
+            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=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):
+    update(repo, originalnode)
+    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 46461ba496c9 -r faf19a7078ed tests/test-check-py3-compat.t
--- a/tests/test-check-py3-compat.t	Mon Feb 22 01:28:59 2016 +0100
+++ b/tests/test-check-py3-compat.t	Mon Feb 22 09:44:32 2016 +0100
@@ -83,6 +83,7 @@
   hgext/share.py not using absolute_import
   hgext/shelve.py not using absolute_import
   hgext/strip.py not using absolute_import
+  hgext/switch.py not using absolute_import
   hgext/transplant.py not using absolute_import
   hgext/win32mbcs.py not using absolute_import
   hgext/win32text.py not using absolute_import
diff -r 46461ba496c9 -r faf19a7078ed 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,214 @@
+  $ 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 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