Patchwork D1074: branch: add a --rev flag to change branch name of given revisions

login
register
mail settings
Submitter phabricator
Date Oct. 14, 2017, 2:42 p.m.
Message ID <differential-rev-PHID-DREV-g6ilhav4ycyppv45s7if-req@phab.mercurial-scm.org>
Download mbox | patch
Permalink /patch/24899/
State Superseded
Headers show

Comments

phabricator - Oct. 14, 2017, 2:42 p.m.
pulkit created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  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
  
  Tests are added for the same.
  
  .. feature::
  
    An experimental flag `--rev` to `hg branch` which can be used to change
    branch of changesets.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

AFFECTED FILES
  mercurial/cmdutil.py
  mercurial/commands.py
  tests/test-branch-change.t
  tests/test-completion.t

CHANGE DETAILS




To: pulkit, #hg-reviewers
Cc: mercurial-devel
phabricator - Oct. 15, 2017, 9:03 a.m.
dlax added inline comments.

INLINE COMMENTS

> cmdutil.py:749
> +    if len(heads) < 1:
> +        raise error.Abort(_("cannot change branch in betwen the stack"))
> +

typo: betwen

Also, I'm not sure "between" is appropriate here. Maybe "cannot change branch in the middle of a stack"?

> cmdutil.py:770
> +
> +            ui.debug("changing branch of '%s' from '%s' to '%s'" % (
> +                            hex(ctx.node()), oldbranch, label))

nit: with `%` on the continuation line and alignment with opening parenthesis, it'd be more readable.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers
Cc: dlax, mercurial-devel
phabricator - Oct. 16, 2017, 7:59 a.m.
dlax requested changes to this revision.
dlax added a comment.
This revision now requires changes to proceed.


  This looks correct to me overall.
  I use this feature with topics a lot and find it pretty convenient so I guess it might make sense for named branches in some workflows.

INLINE COMMENTS

> cmdutil.py:752
> +
> +            ui.debug("changing branch of '%s' from '%s' to '%s'"
> +                     % (hex(ctx.node()), oldbranch, label))

This message is not tested, maybe add a `--debug` call to one of the tests?

> cmdutil.py:774
> +                                branch=label)
> +            # phase handling
> +            commitphase = ctx.phase()

That comment isn't really helpful as is. Maybe explain how you handle phase instead?

> cmdutil.py:782
> +            ui.debug('new node id is %s\n' % hex(newnode))
> +            rewrote += 1
> +

Isn't `rewrote == len(replacements)`?

> commands.py:997
> +    ('C', 'clean', None, _('reset branch name to parent branch name')),
> +    ('r', 'rev', [], _('change branches of the given revs (EXPERIMENTAL)'))],
>      _('[-fC] [NAME]'))

Nit: could you move the closing `]` on the next line and add a `,` at the end of the new line? This way next time we add an option, there will be only a one-line `+` diff.

> commands.py:1035
> +        if revs:
> +            raise error.Abort("no branch name specified for the revisions")
>          ui.write("%s\n" % repo.dirstate.branch())

Maybe `_()`.

> test-branch-change.t:41
> +Try without passing a new branch name
> +-----------------------------------------
> +

Not sure why, but many titles' underlines are either too long or too short. Could you make these consistent?

> test-branch-change.t:67
> +
> +Change in between the stack (linear commits)
> +------------------------------------------------------

"in the middle of the stack"

> test-branch-change.t:255
> +
> +Changing branch on mutliple heads at once
> +-----------------------------------------

typo: mutliple

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax
Cc: dlax, mercurial-devel
phabricator - Oct. 17, 2017, 12:54 p.m.
ryanmce requested changes to this revision.
ryanmce added inline comments.
This revision now requires changes to proceed.

INLINE COMMENTS

> cmdutil.py:722-731
> +    revs = scmutil.revrange(repo, revs)
> +    roots = repo.revs('roots(%ld)', revs)
> +    if len(roots) > 1:
> +        raise error.Abort(_("cannot change branch of non-linear revisions"))
> +    root = repo[roots.first()]
> +    if root.phase() <= phases.public:
> +        raise error.Abort(_("cannot change branch of public revisions"))

What about an unclean working copy? It looks like that's supported -- could we make sure it's tested?

> cmdutil.py:733-734
> +
> +    replacements = {}
> +    with repo.wlock(), repo.lock(), repo.transaction('branches'):
> +        # avoid import cycle mercurial.cmdutil -> mercurial.context ->

