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,95 @@ 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 incase in uncommitted merger of dirty wdir + bailifchanged(repo) + revs = scmutil.revrange(repo, revs) + roots = repo.revs('roots(%ld)', revs) + rootscount = len(roots) + if rootscount > 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")) + if repo.revs('merge() and %ld', revs): + raise error.Abort(_("cannot change branch of a merge commit")) + heads = repo.revs('head() and %ld', revs) + headcount = 0 + # make sure only topological heads + 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")) + + 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) diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -993,7 +993,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 @@ -1025,10 +1027,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 +1050,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,271 @@ +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 in middle of a stack + [255] + +Change with dirty working directory + + $ echo bar > a + $ hg branch -r . foo + abort: uncommitted changes + [255] + + $ hg revert --all + reverting a + +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 + (use 'hg update' to switch to it) + [255] + +Changing on a branch head which is not topological head + + $ hg branch -r 2 stable + abort: cannot change branch in middle of a stack + [255] + +Make sure bookmark movement is correct + + $ hg bookmark b1 + $ hg glog -r . + @ 6:7c1991464886 Added e + | bar (b1) + ~ + + $ hg branch -r '(.^)::' foo --debug + changing branch of '1ea05e93925f806d875a2163f9b76764be644636' from 'bar' to 'foo' + committing files: + d + committing manifest + committing changelog + new node id is 974cbad5a704c10bec744568fec0f2fd2dc000e4 + changing branch of '7c19914648869f5b02fc7fed31ddee9783fdd680' from 'bar' to 'foo' + committing files: + e + committing manifest + committing changelog + new node id is 916bb75f2f1dc742cbbec815d2f89b733f5832b2 + moving bookmarks ['b1'] from 7c19914648869f5b02fc7fed31ddee9783fdd680 to 916bb75f2f1dc742cbbec815d2f89b733f5832b2 + resolving manifests + branchmerge: False, force: False, partial: False + ancestor: 7c1991464886, local: 7c1991464886+, remote: 916bb75f2f1d + changed branch on 2 changesets + updating the branch cache + invalid branchheads cache (served): tip differs + + $ hg glog -r '(.^)::' + @ 8:916bb75f2f1d Added e + | foo (b1) + o 7:974cbad5a704 Added d + | foo () + ~ + +Make sure phase handling is correct + + $ echo foo >> bar + $ hg ci -Aqm "added bar" --secret + $ hg glog -r . + @ 9:bfcc2c6eb9bd added bar + | foo (b1) + ~ + $ hg branch -r . secret + changed branch on 1 changesets + $ hg phase -r . + 10: secret + + $ hg branches + secret 10:9830470c2d8a + foo 8:916bb75f2f1d (inactive) + default 2:28ad74487de9 (inactive) + $ hg branch + secret + +Changing branch of another head, different from one on which we are + + $ hg rebase -s 7 -d 1 -q --keepbranches + $ hg glog + @ 13:d5165df2f9d7 added bar + | secret (b1) + o 12:0a38d1e785fe Added e + | foo () + o 11:398bbf82593d Added d + | foo () + | o 2:28ad74487de9 Added c + |/ default () + o 1:29becc82797a Added b + | default () + o 0:18d04c59bb5d Added a + default () + + $ hg branch + secret + + $ hg branch -r 2 foobar + changed branch on 1 changesets + +The current branch must be preserved + $ hg branch + secret + +Changing branch on multiple heads at once + + $ hg branch -r 0: wat + changed branch on 6 changesets + $ hg glog + o 20:ad0bdd3c3224 Added c + | wat () + | @ 19:518b090e6153 added bar + | | wat (b1) + | o 18:b5389ad3cf87 Added e + | | wat () + | o 17:201f4fbe223e Added d + |/ wat () + o 16:ed6f19fca433 Added b + | wat () + o 15:4c74b7b49c38 Added a + wat () + $ hg branches + wat 20:ad0bdd3c3224 + + $ hg branch + wat + +Changing to same branch name is no-op + + $ hg branch -r 16::19 wat + changed branch on 0 changesets + $ hg glog + o 20:ad0bdd3c3224 Added c + | wat () + | @ 19:518b090e6153 added bar + | | wat (b1) + | o 18:b5389ad3cf87 Added e + | | wat () + | o 17:201f4fbe223e Added d + |/ wat () + o 16:ed6f19fca433 Added b + | wat () + o 15:4c74b7b49c38 Added a + wat () + +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 '(.^)::' stable + abort: cannot change branch of a merge commit + [255] + +Changing branch on public changeset + + $ hg phase -r 21 -p + $ hg branch -r 21 stable + abort: cannot change branch of public revisions + [255] 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