branch: add a --rev flag to change branch name of given revisions
authorPulkit Goyal <7895pulkit@gmail.com>
Sun, 15 Oct 2017 23:08:45 +0530
changeset 35745 3bd8ab4c80a5
parent 35744 8685192a8733
child 35746 e5b6ba786d83
branch: add a --rev flag to change branch name of given revisions This patch adds a new --rev flag to hg branch which can be used to change branch of revisions. This is motivated from topic extension where you can change topic on revisions but this one has few restrictions which are: 1) You cannot change branch name in between the stack 2) You cannot change branch name and set it to an existing name 3) You cannot change branch of non-linear set of commits 4) You cannot change branch of merge commits. Tests are added for the same. .. feature:: An experimental flag `--rev` to `hg branch` which can be used to change branch of changesets. Differential Revision: https://phab.mercurial-scm.org/D1074
mercurial/cmdutil.py
mercurial/commands.py
tests/test-branch-change.t
tests/test-completion.t
--- a/mercurial/cmdutil.py	Tue Jan 16 23:50:01 2018 +0900
+++ b/mercurial/cmdutil.py	Sun Oct 15 23:08:45 2017 +0530
@@ -42,6 +42,7 @@
     revlog,
     revset,
     revsetlang,
+    rewriteutil,
     scmutil,
     smartset,
     templatekw,
@@ -713,6 +714,92 @@
 
     raise error.UnknownCommand(cmd, allcmds)
 
+def changebranch(ui, repo, revs, label):
+    """ Change the branch name of given revs to label """
+
+    with repo.wlock(), repo.lock(), repo.transaction('branches'):
+        # abort in case of uncommitted merge or dirty wdir
+        bailifchanged(repo)
+        revs = scmutil.revrange(repo, revs)
+        if not revs:
+            raise error.Abort("empty revision set")
+        roots = repo.revs('roots(%ld)', revs)
+        if len(roots) > 1:
+            raise error.Abort(_("cannot change branch of non-linear revisions"))
+        rewriteutil.precheck(repo, revs, 'change branch of')
+        if repo.revs('merge() and %ld', revs):
+            raise error.Abort(_("cannot change branch of a merge commit"))
+        if repo.revs('obsolete() and %ld', revs):
+            raise error.Abort(_("cannot change branch of a obsolete changeset"))
+
+        # make sure only topological heads
+        if repo.revs('heads(%ld) - head()', revs):
+            raise error.Abort(_("cannot change branch in middle of a stack"))
+
+        replacements = {}
+        # avoid import cycle mercurial.cmdutil -> mercurial.context ->
+        # mercurial.subrepo -> mercurial.cmdutil
+        from . import context
+        for rev in revs:
+            ctx = repo[rev]
+            oldbranch = ctx.branch()
+            # check if ctx has same branch
+            if oldbranch == label:
+                continue
+
+            def filectxfn(repo, newctx, path):
+                try:
+                    return ctx[path]
+                except error.ManifestLookupError:
+                    return None
+
+            ui.debug("changing branch of '%s' from '%s' to '%s'\n"
+                     % (hex(ctx.node()), oldbranch, label))
+            extra = ctx.extra()
+            extra['branch_change'] = hex(ctx.node())
+            # While changing branch of set of linear commits, make sure that
+            # we base our commits on new parent rather than old parent which
+            # was obsoleted while changing the branch
+            p1 = ctx.p1().node()
+            p2 = ctx.p2().node()
+            if p1 in replacements:
+                p1 = replacements[p1][0]
+            if p2 in replacements:
+                p2 = replacements[p2][0]
+
+            mc = context.memctx(repo, (p1, p2),
+                                ctx.description(),
+                                ctx.files(),
+                                filectxfn,
+                                user=ctx.user(),
+                                date=ctx.date(),
+                                extra=extra,
+                                branch=label)
+
+            commitphase = ctx.phase()
+            overrides = {('phases', 'new-commit'): commitphase}
+            with repo.ui.configoverride(overrides, 'branch-change'):
+                newnode = repo.commitctx(mc)
+
+            replacements[ctx.node()] = (newnode,)
+            ui.debug('new node id is %s\n' % hex(newnode))
+
+        # create obsmarkers and move bookmarks
+        scmutil.cleanupnodes(repo, replacements, 'branch-change')
+
+        # move the working copy too
+        wctx = repo[None]
+        # in-progress merge is a bit too complex for now.
+        if len(wctx.parents()) == 1:
+            newid = replacements.get(wctx.p1().node())
+            if newid is not None:
+                # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
+                # mercurial.cmdutil
+                from . import hg
+                hg.update(repo, newid[0], quietempty=True)
+
+        ui.status(_("changed branch on %d changesets\n") % len(replacements))
+
 def findrepo(p):
     while not os.path.isdir(os.path.join(p, ".hg")):
         oldp, p = p, os.path.dirname(p)