In theory, the phase boundary could move after our check but before we take the repolock. Can we guard against that please?

Two ways to accomplish this:

1/ Take the repo lock earlier
2/ Check before repo lock (fast path) and again after the repo lock is held (in case something changed)

> cmdutil.py:791-792
> +            if newid is not None:
> +                # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
> +                # mercurial.cmdutil
> +                from . import hg

Seems like we need a refactor? (out of scope for this change though)

> test-branch-change.t:8
> +  > [alias]
> +  > glog = log -G -T "{rev}:{node|short} {desc}\n{branch} ({bookmarks})"
> +  > [experimental]

nit: I'd prefer no newline in the glog output so things are more compact

> test-branch-change.t:10
> +  > [experimental]
> +  > evolution = createmarkers
> +  > [extensions]

This test needs to include a case where we strip commits, especially with a merge, just so the behavior is clear in this change.

That will probably (in my understanding of the current code) expose an issue that needs to be fixed.

> test-branch-change.t:17-34
> +  $ for ch in a b c d e f g h; do echo foo >> $ch; hg ci -Aqm "Added "$ch; done
> +  $ hg glog
> +  @  7:ec2426147f0e Added h
> +  |  default ()
> +  o  6:87d6d6676308 Added g
> +  |  default ()
> +  o  5:825660c69f0c Added f

Not sure we need this many commits for the test to be useful.

> test-branch-change.t:304-305
> +
> +  $ hg branch -r 23::28 wat
> +  changed branch on 0 changesets
> +  $ hg glog

Hm, I think I'd prefer this to be an error about public changesets rather than a no-op with a 0 return value. Trying to modify the branch of public changesets -- even if the change is a no-op, still seems like an error to me. Thoughts?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce
Cc: ryanmce, dlax, mercurial-devel
phabricator - Oct. 17, 2017, 1:26 p.m.
pulkit added inline comments.

INLINE COMMENTS

> ryanmce wrote in cmdutil.py:791-792
> Seems like we need a refactor? (out of scope for this change though)

Yeah, I will be happy to do that in next cycle. Can you suggest a name for file in which we can split cmdutil.py.

> ryanmce wrote in test-branch-change.t:304-305
> Hm, I think I'd prefer this to be an error about public changesets rather than a no-op with a 0 return value. Trying to modify the branch of public changesets -- even if the change is a no-op, still seems like an error to me. Thoughts?

Oh, no. They are not public changesets. The test for public changeset is just above it. I will plug the phase info in the glog output in next version.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce
Cc: ryanmce, dlax, mercurial-devel
phabricator - Oct. 17, 2017, 9:01 p.m.
pulkit marked 4 inline comments as done.
pulkit added inline comments.

INLINE COMMENTS

> ryanmce wrote in cmdutil.py:722-731
> What about an unclean working copy? It looks like that's supported -- could we make sure it's tested?

It's not supported. Handled that case and added a test. Thanks!

> ryanmce wrote in cmdutil.py:733-734
> In theory, the phase boundary could move after our check but before we take the repolock. Can we guard against that please?
> 
> Two ways to accomplish this:
> 
> 1/ Take the repo lock earlier
> 2/ Check before repo lock (fast path) and again after the repo lock is held (in case something changed)

Improved the implementation by taking lock earlier. Thanks!

> ryanmce wrote in test-branch-change.t:10
> This test needs to include a case where we strip commits, especially with a merge, just so the behavior is clear in this change.
> 
> That will probably (in my understanding of the current code) expose an issue that needs to be fixed.

Oh, I didn't know that. Just banned merge revisions for now.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce
Cc: ryanmce, dlax, mercurial-devel
phabricator - Nov. 2, 2017, 6:14 p.m.
pulkit added a comment.


  Re-requesting review since the freeze is over.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce
Cc: ryanmce, dlax, mercurial-devel
phabricator - Nov. 17, 2017, 10:39 a.m.
lothiraldan added a comment.


  Looking good, should we allow changing the branch if any changeset is unstable or obsolete?

INLINE COMMENTS

> cmdutil.py:723
> +    with repo.wlock(), repo.lock(), repo.transaction('branches'):
> +        # abort incase in uncommitted merger of dirty wdir
> +        bailifchanged(repo)

