split.py
203 lines
| 6.7 KiB
| text/x-python
|
PythonLexer
/ hgext / split.py
Jun Wu
|
r35471 | # split.py - split a changeset into smaller ones | ||
# | ||||
# Copyright 2015 Laurent Charignon <lcharignon@fb.com> | ||||
# Copyright 2017 Facebook, Inc. | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
"""command to split a changeset into smaller ones (EXPERIMENTAL)""" | ||||
Matt Harbison
|
r52756 | from __future__ import annotations | ||
Jun Wu
|
r35471 | |||
from mercurial.i18n import _ | ||||
from mercurial.node import ( | ||||
Joerg Sonnenberger
|
r47601 | nullrev, | ||
Jun Wu
|
r35471 | short, | ||
) | ||||
from mercurial import ( | ||||
bookmarks, | ||||
cmdutil, | ||||
commands, | ||||
error, | ||||
hg, | ||||
Martin von Zweigbergk
|
r48928 | logcmdutil, | ||
Jun Wu
|
r35471 | registrar, | ||
revsetlang, | ||||
Martin von Zweigbergk
|
r44387 | rewriteutil, | ||
Jun Wu
|
r35471 | scmutil, | ||
Martin von Zweigbergk
|
r47492 | util, | ||
Jun Wu
|
r35471 | ) | ||
# allow people to use split without explicitly enabling rebase extension | ||||
Augie Fackler
|
r43346 | from . import rebase | ||
Jun Wu
|
r35471 | |||
cmdtable = {} | ||||
command = registrar.command(cmdtable) | ||||
# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' 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. | ||||
Augie Fackler
|
r43347 | testedwith = b'ships-with-hg-core' | ||
Jun Wu
|
r35471 | |||
Augie Fackler
|
r43346 | |||
@command( | ||||
Augie Fackler
|
r43347 | b'split', | ||
Augie Fackler
|
r43346 | [ | ||
Augie Fackler
|
r43347 | (b'r', b'rev', b'', _(b"revision to split"), _(b'REV')), | ||
(b'', b'rebase', True, _(b'rebase descendants after split')), | ||||
Augie Fackler
|
r43346 | ] | ||
+ cmdutil.commitopts2, | ||||
Augie Fackler
|
r43347 | _(b'hg split [--no-rebase] [[-r] REV]'), | ||
Augie Fackler
|
r43346 | helpcategory=command.CATEGORY_CHANGE_MANAGEMENT, | ||
helpbasic=True, | ||||
) | ||||
Jun Wu
|
r35471 | def split(ui, repo, *revs, **opts): | ||
"""split a changeset into smaller ones | ||||
Repeatedly prompt changes and commit message for new changesets until there | ||||
is nothing left in the original changeset. | ||||
If --rev was not given, split the working directory parent. | ||||
By default, rebase connected non-obsoleted descendants onto the new | ||||
changeset. Use --no-rebase to avoid the rebase. | ||||
""" | ||||
revlist = [] | ||||
Matt Harbison
|
r51777 | if opts.get('rev'): | ||
revlist.append(opts.get('rev')) | ||||
Jun Wu
|
r35471 | revlist.extend(revs) | ||
Martin von Zweigbergk
|
r47492 | with repo.wlock(), repo.lock(): | ||
tr = repo.transaction(b'split') | ||||
# If the rebase somehow runs into conflicts, make sure | ||||
# we close the transaction so the user can continue it. | ||||
with util.acceptintervention(tr): | ||||
Martin von Zweigbergk
|
r48928 | revs = logcmdutil.revrange(repo, revlist or [b'.']) | ||
Martin von Zweigbergk
|
r47492 | if len(revs) > 1: | ||
raise error.InputError(_(b'cannot split multiple revisions')) | ||||
Jun Wu
|
r35471 | |||
Martin von Zweigbergk
|
r47492 | rev = revs.first() | ||
Joerg Sonnenberger
|
r47601 | # Handle nullrev specially here (instead of leaving for precheck() | ||
Martin von Zweigbergk
|
r47492 | # below) so we get a nicer message and error code. | ||
Joerg Sonnenberger
|
r47601 | if rev is None or rev == nullrev: | ||
Martin von Zweigbergk
|
r47492 | ui.status(_(b'nothing to split\n')) | ||
return 1 | ||||
Joerg Sonnenberger
|
r47601 | ctx = repo[rev] | ||
Martin von Zweigbergk
|
r47492 | if ctx.node() is None: | ||
raise error.InputError(_(b'cannot split working directory')) | ||||
Jun Wu
|
r35471 | |||
Matt Harbison
|
r51777 | if opts.get('rebase'): | ||
Martin von Zweigbergk
|
r47492 | # Skip obsoleted descendants and their descendants so the rebase | ||
# won't cause conflicts for sure. | ||||
descendants = list(repo.revs(b'(%d::) - (%d)', rev, rev)) | ||||
torebase = list( | ||||
repo.revs( | ||||
b'%ld - (%ld & obsolete())::', descendants, descendants | ||||
) | ||||
Augie Fackler
|
r43346 | ) | ||
Martin von Zweigbergk
|
r47492 | else: | ||
torebase = [] | ||||
rewriteutil.precheck(repo, [rev] + torebase, b'split') | ||||
Jun Wu
|
r35471 | |||
Martin von Zweigbergk
|
r47492 | if len(ctx.parents()) > 1: | ||
raise error.InputError(_(b'cannot split a merge changeset')) | ||||
Jun Wu
|
r35471 | |||
Martin von Zweigbergk
|
r47492 | cmdutil.bailifchanged(repo) | ||
Jun Wu
|
r35471 | |||
Martin von Zweigbergk
|
r47492 | # Deactivate bookmark temporarily so it won't get moved | ||
# unintentionally | ||||
bname = repo._activebookmark | ||||
if bname and repo._bookmarks[bname] != ctx.node(): | ||||
bookmarks.deactivate(repo) | ||||
Jun Wu
|
r35471 | |||
Martin von Zweigbergk
|
r47492 | wnode = repo[b'.'].node() | ||
top = None | ||||
try: | ||||
Matt Harbison
|
r51777 | top = dosplit(ui, repo, tr, ctx, **opts) | ||
Martin von Zweigbergk
|
r47492 | finally: | ||
# top is None: split failed, need update --clean recovery. | ||||
# wnode == ctx.node(): wnode split, no need to update. | ||||
if top is None or wnode != ctx.node(): | ||||
hg.clean(repo, wnode, show_stats=False) | ||||
if bname: | ||||
bookmarks.activate(repo, bname) | ||||
if torebase and top: | ||||
dorebase(ui, repo, torebase, top) | ||||
Jun Wu
|
r35471 | |||
Augie Fackler
|
r43346 | |||
Matt Harbison
|
r51777 | def dosplit(ui, repo, tr, ctx, **opts): | ||
Augie Fackler
|
r43346 | committed = [] # [ctx] | ||
Jun Wu
|
r35471 | |||
# Set working parent to ctx.p1(), and keep working copy as ctx's content | ||||
Martin von Zweigbergk
|
r42132 | if ctx.node() != repo.dirstate.p1(): | ||
hg.clean(repo, ctx.node(), show_stats=False) | ||||
r50855 | with repo.dirstate.changing_parents(repo): | |||
Martin von Zweigbergk
|
r42132 | scmutil.movedirstate(repo, ctx.p1()) | ||
Jun Wu
|
r35471 | |||
# Any modified, added, removed, deleted result means split is incomplete | ||||
Augie Fackler
|
r44040 | def incomplete(repo): | ||
st = repo.status() | ||||
return any((st.modified, st.added, st.removed, st.deleted)) | ||||
Jun Wu
|
r35471 | |||
# Main split loop | ||||
while incomplete(repo): | ||||
if committed: | ||||
Augie Fackler
|
r43346 | header = _( | ||
Augie Fackler
|
r43347 | b'HG: Splitting %s. So far it has been split into:\n' | ||
Augie Fackler
|
r43346 | ) % short(ctx.node()) | ||
Martin von Zweigbergk
|
r46460 | # We don't want color codes in the commit message template, so | ||
# disable the label() template function while we render it. | ||||
with ui.configoverride( | ||||
{(b'templatealias', b'label(l,x)'): b"x"}, b'split' | ||||
): | ||||
for c in committed: | ||||
summary = cmdutil.format_changeset_summary(ui, c, b'split') | ||||
header += _(b'HG: - %s\n') % summary | ||||
Augie Fackler
|
r43346 | header += _( | ||
Martin von Zweigbergk
|
r43387 | b'HG: Write commit message for the next split changeset.\n' | ||
Augie Fackler
|
r43346 | ) | ||
Jun Wu
|
r35471 | else: | ||
Augie Fackler
|
r43346 | header = _( | ||
Augie Fackler
|
r43347 | b'HG: Splitting %s. Write commit message for the ' | ||
b'first split changeset.\n' | ||||
Augie Fackler
|
r43346 | ) % short(ctx.node()) | ||
opts.update( | ||||
{ | ||||
Matt Harbison
|
r51777 | 'edit': True, | ||
'interactive': True, | ||||
'message': header + ctx.description(), | ||||
Augie Fackler
|
r43346 | } | ||
) | ||||
Kyle Lippincott
|
r47685 | origctx = repo[b'.'] | ||
Matt Harbison
|
r51777 | commands.commit(ui, repo, **opts) | ||
Augie Fackler
|
r43347 | newctx = repo[b'.'] | ||
Kyle Lippincott
|
r47685 | # Ensure user didn't do a "no-op" split (such as deselecting | ||
# everything). | ||||
if origctx.node() != newctx.node(): | ||||
committed.append(newctx) | ||||
Jun Wu
|
r35471 | |||
if not committed: | ||||
Martin von Zweigbergk
|
r46488 | raise error.InputError(_(b'cannot split an empty revision')) | ||
Jun Wu
|
r35471 | |||
Kyle Lippincott
|
r47686 | if len(committed) != 1 or committed[0].node() != ctx.node(): | ||
# Ensure we don't strip a node if we produce the same commit as already | ||||
# exists | ||||
scmutil.cleanupnodes( | ||||
repo, | ||||
{ctx.node(): [c.node() for c in committed]}, | ||||
operation=b'split', | ||||
fixphase=True, | ||||
) | ||||
Jun Wu
|
r35471 | |||
return committed[-1] | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r36426 | def dorebase(ui, repo, src, destctx): | ||
Augie Fackler
|
r43346 | rebase.rebase( | ||
ui, | ||||
repo, | ||||
Augie Fackler
|
r43347 | rev=[revsetlang.formatspec(b'%ld', src)], | ||
dest=revsetlang.formatspec(b'%d', destctx.rev()), | ||||
Augie Fackler
|
r43346 | ) | ||