--- a/mercurial/commands.py	Tue Jan 16 23:50:01 2018 +0900
+++ b/mercurial/commands.py	Sun Oct 15 23:08:45 2017 +0530
@@ -1002,7 +1002,9 @@
 @command('branch',
     [('f', 'force', None,
      _('set branch name even if it shadows an existing branch')),
-    ('C', 'clean', None, _('reset branch name to parent branch name'))],
+     ('C', 'clean', None, _('reset branch name to parent branch name')),
+     ('r', 'rev', [], _('change branches of the given revs (EXPERIMENTAL)')),
+    ],
     _('[-fC] [NAME]'))
 def branch(ui, repo, label=None, **opts):
     """set or show the current branch name
@@ -1034,10 +1036,13 @@
     Returns 0 on success.
     """
     opts = pycompat.byteskwargs(opts)
+    revs = opts.get('rev')
     if label:
         label = label.strip()
 
     if not opts.get('clean') and not label:
+        if revs:
+            raise error.Abort(_("no branch name specified for the revisions"))
         ui.write("%s\n" % repo.dirstate.branch())
         return
 
@@ -1047,13 +1052,23 @@
             repo.dirstate.setbranch(label)
             ui.status(_('reset working directory to branch %s\n') % label)
         elif label:
+
+            scmutil.checknewlabel(repo, label, 'branch')
+            if revs:
+                # XXX: we should allow setting name to existing branch if the
+                # branch of root of the revs is same as the new branch name
+                if label in repo.branchmap():
+                    raise error.Abort(_('a branch of the same'
+                                        ' name already exists'))
+                return cmdutil.changebranch(ui, repo, revs, label)
+
             if not opts.get('force') and label in repo.branchmap():
                 if label not in [p.branch() for p in repo[None].parents()]:
                     raise error.Abort(_('a branch of the same name already'
                                        ' exists'),
                                      # i18n: "it" refers to an existing branch
                                      hint=_("use 'hg update' to switch to it"))
-            scmutil.checknewlabel(repo, label, 'branch')
+
             repo.dirstate.setbranch(label)
             ui.status(_('marked working directory as branch %s\n') % label)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-branch-change.t	Sun Oct 15 23:08:45 2017 +0530