Small typo, missing space between `in` and `case`

> cmdutil.py:774
> +                p2 = replacements[p2][0]
> +
> +            mc = context.memctx(repo, (p1, p2),

For safety, I would add a check that p1 and p2 are not obsolete here.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce
Cc: lothiraldan, ryanmce, dlax, mercurial-devel
phabricator - Nov. 28, 2017, 10:49 p.m.
pulkit added a comment.


  In https://phab.mercurial-scm.org/D1074#23972, @lothiraldan wrote:
  
  > Looking good, should we allow changing the branch if any changeset is unstable or obsolete?
  
  
  For unstable `yes`, for obsolete `no`. Will add a check and test for obsolete case.

INLINE COMMENTS

> lothiraldan wrote in cmdutil.py:774
> For safety, I would add a check that p1 and p2 are not obsolete here.

A user may want to change branch of an unstable changeset which I think is okay. I am not sure why you want that check?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce
Cc: lothiraldan, ryanmce, dlax, mercurial-devel
phabricator - Dec. 4, 2017, 2:38 p.m.
pulkit added a comment.


  This was send before the code freeze of last major release but since it was late in the release, it was not taken. Let's iterate on it now.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce
Cc: lothiraldan, ryanmce, dlax, mercurial-devel
phabricator - Jan. 15, 2018, 6:45 p.m.
pulkit added subscribers: indygreg, yuja, krbullock, durin42.
pulkit added a comment.


  @durin42 @yuja @indygreg @krbullock this one is lying here for months. Does this conflict with anything or is a BC or something else, or was just missed for the review?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce
Cc: durin42, krbullock, yuja, indygreg, lothiraldan, ryanmce, dlax, mercurial-devel
phabricator - Jan. 16, 2018, 8:15 p.m.
lothiraldan added a comment.


  Apart from my review, it looks good to me.
  
  Also I don't think you have a test when trying to change the branch of several changesets on different branches. If not, could you add one?

INLINE COMMENTS

> cmdutil.py:731
> +        if repo.revs('merge() and %ld', revs):
> +            raise error.Abort(_("cannot change branch of a merge commit"))
> +        if repo.revs('obsolete() and %ld', revs):

Might be helpful to precise which changeset is a merge commit.

> cmdutil.py:733
> +        if repo.revs('obsolete() and %ld', revs):
> +            raise error.Abort(_("cannot change branch of a obsolete changeset"))
> +        heads = repo.revs('head() and %ld', revs)

Might be helpful to precise which changeset is am obsolete commit.

> test-branch-change.t:143
> +  $ hg bookmark b1
> +  $ hg glog -r .
> +  @  6:7c1991464886 Added e

You should update the revset to use the same as the next glog call `(.^)::`

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce
Cc: durin42, krbullock, yuja, indygreg, lothiraldan, ryanmce, dlax, mercurial-devel
phabricator - Jan. 18, 2018, 7:29 p.m.
martinvonz added inline comments.

INLINE COMMENTS

> cmdutil.py:720
> +    with repo.wlock(), repo.lock(), repo.transaction('branches'):
> +        # abort incase in uncommitted merger of dirty wdir
> +        bailifchanged(repo)

"in case of uncommited merge or dirty wdir"? (note that there are 4 fixes there)

> cmdutil.py:722
> +        bailifchanged(repo)
> +        revs = scmutil.revrange(repo, revs)
> +        roots = repo.revs('roots(%ld)', revs)

may want to check if revs is empty (e.g. `hg branch -r 'draft() - all()' foo`)

> cmdutil.py:728
> +        root = repo[roots.first()]
> +        if root.phase() <= phases.public:
> +            raise error.Abort(_("cannot change branch of public revisions"))

maybe `if not root.mutable():`

> cmdutil.py:737-741
> +        for r in heads:
> +            if not repo[r].children():
> +                headcount += 1
> +        if headcount < 1:
> +            raise error.Abort(_("cannot change branch in middle of a stack"))

children() can be expensive because it iterates over all later revisions (in revlog order). So when you want to call children() for many revisions (especially old revisions), it can be better to do a single scan. This *might* be better:

  if repo.revs('(%ld::head()) - %ld', heads, heads):
    raise error.Abort(_("cannot change branch in middle of a stack"))

I'm not sure it will be, or if it's even correct, so please test (or just tell me that this command will be used rarely enough that no one cares).

> commands.py:1051-1056
>              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"))

Does this need to be modified to work with --rev? I've never used `hg branch`, but it sounds like `hg branch foo` (without --force) is supposed to fail if `foo` is already taken unless one of the working directory parents already has that name. Should we do the corresponding check for --rev? If not, it seems like we should at least *not* do the check (in its current form) when --rev is given. If I'm doing `hg branch --rev deadbeef foo`, it doesn't seem relevant what the branch names in my working directory parents are. Would be good to add a test case or two for this too.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce
Cc: martinvonz, durin42, krbullock, yuja, indygreg, lothiraldan, ryanmce, dlax, mercurial-devel
phabricator - Jan. 19, 2018, 7:57 a.m.
pulkit removed subscribers: durin42, krbullock, yuja, indygreg.
pulkit marked 5 inline comments as done.
pulkit added inline comments.

INLINE COMMENTS

> martinvonz wrote in commands.py:1051-1056
> Does this need to be modified to work with --rev? I've never used `hg branch`, but it sounds like `hg branch foo` (without --force) is supposed to fail if `foo` is already taken unless one of the working directory parents already has that name. Should we do the corresponding check for --rev? If not, it seems like we should at least *not* do the check (in its current form) when --rev is given. If I'm doing `hg branch --rev deadbeef foo`, it doesn't seem relevant what the branch names in my working directory parents are. Would be good to add a test case or two for this too.

This is a very basic and experimental implementation, hence I have changed the logic to not accept a new name if any such name already exists. Test cases are also added.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce
Cc: martinvonz, lothiraldan, ryanmce, dlax, mercurial-devel
phabricator - Jan. 19, 2018, 2:31 p.m.
pulkit added inline comments.

INLINE COMMENTS

> cmdutil.py:739
> +        if repo.revs('(%ld::head()) - %ld', revs, revs):
> +            raise error.Abort(_("cannot change branch in middle of a stack"))
> +

After some more testing, I found this is not correct and raise the error even in cases when it should not. For example:

o  3

| o  2 |
| /    |
|

o  1
o  0

Here if I want to change the branch of 1::2, this will raise error however this should be completely fine if we have allowunstable enabled. So in next version, I am going to add a test for allowunstable and if that's enabled, allow to change branch of 1::2.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce, lothiraldan
Cc: martinvonz, lothiraldan, ryanmce, dlax, mercurial-devel
phabricator - Jan. 19, 2018, 7:56 p.m.
durin42 added a comment.


  Accepting this since it seems generally liked and is marked experimental. We can always rip it out if it turns out to be a mistake.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D1074

To: pulkit, #hg-reviewers, dlax, ryanmce, lothiraldan, durin42
Cc: martinvonz, lothiraldan, ryanmce, dlax, mercurial-devel

Patch

diff --git a/tests/test-completion.t b/tests/test-completion.t
--- a/tests/test-completion.t
+++ b/tests/test-completion.t
@@ -239,7 +239,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
diff --git a/tests/test-branch-change.t b/tests/test-branch-change.t
new file mode 100644
--- /dev/null
+++ b/tests/test-branch-change.t
@@ -0,0 +1,317 @@ 
+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 f g h; do echo foo >> $ch; hg ci -Aqm "Added "$ch; done
+  $ hg glog
+  @  7:ec2426147f0e Added h
+  |  default ()
+  o  6:87d6d6676308 Added g
+  |  default ()
+  o  5:825660c69f0c Added f
+  |  default ()
+  o  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                        7:ec2426147f0e
+
+Try without passing a new branch name
+-----------------------------------------
+
+  $ hg branch -r 5::7
+  abort: no branch name specified for the revisions
+  [255]
+
+Setting an invalid branch name
+-------------------------------------
+
+  $ hg branch -r 5::7 a:b
+  abort: ':' cannot be used in a name
+  [255]
+  $ hg branch -r 5::7 tip
+  abort: the name 'tip' is reserved
+  [255]
+  $ hg branch -r 5::7 1234
+  abort: cannot use an integer as a name
+  [255]
+
+Change on non-linear set of commits
+--------------------------------------------
+
+  $ hg branch -r 4 -r 6 foo
+  abort: cannot change branch of non-linear revisions
+  [255]
+
+Change in between the stack (linear commits)
+------------------------------------------------------
+
+  $ hg branch -r 4::6 foo
+  abort: cannot change branch in betwen the stack
+  [255]
+
+Changing branch on linear set of commits from head
+--------------------------------------------------
+
+Without obsmarkers
+
+  $ hg branch -r 5::7 foo --config experimental.evolution=!
+  changed branch on 3 changesets
+  saved backup bundle to $TESTTMP/repo/.hg/strip-backup/825660c69f0c-ce9f7a94-branch-change.hg (glob)
+  $ hg glog
+  @  7:c23036697d1b Added h
+  |  foo ()
+  o  6:a53c3f56770a Added g
+  |  foo ()
+  o  5:ff1da3b38f9e Added f
+  |  foo ()
+  o  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
+  foo                            7:c23036697d1b
+  default                        4:aa98ab95a928 (inactive)
+
+With obsmarkers
+
+  $ hg branch -r 5::7 bar
+  changed branch on 3 changesets
+  $ hg glog
+  @  10:e6dd2bf0e93e Added h
+  |  bar ()
+  o  9:b71d0e6b76ec Added g
+  |  bar ()
+  o  8:e47e2354372c Added f
+  |  bar ()
+  o  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
+  bar                           10:e6dd2bf0e93e
+  default                        4:aa98ab95a928 (inactive)
+
+Change branch name to an existing branch
+-------------------------------------------
+
+  $ hg branch -r . default
+  abort: a branch of the same name already exists
+  (use 'hg update' to switch to it)
+  [255]
+
+Make sure bookmark movement is correct
+----------------------------------------
+
+  $ hg bookmark b1
+  $ hg glog -r .
+  @  10:e6dd2bf0e93e Added h
+  |  bar (b1)
+  ~
+  $ hg branch -r '(.^)::' foo
+  changed branch on 2 changesets
+  $ hg glog -r .
+  @  12:03fd98f490cd Added h
+  |  foo (b1)
+  ~
+  $ hg glog
+  @  12:03fd98f490cd Added h
+  |  foo (b1)
+  o  11:6bcbfdc170f7 Added g
+  |  foo ()
+  o  8:e47e2354372c Added f
+  |  bar ()
+  o  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 ()
+
+Make sure phase handling is correct
+------------------------------------
+
+  $ echo foo >> bar
+  $ hg ci -Aqm "added bar" --secret
+  $ hg glog -r .
+  @  13:f11139b5413b added bar
+  |  foo (b1)
+  ~
+  $ hg branch -r . secret
+  changed branch on 1 changesets
+  $ hg phase -r .
+  14: secret
+  $ hg branches
+  secret                        14:42e97792ed5d
+  foo                           12:03fd98f490cd (inactive)
+  bar                            8:e47e2354372c (inactive)
+  default                        4:aa98ab95a928 (inactive)
+  $ hg branch
+  secret
+
+Changing branch of another head, different from one on which we are
+-------------------------------------------------------------------
+
+  $ hg rebase -s 3 -d 1 -q --keepbranches
+  $ hg glog
+  @  20:4312b52874e6 added bar
+  |  secret (b1)
+  o  19:d8a0d829626c Added h
+  |  foo ()
+  o  18:a3992cbc5da1 Added g
+  |  foo ()
+  o  17:271f592ffeb1 Added f
+  |  bar ()
+  o  16:21bf4f045390 Added e
+  |  default ()
+  o  15:a032b5424026 Added d
+  |  default ()
+  | o  2:28ad74487de9 Added c
+  |/   default ()
+  o  1:29becc82797a Added b
+  |  default ()
+  o  0:18d04c59bb5d Added a
+     default ()
+
+  $ hg branches
+  secret                        20:4312b52874e6
+  default                       16:21bf4f045390
+  foo                           19:d8a0d829626c (inactive)
+  bar                           17:271f592ffeb1 (inactive)
+  $ hg branch
+  secret
+
+  $ hg branch -r 2 foobar
+  changed branch on 1 changesets
+  $ hg glog
+  o  21:13c2545aa399 Added c
+  |  foobar ()
+  | @  20:4312b52874e6 added bar
+  | |  secret (b1)
+  | o  19:d8a0d829626c Added h
+  | |  foo ()
+  | o  18:a3992cbc5da1 Added g
+  | |  foo ()
+  | o  17:271f592ffeb1 Added f
+  | |  bar ()
+  | o  16:21bf4f045390 Added e
+  | |  default ()
+  | o  15:a032b5424026 Added d
+  |/   default ()
+  o  1:29becc82797a Added b
+  |  default ()
+  o  0:18d04c59bb5d Added a
+     default ()
+
+  $ hg branches
+  foobar                        21:13c2545aa399
+  secret                        20:4312b52874e6
+  foo                           19:d8a0d829626c (inactive)
+  bar                           17:271f592ffeb1 (inactive)
+  default                       16:21bf4f045390 (inactive)
+The current branch must be preserved
+  $ hg branch
+  secret
+
+Changing branch on mutliple heads at once
+-----------------------------------------
+
+  $ hg branch -r 1: wat
+  changed branch on 8 changesets
+  $ hg glog
+  o  29:96deb8de5734 Added c
+  |  wat ()
+  | @  28:90f2960c3410 added bar
+  | |  wat (b1)
+  | o  27:a285fc5cd0b9 Added h
+  | |  wat ()
+  | o  26:6906ec2d36a8 Added g
+  | |  wat ()
+  | o  25:00fbab1bf47f Added f
+  | |  wat ()
+  | o  24:1a764b704348 Added e
+  | |  wat ()
+  | o  23:5d2e3635fbc0 Added d
+  |/   wat ()
+  o  22:aa56aac9964f Added b
+  |  wat ()
+  o  0:18d04c59bb5d Added a
+     default ()
+  $ hg branches
+  wat                           29:96deb8de5734
+  default                        0:18d04c59bb5d (inactive)
+
+  $ hg branch
+  wat
+
+Changing branch on public changeset
+-----------------------------------
+
+  $ hg phase -r 29 -p
+  $ hg branch -r 29 stable
+  abort: cannot change branch of public revisions
+  [255]
+
+Changing to same branch name is no-op
+------------------------------------
+
+  $ hg branch -r 23::28 wat
+  changed branch on 0 changesets
+  $ hg glog
+  o  29:96deb8de5734 Added c
+  |  wat ()
+  | @  28:90f2960c3410 added bar
+  | |  wat (b1)
+  | o  27:a285fc5cd0b9 Added h
+  | |  wat ()
+  | o  26:6906ec2d36a8 Added g
+  | |  wat ()
+  | o  25:00fbab1bf47f Added f
+  | |  wat ()
+  | o  24:1a764b704348 Added e
+  | |  wat ()
+  | o  23:5d2e3635fbc0 Added d
+  |/   wat ()
+  o  22:aa56aac9964f Added b
+  |  wat ()
+  o  0:18d04c59bb5d Added a
+     default ()
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -993,7 +993,8 @@ 
 @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
@@ -1025,10 +1026,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
 
@@ -1045,7 +1049,9 @@ 
                                      # i18n: "it" refers to an existing branch
                                      hint=_("use 'hg update' to switch to it"))
             scmutil.checknewlabel(repo, label, 'branch')
