rewriteutil.py
272 lines
| 9.0 KiB
| text/x-python
|
PythonLexer
/ mercurial / rewriteutil.py
Pulkit Goyal
|
r35242 | # rewriteutil.py - utility functions for rewriting changesets | ||
# | ||||
# Copyright 2017 Octobus <contact@octobus.net> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
Matt Harbison
|
r52756 | from __future__ import annotations | ||
Pulkit Goyal
|
r35242 | |||
Matt Harbison
|
r45993 | import re | ||
Pulkit Goyal
|
r35243 | from .i18n import _ | ||
Joerg Sonnenberger
|
r46729 | from .node import ( | ||
hex, | ||||
nullrev, | ||||
) | ||||
Pulkit Goyal
|
r35243 | |||
Pulkit Goyal
|
r35242 | from . import ( | ||
Pulkit Goyal
|
r35243 | error, | ||
Martin von Zweigbergk
|
r47835 | node, | ||
Pulkit Goyal
|
r35242 | obsolete, | ||
Matt Harbison
|
r45993 | obsutil, | ||
Pulkit Goyal
|
r35242 | revset, | ||
Matt Harbison
|
r45993 | scmutil, | ||
Pulkit Goyal
|
r35242 | ) | ||
Augie Fackler
|
r43345 | |||
Matt Harbison
|
r45996 | NODE_RE = re.compile(br'\b[0-9a-f]{6,64}\b') | ||
Matt Harbison
|
r45993 | |||
r51536 | # set of extra entry that should survive a rebase-like operation, extensible by extensions | |||
retained_extras_on_rebase = { | ||||
b'source', | ||||
b'intermediate-source', | ||||
} | ||||
def preserve_extras_on_rebase(old_ctx, new_extra): | ||||
"""preserve the relevant `extra` entry from old_ctx on rebase-like operation""" | ||||
new_extra.update( | ||||
(key, value) | ||||
for key, value in old_ctx.extra().items() | ||||
if key in retained_extras_on_rebase | ||||
) | ||||
Matt Harbison
|
r45993 | |||
Martin von Zweigbergk
|
r47835 | def _formatrevs(repo, revs, maxrevs=4): | ||
"""returns a string summarizing revisions in a decent size | ||||
If there are few enough revisions, we list them all. Otherwise we display a | ||||
summary of the form: | ||||
1ea73414a91b and 5 others | ||||
""" | ||||
tonode = repo.changelog.node | ||||
numrevs = len(revs) | ||||
if numrevs < maxrevs: | ||||
shorts = [node.short(tonode(r)) for r in revs] | ||||
summary = b', '.join(shorts) | ||||
else: | ||||
first = revs.first() | ||||
summary = _(b'%s and %d others') | ||||
summary %= (node.short(tonode(first)), numrevs - 1) | ||||
return summary | ||||
Sushil khanchi
|
r49848 | def precheck(repo, revs, action=b'rewrite', check_divergence=True): | ||
Pulkit Goyal
|
r35243 | """check if revs can be rewritten | ||
action is used to control the error message. | ||||
Sushil khanchi
|
r49848 | check_divergence allows skipping the divergence checks in cases like adding | ||
a prune marker (A, ()) to obsstore (which can't be diverging). | ||||
Pulkit Goyal
|
r35243 | Make sure this function is called after taking the lock. | ||
""" | ||||
Joerg Sonnenberger
|
r46729 | if nullrev in revs: | ||
Martin von Zweigbergk
|
r47779 | msg = _(b"cannot %s the null revision") % action | ||
Augie Fackler
|
r43347 | hint = _(b"no changeset checked out") | ||
Martin von Zweigbergk
|
r46457 | raise error.InputError(msg, hint=hint) | ||
Pulkit Goyal
|
r35243 | |||
r51821 | if any(hasattr(r, 'rev') for r in revs): | |||
Martin von Zweigbergk
|
r47780 | repo.ui.develwarn(b"rewriteutil.precheck called with ctx not revs") | ||
revs = (r.rev() for r in revs) | ||||
Pulkit Goyal
|
r35243 | if len(repo[None].parents()) > 1: | ||
Martin von Zweigbergk
|
r47784 | raise error.StateError( | ||
_(b"cannot %s changesets while merging") % action | ||||
) | ||||
Pulkit Goyal
|
r35243 | |||
Augie Fackler
|
r43347 | publicrevs = repo.revs(b'%ld and public()', revs) | ||
Pulkit Goyal
|
r35243 | if publicrevs: | ||
Martin von Zweigbergk
|
r47835 | summary = _formatrevs(repo, publicrevs) | ||
msg = _(b"cannot %s public changesets: %s") % (action, summary) | ||||
Augie Fackler
|
r43347 | hint = _(b"see 'hg help phases' for details") | ||
Martin von Zweigbergk
|
r46457 | raise error.InputError(msg, hint=hint) | ||
Pulkit Goyal
|
r35243 | |||
newunstable = disallowednewunstable(repo, revs) | ||||
if newunstable: | ||||
Martin von Zweigbergk
|
r47782 | hint = _(b"see 'hg help evolution.instability'") | ||
raise error.InputError( | ||||
Martin von Zweigbergk
|
r47836 | _(b"cannot %s changeset, as that will orphan %d descendants") | ||
% (action, len(newunstable)), | ||||
hint=hint, | ||||
Martin von Zweigbergk
|
r47782 | ) | ||
Pulkit Goyal
|
r35243 | |||
Sushil khanchi
|
r49848 | if not check_divergence: | ||
return | ||||
Martin von Zweigbergk
|
r47784 | if not obsolete.isenabled(repo, obsolete.allowdivergenceopt): | ||
new_divergence = _find_new_divergence(repo, revs) | ||||
if new_divergence: | ||||
local_ctx, other_ctx, base_ctx = new_divergence | ||||
msg = _( | ||||
b'cannot %s %s, as that creates content-divergence with %s' | ||||
) % ( | ||||
action, | ||||
local_ctx, | ||||
other_ctx, | ||||
) | ||||
if local_ctx.rev() != base_ctx.rev(): | ||||
msg += _(b', from %s') % base_ctx | ||||
if repo.ui.verbose: | ||||
if local_ctx.rev() != base_ctx.rev(): | ||||
msg += _( | ||||
b'\n changeset %s is a successor of ' b'changeset %s' | ||||
) % (local_ctx, base_ctx) | ||||
msg += _( | ||||
b'\n changeset %s already has a successor in ' | ||||
b'changeset %s\n' | ||||
b' rewriting changeset %s would create ' | ||||
b'"content-divergence"\n' | ||||
b' set experimental.evolution.allowdivergence=True to ' | ||||
b'skip this check' | ||||
) % (base_ctx, other_ctx, local_ctx) | ||||
Martin von Zweigbergk
|
r48166 | raise error.InputError( | ||
msg, | ||||
hint=_( | ||||
b"see 'hg help evolution.instability' for details on content-divergence" | ||||
), | ||||
) | ||||
Martin von Zweigbergk
|
r47784 | else: | ||
raise error.InputError( | ||||
Martin von Zweigbergk
|
r48045 | msg, | ||
hint=_( | ||||
b"add --verbose for details or see " | ||||
b"'hg help evolution.instability'" | ||||
), | ||||
Martin von Zweigbergk
|
r47784 | ) | ||
Augie Fackler
|
r43345 | |||
Pulkit Goyal
|
r35242 | def disallowednewunstable(repo, revs): | ||
"""Checks whether editing the revs will create new unstable changesets and | ||||
are we allowed to create them. | ||||
To allow new unstable changesets, set the config: | ||||
`experimental.evolution.allowunstable=True` | ||||
""" | ||||
allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt) | ||||
if allowunstable: | ||||
return revset.baseset() | ||||
Augie Fackler
|
r43347 | return repo.revs(b"(%ld::) - %ld", revs, revs) | ||
Manuel Jacob
|
r45682 | |||
Martin von Zweigbergk
|
r47784 | def _find_new_divergence(repo, revs): | ||
obsrevs = repo.revs(b'%ld and obsolete()', revs) | ||||
for r in obsrevs: | ||||
div = find_new_divergence_from(repo, repo[r]) | ||||
if div: | ||||
Martin von Zweigbergk
|
r48348 | return (repo[r], repo[div[0]], repo.unfiltered()[div[1]]) | ||
Martin von Zweigbergk
|
r47784 | return None | ||
def find_new_divergence_from(repo, ctx): | ||||
"""return divergent revision if rewriting an obsolete cset (ctx) will | ||||
create divergence | ||||
Returns (<other node>, <common ancestor node>) or None | ||||
""" | ||||
if not ctx.obsolete(): | ||||
return None | ||||
# We need to check two cases that can cause divergence: | ||||
# case 1: the rev being rewritten has a non-obsolete successor (easily | ||||
# detected by successorssets) | ||||
sset = obsutil.successorssets(repo, ctx.node()) | ||||
if sset: | ||||
return (sset[0][0], ctx.node()) | ||||
else: | ||||
# case 2: one of the precursors of the rev being revived has a | ||||
# non-obsolete successor (we need divergentsets for this) | ||||
divsets = obsutil.divergentsets(repo, ctx) | ||||
if divsets: | ||||
nsuccset = divsets[0][b'divergentnodes'] | ||||
prec = divsets[0][b'commonpredecessor'] | ||||
return (nsuccset[0], prec) | ||||
return None | ||||
Manuel Jacob
|
r45682 | def skip_empty_successor(ui, command): | ||
empty_successor = ui.config(b'rewrite', b'empty-successor') | ||||
if empty_successor == b'skip': | ||||
return True | ||||
elif empty_successor == b'keep': | ||||
return False | ||||
else: | ||||
raise error.ConfigError( | ||||
_( | ||||
b"%s doesn't know how to handle config " | ||||
b"rewrite.empty-successor=%s (only 'skip' and 'keep' are " | ||||
b"supported)" | ||||
) | ||||
% (command, empty_successor) | ||||
) | ||||
Matt Harbison
|
r45993 | |||
Matt Harbison
|
r45994 | def update_hash_refs(repo, commitmsg, pending=None): | ||
Matt Harbison
|
r45993 | """Replace all obsolete commit hashes in the message with the current hash. | ||
If the obsolete commit was split or is divergent, the hash is not replaced | ||||
as there's no way to know which successor to choose. | ||||
Matt Harbison
|
r45994 | |||
For commands that update a series of commits in the current transaction, the | ||||
new obsolete markers can be considered by setting ``pending`` to a mapping | ||||
of ``pending[oldnode] = [successor_node1, successor_node2,..]``. | ||||
Matt Harbison
|
r45993 | """ | ||
Matt Harbison
|
r45994 | if not pending: | ||
pending = {} | ||||
Matt Harbison
|
r45993 | cache = {} | ||
Matt Harbison
|
r45996 | hashes = re.findall(NODE_RE, commitmsg) | ||
Matt Harbison
|
r45993 | unfi = repo.unfiltered() | ||
Matt Harbison
|
r45996 | for h in hashes: | ||
Augie Fackler
|
r48591 | try: | ||
fullnode = scmutil.resolvehexnodeidprefix(unfi, h) | ||||
Kyle Lippincott
|
r49941 | except (error.WdirUnsupported, error.AmbiguousPrefixLookupError): | ||
# Someone has an fffff... or some other prefix that's ambiguous in a | ||||
# commit message we're rewriting. Don't try rewriting that. | ||||
Augie Fackler
|
r48591 | continue | ||
Matt Harbison
|
r45993 | if fullnode is None: | ||
continue | ||||
ctx = unfi[fullnode] | ||||
if not ctx.obsolete(): | ||||
Matt Harbison
|
r45994 | successors = pending.get(fullnode) | ||
if successors is None: | ||||
continue | ||||
# obsutil.successorssets() returns a list of list of nodes | ||||
successors = [successors] | ||||
else: | ||||
successors = obsutil.successorssets(repo, ctx.node(), cache=cache) | ||||
Matt Harbison
|
r45993 | |||
# We can't make any assumptions about how to update the hash if the | ||||
# cset in question was split or diverged. | ||||
if len(successors) == 1 and len(successors[0]) == 1: | ||||
Matt Harbison
|
r46303 | successor = successors[0][0] | ||
if successor is not None: | ||||
Joerg Sonnenberger
|
r46729 | newhash = hex(successor) | ||
Matt Harbison
|
r46303 | commitmsg = commitmsg.replace(h, newhash[: len(h)]) | ||
else: | ||||
repo.ui.note( | ||||
_( | ||||
b'The stale commit message reference to %s could ' | ||||
b'not be updated\n(The referenced commit was dropped)\n' | ||||
) | ||||
% h | ||||
) | ||||
Matt Harbison
|
r45993 | else: | ||
repo.ui.note( | ||||
_( | ||||
b'The stale commit message reference to %s could ' | ||||
b'not be updated\n' | ||||
) | ||||
Matt Harbison
|
r45996 | % h | ||
Matt Harbison
|
r45993 | ) | ||
return commitmsg | ||||