@@ -0,0 +1,296 @@
+Testing changing branch on commits
+==================================
+
+Setup
+
+  $ cat >> $HGRCPATH << EOF
+  > [alias]
+  > glog = log -G -T "{rev}:{node|short} {desc}\n{branch} ({bookmarks})"
+  > [experimental]
+  > evolution = createmarkers
+  > [extensions]
+  > rebase=
+  > EOF
+
+  $ hg init repo
+  $ cd repo
+  $ for ch in a b c d e; do echo foo >> $ch; hg ci -Aqm "Added "$ch; done
+  $ hg glog
+  @  4:aa98ab95a928 Added e
+  |  default ()
+  o  3:62615734edd5 Added d
+  |  default ()
+  o  2:28ad74487de9 Added c
+  |  default ()
+  o  1:29becc82797a Added b
+  |  default ()
+  o  0:18d04c59bb5d Added a
+     default ()
+
+  $ hg branches
+  default                        4:aa98ab95a928
+
+Try without passing a new branch name
+
+  $ hg branch -r .
+  abort: no branch name specified for the revisions
+  [255]
+
+Setting an invalid branch name
+
+  $ hg branch -r . a:b
+  abort: ':' cannot be used in a name
+  [255]
+  $ hg branch -r . tip
+  abort: the name 'tip' is reserved
+  [255]
+  $ hg branch -r . 1234
+  abort: cannot use an integer as a name
+  [255]
+
+Change on non-linear set of commits
+
+  $ hg branch -r 2 -r 4 foo
+  abort: cannot change branch of non-linear revisions
+  [255]
+
+Change in middle of the stack (linear commits)
+
+  $ hg branch -r 1::3 foo
+  abort: cannot change branch of changeset with children
+  [255]
+
+Change with dirty working directory
+
+  $ echo bar > a
+  $ hg branch -r . foo
+  abort: uncommitted changes
+  [255]
+
+  $ hg revert --all
+  reverting a
+
+Change on empty revision set
+
+  $ hg branch -r 'draft() - all()' foo
+  abort: empty revision set
+  [255]
+
+Changing branch on linear set of commits from head
+
+Without obsmarkers
+
+  $ hg branch -r 3:4 foo --config experimental.evolution=!
+  changed branch on 2 changesets
+  saved backup bundle to $TESTTMP/repo/.hg/strip-backup/62615734edd5-e86bd13a-branch-change.hg (glob)
+  $ hg glog
+  @  4:3938acfb5c0f Added e
+  |  foo ()
+  o  3:9435da006bdc Added d
+  |  foo ()
+  o  2:28ad74487de9 Added c
+  |  default ()
+  o  1:29becc82797a Added b
+  |  default ()
+  o  0:18d04c59bb5d Added a
+     default ()
+
+  $ hg branches
+  foo                            4:3938acfb5c0f
+  default                        2:28ad74487de9 (inactive)
+
+With obsmarkers
+
+  $ hg branch -r 3::4 bar
+  changed branch on 2 changesets
+  $ hg glog
+  @  6:7c1991464886 Added e
+  |  bar ()
+  o  5:1ea05e93925f Added d
+  |  bar ()
+  o  2:28ad74487de9 Added c
+  |  default ()
+  o  1:29becc82797a Added b
+  |  default ()
+  o  0:18d04c59bb5d Added a
+     default ()
+
+  $ hg branches
+  bar                            6:7c1991464886
+  default                        2:28ad74487de9 (inactive)
+
+Change branch name to an existing branch
+
+  $ hg branch -r . default
+  abort: a branch of the same name already exists
+  [255]
+
+Changing on a branch head which is not topological head
+
+  $ hg branch -r 2 stable
+  abort: cannot change branch of changeset with children
+  [255]
+
+Enabling the allowunstable config and trying to change branch on a branch head
+which is not a topological head
+
+  $ echo "[experimental]" >> .hg/hgrc
+  $ echo "evolution.allowunstable=yes" >> .hg/hgrc
+  $ hg branch -r 2 foo
+  changed branch on 1 changesets
+  2 new orphan changesets
+
+Changing branch of an obsoleted changeset
+
+  $ hg branch -r 4 foobar
+  abort: hidden revision '4' was rewritten as: 7c1991464886!
+  (use --hidden to access hidden revisions)
+  [255]
+
+  $ hg branch -r 4 --hidden foobar
+  abort: cannot change branch of a obsolete changeset
+  [255]
+
+Make sure bookmark movement is correct
+
+  $ hg bookmark b1
+  $ hg glog -r '.^::'
+  @  6:7c1991464886 Added e
+  |  bar (b1)
+  *  5:1ea05e93925f Added d
+  |  bar ()
+  ~
+
+  $ hg branch -r '(.^)::' wat --debug
+  changing branch of '1ea05e93925f806d875a2163f9b76764be644636' from 'bar' to 'wat'
+  committing files:
+  d
+  committing manifest
+  committing changelog
+  new node id is 343660ccab7400da637bd6a211d07f413536d718
+  changing branch of '7c19914648869f5b02fc7fed31ddee9783fdd680' from 'bar' to 'wat'
+  committing files:
+  e
+  committing manifest
+  committing changelog
+  new node id is de1404b45a69f8cc6437d7679033ee33e9efb4ba
+  moving bookmarks ['b1'] from 7c19914648869f5b02fc7fed31ddee9783fdd680 to de1404b45a69f8cc6437d7679033ee33e9efb4ba
+  resolving manifests
+   branchmerge: False, force: False, partial: False
+   ancestor: 7c1991464886, local: 7c1991464886+, remote: de1404b45a69
+  changed branch on 2 changesets
+  updating the branch cache
+  invalid branchheads cache (served): tip differs
+
+  $ hg glog -r '(.^)::'
+  @  9:de1404b45a69 Added e
+  |  wat (b1)
+  *  8:343660ccab74 Added d
+  |  wat ()
+  ~
+
+Make sure phase handling is correct
+
+  $ echo foo >> bar
+  $ hg ci -Aqm "added bar" --secret
+  1 new orphan changesets
+  $ hg glog -r .
+  @  10:8ad1294c1660 added bar
+  |  wat (b1)
+  ~
+  $ hg branch -r . secret
+  changed branch on 1 changesets
+  $ hg phase -r .
+  11: secret
+
+  $ hg branches
+  secret                        11:38a9b2d53f98
+  foo                            7:8a4729a5e2b8
+  wat                            9:de1404b45a69 (inactive)
+  default                        2:28ad74487de9 (inactive)
+  $ hg branch
+  secret
+
+Changing branch of another head, different from one on which we are
+
+  $ hg glog
+  @  11:38a9b2d53f98 added bar
+  |  secret (b1)
+  *  9:de1404b45a69 Added e
+  |  wat ()
+  *  8:343660ccab74 Added d
+  |  wat ()
+  | o  7:8a4729a5e2b8 Added c
+  | |  foo ()
+  x |  2:28ad74487de9 Added c
+  |/   default ()
+  o  1:29becc82797a Added b
+  |  default ()
+  o  0:18d04c59bb5d Added a
+     default ()
+
+  $ hg branch
+  secret
+
+  $ hg branch -r 7 foobar
+  changed branch on 1 changesets
+
+The current branch must be preserved
+  $ hg branch
+  secret
+
+Changing branch on multiple heads at once
+
+  $ hg rebase -s 8 -d 12 --keepbranches -q
+
+  $ hg rebase -s 14 -d 1 --keepbranches -q
+
+  $ hg branch -r 0: stable
+  changed branch on 6 changesets
+  $ hg glog
+  @  23:6a5ddbcfb870 added bar
+  |  stable (b1)
+  o  22:baedc6e98a67 Added e
+  |  stable ()
+  | o  21:99ac7bf8aad1 Added d
+  | |  stable ()
+  | o  20:0ecb4d39c4bd Added c
+  |/   stable ()
+  o  19:fd45b986b109 Added b
+  |  stable ()
+  o  18:204d2769eca2 Added a
+     stable ()
+
+  $ hg branches
+  stable                        23:6a5ddbcfb870
+
+  $ hg branch
+  stable
+
+Changing to same branch name does not work
+
+  $ hg branch -r 19::21 stable
+  abort: a branch of the same name already exists
+  [255]
+
+Testing on merge
+
+  $ hg merge -r 20
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+
+  $ hg branch -r . abcd
+  abort: outstanding uncommitted merge
+  [255]
+  $ hg ci -m "Merge commit"
+  $ hg branch -r '(.^)::' def
+  abort: cannot change branch of a merge commit
+  [255]
+
+Changing branch on public changeset
+
+  $ hg phase -r 21 -p
+  $ hg branch -r 21 def
+  abort: cannot change branch of public changesets
+  (see 'hg help phases' for details)
+  [255]
--- a/tests/test-completion.t	Tue Jan 16 23:50:01 2018 +0900
+++ b/tests/test-completion.t	Sun Oct 15 23:08:45 2017 +0530
@@ -242,7 +242,7 @@
   backout: merge, commit, no-commit, parent, rev, edit, tool, include, exclude, message, logfile, date, user
   bisect: reset, good, bad, skip, extend, command, noupdate
   bookmarks: force, rev, delete, rename, inactive, template
-  branch: force, clean
+  branch: force, clean, rev
   branches: active, closed, template
   bundle: force, rev, branch, base, all, type, ssh, remotecmd, insecure
   cat: output, rev, decode, include, exclude, template