-            if True:
+            if revs:
+                cmdutil.changebranch(ui, repo, revs, label)
+            else:
                 repo.dirstate.setbranch(label)
                 ui.status(_('marked working directory as branch %s\n') % label)
 
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -35,6 +35,7 @@ 
     obsolete,
     patch,
     pathutil,
+    phases,
     pycompat,
     registrar,
     revlog,
@@ -733,6 +734,87 @@ 
 
     raise error.UnknownCommand(cmd, allcmds)
 
+def changebranch(ui, repo, revs, label):
+    """ Change the branch name of given revs to label """
+
+    revs = scmutil.revrange(repo, revs)
+    roots = repo.revs('roots(%ld)', revs)
+    if len(roots) > 1:
+        raise error.Abort(_("cannot change branch of non-linear revisions"))
+    root = repo[roots.first()]
+    if root.phase() <= phases.public:
+        raise error.Abort(_("cannot change branch of public revisions"))
+    heads = repo.revs('head() and %ld', revs)
+    if len(heads) < 1:
+        raise error.Abort(_("cannot change branch in betwen the stack"))
+
+    replacements = {}
+    rewrote = 0
+    with repo.wlock(), repo.lock(), repo.transaction('branches'):
+        # 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'" % (
+                            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)
+            # phase handling
+            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))
+            rewrote += 1
+
+        # 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") % rewrote)
+
 def findrepo(p):
     while not os.path.isdir(os.path.join(p, ".hg")):
         oldp, p = p, os.path.dirname(p)