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, @@ -715,6 +716,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 middle of a 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) 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,6 +1049,9 @@ # i18n: "it" refers to an existing branch hint=_("use 'hg update' to switch to it")) scmutil.checknewlabel(repo, label, 'branch') + if revs: + return cmdutil.changebranch(ui, repo, revs, label) + repo.dirstate.setbranch(label) ui.status(_('marked working directory as branch %s\n') % label) 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 middle of a 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/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