phases.py
1281 lines
| 43.2 KiB
| text/x-python
|
PythonLexer
/ mercurial / phases.py
Pierre-Yves David
|
r15659 | """ Mercurial phases support code | ||
--- | ||||
Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org> | ||||
Logilab SA <contact@logilab.fr> | ||||
Augie Fackler <durin42@gmail.com> | ||||
Martin Geisler
|
r16725 | This software may be used and distributed according to the terms | ||
of the GNU General Public License version 2 or any later version. | ||||
Pierre-Yves David
|
r15659 | |||
--- | ||||
This module implements most phase logic in mercurial. | ||||
Basic Concept | ||||
============= | ||||
Martin Geisler
|
r16724 | A 'changeset phase' is an indicator that tells us how a changeset is | ||
Martin Geisler
|
r16725 | manipulated and communicated. The details of each phase is described | ||
below, here we describe the properties they have in common. | ||||
Pierre-Yves David
|
r15659 | |||
Martin Geisler
|
r16725 | Like bookmarks, phases are not stored in history and thus are not | ||
permanent and leave no audit trail. | ||||
Pierre-Yves David
|
r15659 | |||
Martin Geisler
|
r16725 | First, no changeset can be in two phases at once. Phases are ordered, | ||
so they can be considered from lowest to highest. The default, lowest | ||||
phase is 'public' - this is the normal phase of existing changesets. A | ||||
child changeset can not be in a lower phase than its parents. | ||||
Pierre-Yves David
|
r15659 | |||
These phases share a hierarchy of traits: | ||||
immutable shared | ||||
public: X X | ||||
draft: X | ||||
Pierre-Yves David
|
r15705 | secret: | ||
Pierre-Yves David
|
r15659 | |||
Martin Geisler
|
r16724 | Local commits are draft by default. | ||
Pierre-Yves David
|
r15659 | |||
Martin Geisler
|
r16724 | Phase Movement and Exchange | ||
=========================== | ||||
Pierre-Yves David
|
r15659 | |||
Martin Geisler
|
r16725 | Phase data is exchanged by pushkey on pull and push. Some servers have | ||
a publish option set, we call such a server a "publishing server". | ||||
Pushing a draft changeset to a publishing server changes the phase to | ||||
public. | ||||
Pierre-Yves David
|
r15659 | |||
A small list of fact/rules define the exchange of phase: | ||||
* old client never changes server states | ||||
* pull never changes server states | ||||
Martin Geisler
|
r16724 | * publish and old server changesets are seen as public by client | ||
Martin Geisler
|
r16725 | * any secret changeset seen in another repository is lowered to at | ||
least draft | ||||
Pierre-Yves David
|
r15659 | |||
Martin Geisler
|
r16725 | Here is the final table summing up the 49 possible use cases of phase | ||
exchange: | ||||
Pierre-Yves David
|
r15659 | |||
server | ||||
old publish non-publish | ||||
N X N D P N D P | ||||
old client | ||||
pull | ||||
N - X/X - X/D X/P - X/D X/P | ||||
X - X/X - X/D X/P - X/D X/P | ||||
push | ||||
X X/X X/X X/P X/P X/P X/D X/D X/P | ||||
new client | ||||
pull | ||||
N - P/X - P/D P/P - D/D P/P | ||||
D - P/X - P/D P/P - D/D P/P | ||||
P - P/X - P/D P/P - P/D P/P | ||||
push | ||||
D P/X P/X P/P P/P P/P D/D D/D P/P | ||||
P P/X P/X P/P P/P P/P P/P P/P P/P | ||||
Legend: | ||||
A/B = final state on client / state on server | ||||
* N = new/not present, | ||||
* P = public, | ||||
* D = draft, | ||||
Martin Geisler
|
r16725 | * X = not tracked (i.e., the old client or server has no internal | ||
way of recording the phase.) | ||||
Pierre-Yves David
|
r15659 | |||
passive = only pushes | ||||
A cell here can be read like this: | ||||
Martin Geisler
|
r16725 | "When a new client pushes a draft changeset (D) to a publishing | ||
server where it's not present (N), it's marked public on both | ||||
sides (P/P)." | ||||
Pierre-Yves David
|
r15659 | |||
Martin Geisler
|
r16724 | Note: old client behave as a publishing server with draft only content | ||
Pierre-Yves David
|
r15659 | - other people see it as public | ||
- content is pushed as draft | ||||
""" | ||||
Pierre-Yves David
|
r15417 | |||
Matt Harbison
|
r52756 | from __future__ import annotations | ||
Gregory Szorc
|
r25966 | |||
r52316 | import heapq | |||
Boris Feld
|
r34320 | import struct | ||
r52178 | import typing | |||
r52297 | import weakref | |||
r52178 | ||||
from typing import ( | ||||
Any, | ||||
Callable, | ||||
r52473 | Collection, | |||
r52178 | Dict, | |||
Iterable, | ||||
List, | ||||
Optional, | ||||
Set, | ||||
Tuple, | ||||
Matt Harbison
|
r52704 | overload, | ||
r52178 | ) | |||
Gregory Szorc
|
r25966 | |||
from .i18n import _ | ||||
from .node import ( | ||||
bin, | ||||
hex, | ||||
nullrev, | ||||
short, | ||||
Rodrigo Damazio Bovendorp
|
r44456 | wdirrev, | ||
Gregory Szorc
|
r25966 | ) | ||
from . import ( | ||||
error, | ||||
Pulkit Goyal
|
r45932 | requirements, | ||
Jun Wu
|
r31016 | smartset, | ||
FUJIWARA Katsunori
|
r31053 | txnutil, | ||
Gregory Szorc
|
r32000 | util, | ||
Gregory Szorc
|
r25966 | ) | ||
Pierre-Yves David
|
r15418 | |||
r52299 | Phaseroots = Dict[int, Set[int]] | |||
r52304 | PhaseSets = Dict[int, Set[int]] | |||
r52178 | ||||
if typing.TYPE_CHECKING: | ||||
Matt Harbison
|
r52704 | from typing_extensions import ( | ||
Literal, # py3.8+ | ||||
) | ||||
Matt Harbison
|
r47390 | from . import ( | ||
localrepo, | ||||
ui as uimod, | ||||
) | ||||
r52178 | # keeps pyflakes happy | |||
assert [uimod] | ||||
Matt Harbison
|
r47390 | Phasedefaults = List[ | ||
Callable[[localrepo.localrepository, Phaseroots], Phaseroots] | ||||
] | ||||
Augie Fackler
|
r43347 | _fphasesentry = struct.Struct(b'>i20s') | ||
Boris Feld
|
r34320 | |||
Boris Feld
|
r39333 | # record phase index | ||
r52181 | public: int = 0 | |||
draft: int = 1 | ||||
secret: int = 2 | ||||
Joerg Sonnenberger
|
r45677 | archived = 32 # non-continuous for compatibility | ||
internal = 96 # non-continuous for compatibility | ||||
allphases = (public, draft, secret, archived, internal) | ||||
trackedphases = (draft, secret, archived, internal) | ||||
r51201 | not_public_phases = trackedphases | |||
Boris Feld
|
r39333 | # record phase names | ||
Augie Fackler
|
r43347 | cmdphasenames = [b'public', b'draft', b'secret'] # known to `hg phase` command | ||
Joerg Sonnenberger
|
r45677 | phasenames = dict(enumerate(cmdphasenames)) | ||
Augie Fackler
|
r43347 | phasenames[archived] = b'archived' | ||
phasenames[internal] = b'internal' | ||||
Joerg Sonnenberger
|
r45677 | # map phase name to phase number | ||
phasenumber = {name: phase for phase, name in phasenames.items()} | ||||
# like phasenumber, but also include maps for the numeric and binary | ||||
# phase number to the phase number | ||||
phasenumber2 = phasenumber.copy() | ||||
phasenumber2.update({phase: phase for phase in phasenames}) | ||||
phasenumber2.update({b'%i' % phase: phase for phase in phasenames}) | ||||
Boris Feld
|
r39333 | # record phase property | ||
Joerg Sonnenberger
|
r45677 | mutablephases = (draft, secret, archived, internal) | ||
r52017 | relevant_mutable_phases = (draft, secret) # could be obsolete or unstable | |||
Joerg Sonnenberger
|
r45677 | remotehiddenphases = (secret, archived, internal) | ||
localhiddenphases = (internal, archived) | ||||
Pierre-Yves David
|
r15418 | |||
r51210 | all_internal_phases = tuple(p for p in allphases if p & internal) | |||
# We do not want any internal content to exit the repository, ever. | ||||
no_bundle_phases = all_internal_phases | ||||
Augie Fackler
|
r43346 | |||
r52180 | def supportinternal(repo: "localrepo.localrepository") -> bool: | |||
Boris Feld
|
r39335 | """True if the internal phase can be used on a repository""" | ||
Pulkit Goyal
|
r45932 | return requirements.INTERNAL_PHASE_REQUIREMENT in repo.requirements | ||
Boris Feld
|
r39335 | |||
Augie Fackler
|
r43346 | |||
r52180 | def supportarchived(repo: "localrepo.localrepository") -> bool: | |||
r50345 | """True if the archived phase can be used on a repository""" | |||
r50346 | return requirements.ARCHIVED_PHASE_REQUIREMENT in repo.requirements | |||
r50345 | ||||
r52180 | def _readroots( | |||
repo: "localrepo.localrepository", | ||||
phasedefaults: Optional["Phasedefaults"] = None, | ||||
) -> Tuple[Phaseroots, bool]: | ||||
Patrick Mezard
|
r16625 | """Read phase roots from disk | ||
phasedefaults is a list of fn(repo, roots) callable, which are | ||||
executed if the phase roots file does not exist. When phases are | ||||
being initialized on an existing repository, this could be used to | ||||
set selected changesets phase to something else than public. | ||||
Return (roots, dirty) where dirty is true if roots differ from | ||||
what is being stored. | ||||
""" | ||||
Pierre-Yves David
|
r18002 | repo = repo.unfiltered() | ||
Patrick Mezard
|
r16625 | dirty = False | ||
Joerg Sonnenberger
|
r45691 | roots = {i: set() for i in allphases} | ||
r52299 | to_rev = repo.changelog.index.get_rev | |||
r52298 | unknown_msg = b'removing unknown node %s from %i-phase boundary\n' | |||
Pierre-Yves David
|
r15418 | try: | ||
Augie Fackler
|
r43347 | f, pending = txnutil.trypending(repo.root, repo.svfs, b'phaseroots') | ||
Pierre-Yves David
|
r15418 | try: | ||
for line in f: | ||||
r52298 | str_phase, hex_node = line.split() | |||
phase = int(str_phase) | ||||
node = bin(hex_node) | ||||
r52299 | rev = to_rev(node) | |||
if rev is None: | ||||
r52298 | repo.ui.debug(unknown_msg % (short(hex_node), phase)) | |||
dirty = True | ||||
else: | ||||
r52299 | roots[phase].add(rev) | |||
Pierre-Yves David
|
r15418 | finally: | ||
f.close() | ||||
Manuel Jacob
|
r50201 | except FileNotFoundError: | ||
Patrick Mezard
|
r16625 | if phasedefaults: | ||
for f in phasedefaults: | ||||
roots = f(repo, roots) | ||||
dirty = True | ||||
return roots, dirty | ||||
Pierre-Yves David
|
r15418 | |||
Augie Fackler
|
r43346 | |||
r52180 | def binaryencode(phasemapping: Dict[int, List[bytes]]) -> bytes: | |||
Boris Feld
|
r34320 | """encode a 'phase -> nodes' mapping into a binary stream | ||
Joerg Sonnenberger
|
r45677 | The revision lists are encoded as (phase, root) pairs. | ||
Boris Feld
|
r34320 | """ | ||
binarydata = [] | ||||
Gregory Szorc
|
r49768 | for phase, nodes in phasemapping.items(): | ||
Boris Feld
|
r34320 | for head in nodes: | ||
binarydata.append(_fphasesentry.pack(phase, head)) | ||||
Augie Fackler
|
r43347 | return b''.join(binarydata) | ||
Boris Feld
|
r34320 | |||
Augie Fackler
|
r43346 | |||
r52180 | def binarydecode(stream) -> Dict[int, List[bytes]]: | |||
Boris Feld
|
r34321 | """decode a binary stream into a 'phase -> nodes' mapping | ||
Joerg Sonnenberger
|
r45677 | The (phase, root) pairs are turned back into a dictionary with | ||
the phase as index and the aggregated roots of that phase as value.""" | ||||
headsbyphase = {i: [] for i in allphases} | ||||
Boris Feld
|
r34321 | entrysize = _fphasesentry.size | ||
while True: | ||||
entry = stream.read(entrysize) | ||||
if len(entry) < entrysize: | ||||
if entry: | ||||
Augie Fackler
|
r43347 | raise error.Abort(_(b'bad phase-heads stream')) | ||
Boris Feld
|
r34321 | break | ||
phase, node = _fphasesentry.unpack(entry) | ||||
headsbyphase[phase].append(node) | ||||
return headsbyphase | ||||
Augie Fackler
|
r43346 | |||
Joerg Sonnenberger
|
r45036 | def _sortedrange_insert(data, idx, rev, t): | ||
merge_before = False | ||||
if idx: | ||||
r1, t1 = data[idx - 1] | ||||
merge_before = r1[-1] + 1 == rev and t1 == t | ||||
merge_after = False | ||||
if idx < len(data): | ||||
r2, t2 = data[idx] | ||||
merge_after = r2[0] == rev + 1 and t2 == t | ||||
if merge_before and merge_after: | ||||
Manuel Jacob
|
r50179 | data[idx - 1] = (range(r1[0], r2[-1] + 1), t) | ||
Joerg Sonnenberger
|
r45036 | data.pop(idx) | ||
elif merge_before: | ||||
Manuel Jacob
|
r50179 | data[idx - 1] = (range(r1[0], rev + 1), t) | ||
Joerg Sonnenberger
|
r45036 | elif merge_after: | ||
Manuel Jacob
|
r50179 | data[idx] = (range(rev, r2[-1] + 1), t) | ||
Joerg Sonnenberger
|
r45036 | else: | ||
Manuel Jacob
|
r50179 | data.insert(idx, (range(rev, rev + 1), t)) | ||
Joerg Sonnenberger
|
r45036 | |||
def _sortedrange_split(data, idx, rev, t): | ||||
r1, t1 = data[idx] | ||||
if t == t1: | ||||
return | ||||
t = (t1[0], t[1]) | ||||
if len(r1) == 1: | ||||
data.pop(idx) | ||||
_sortedrange_insert(data, idx, rev, t) | ||||
elif r1[0] == rev: | ||||
Manuel Jacob
|
r50179 | data[idx] = (range(rev + 1, r1[-1] + 1), t1) | ||
Joerg Sonnenberger
|
r45036 | _sortedrange_insert(data, idx, rev, t) | ||
elif r1[-1] == rev: | ||||
Manuel Jacob
|
r50179 | data[idx] = (range(r1[0], rev), t1) | ||
Joerg Sonnenberger
|
r45036 | _sortedrange_insert(data, idx + 1, rev, t) | ||
else: | ||||
data[idx : idx + 1] = [ | ||||
Manuel Jacob
|
r50179 | (range(r1[0], rev), t1), | ||
(range(rev, rev + 1), t), | ||||
(range(rev + 1, r1[-1] + 1), t1), | ||||
Joerg Sonnenberger
|
r45036 | ] | ||
Boris Feld
|
r33451 | def _trackphasechange(data, rev, old, new): | ||
Joerg Sonnenberger
|
r45036 | """add a phase move to the <data> list of ranges | ||
Boris Feld
|
r33451 | |||
If data is None, nothing happens. | ||||
""" | ||||
if data is None: | ||||
return | ||||
Joerg Sonnenberger
|
r45036 | |||
# If data is empty, create a one-revision range and done | ||||
if not data: | ||||
Manuel Jacob
|
r50179 | data.insert(0, (range(rev, rev + 1), (old, new))) | ||
Joerg Sonnenberger
|
r45036 | return | ||
low = 0 | ||||
high = len(data) | ||||
t = (old, new) | ||||
while low < high: | ||||
mid = (low + high) // 2 | ||||
revs = data[mid][0] | ||||
Joerg Sonnenberger
|
r46127 | revs_low = revs[0] | ||
revs_high = revs[-1] | ||||
Joerg Sonnenberger
|
r45036 | |||
Joerg Sonnenberger
|
r46127 | if rev >= revs_low and rev <= revs_high: | ||
Joerg Sonnenberger
|
r45036 | _sortedrange_split(data, mid, rev, t) | ||
return | ||||
Joerg Sonnenberger
|
r46127 | if revs_low == rev + 1: | ||
Joerg Sonnenberger
|
r45036 | if mid and data[mid - 1][0][-1] == rev: | ||
_sortedrange_split(data, mid - 1, rev, t) | ||||
else: | ||||
_sortedrange_insert(data, mid, rev, t) | ||||
return | ||||
Joerg Sonnenberger
|
r46127 | if revs_high == rev - 1: | ||
Joerg Sonnenberger
|
r45036 | if mid + 1 < len(data) and data[mid + 1][0][0] == rev: | ||
_sortedrange_split(data, mid + 1, rev, t) | ||||
else: | ||||
_sortedrange_insert(data, mid + 1, rev, t) | ||||
return | ||||
Joerg Sonnenberger
|
r46127 | if revs_low > rev: | ||
Joerg Sonnenberger
|
r45036 | high = mid | ||
else: | ||||
low = mid + 1 | ||||
if low == len(data): | ||||
Manuel Jacob
|
r50179 | data.append((range(rev, rev + 1), t)) | ||
Joerg Sonnenberger
|
r45036 | return | ||
r1, t1 = data[low] | ||||
if r1[0] > rev: | ||||
Manuel Jacob
|
r50179 | data.insert(low, (range(rev, rev + 1), t)) | ||
Joerg Sonnenberger
|
r45036 | else: | ||
Manuel Jacob
|
r50179 | data.insert(low + 1, (range(rev, rev + 1), t)) | ||
Boris Feld
|
r33451 | |||
Augie Fackler
|
r43346 | |||
r52312 | # consider incrementaly updating the phase set the update set is not bigger | |||
# than this size | ||||
# | ||||
# Be warned, this number is picked arbitrarily, without any benchmark. It | ||||
# should blindly pickup "small update" | ||||
INCREMENTAL_PHASE_SETS_UPDATE_MAX_UPDATE = 100 | ||||
Gregory Szorc
|
r49801 | class phasecache: | ||
Matt Harbison
|
r52704 | if typing.TYPE_CHECKING: | ||
@overload | ||||
def __init__( | ||||
self, | ||||
repo: Any, | ||||
phasedefaults: Any, | ||||
_load: Literal[False], | ||||
) -> None: | ||||
pass | ||||
@overload | ||||
def __init__( | ||||
self, | ||||
repo: "localrepo.localrepository", | ||||
phasedefaults: Optional["Phasedefaults"], | ||||
_load: bool = True, | ||||
) -> None: | ||||
pass | ||||
r52180 | def __init__( | |||
self, | ||||
Matt Harbison
|
r52704 | repo, | ||
phasedefaults, | ||||
_load=True, | ||||
r52180 | ): | |||
Patrick Mezard
|
r16658 | if _load: | ||
# Cheap trick to allow shallow-copy without copy module | ||||
r52298 | loaded = _readroots(repo, phasedefaults) | |||
self._phaseroots: Phaseroots = loaded[0] | ||||
self.dirty: bool = loaded[1] | ||||
Yuya Nishihara
|
r35458 | self._loadedrevslen = 0 | ||
Matt Harbison
|
r52703 | self._phasesets: Optional[PhaseSets] = None | ||
Patrick Mezard
|
r16658 | |||
r52180 | def hasnonpublicphases(self, repo: "localrepo.localrepository") -> bool: | |||
Joerg Sonnenberger
|
r45674 | """detect if there are revisions with non-public phase""" | ||
r52309 | # XXX deprecate the unused repo argument | |||
Joerg Sonnenberger
|
r45691 | return any( | ||
r52296 | revs for phase, revs in self._phaseroots.items() if phase != public | |||
Joerg Sonnenberger
|
r45691 | ) | ||
Joerg Sonnenberger
|
r45674 | |||
r52180 | def nonpublicphaseroots( | |||
self, repo: "localrepo.localrepository" | ||||
r52299 | ) -> Set[int]: | |||
Joerg Sonnenberger
|
r45674 | """returns the roots of all non-public phases | ||
The roots are not minimized, so if the secret revisions are | ||||
descendants of draft revisions, their roots will still be present. | ||||
""" | ||||
repo = repo.unfiltered() | ||||
r52308 | self._ensure_phase_sets(repo) | |||
Joerg Sonnenberger
|
r45691 | return set().union( | ||
*[ | ||||
revs | ||||
r52296 | for phase, revs in self._phaseroots.items() | |||
Joerg Sonnenberger
|
r45691 | if phase != public | ||
] | ||||
) | ||||
Joerg Sonnenberger
|
r45674 | |||
r52478 | def get_raw_set( | |||
self, | ||||
repo: "localrepo.localrepository", | ||||
phase: int, | ||||
) -> Set[int]: | ||||
"""return the set of revision in that phase | ||||
The returned set is not filtered and might contains revision filtered | ||||
for the passed repoview. | ||||
The returned set might be the internal one and MUST NOT be mutated to | ||||
avoid side effect. | ||||
""" | ||||
if phase == public: | ||||
raise error.ProgrammingError("cannot get_set for public phase") | ||||
self._ensure_phase_sets(repo.unfiltered()) | ||||
revs = self._phasesets.get(phase) | ||||
if revs is None: | ||||
return set() | ||||
return revs | ||||
r52180 | def getrevset( | |||
self, | ||||
repo: "localrepo.localrepository", | ||||
phases: Iterable[int], | ||||
subset: Optional[Any] = None, | ||||
) -> Any: | ||||
Matt Harbison
|
r47390 | # TODO: finish typing this | ||
Jun Wu
|
r31016 | """return a smartset for the given phases""" | ||
r52310 | self._ensure_phase_sets(repo.unfiltered()) | |||
Joerg Sonnenberger
|
r35310 | phases = set(phases) | ||
Rodrigo Damazio Bovendorp
|
r44521 | publicphase = public in phases | ||
Rodrigo Damazio Bovendorp
|
r44456 | |||
Rodrigo Damazio Bovendorp
|
r44521 | if publicphase: | ||
# In this case, phases keeps all the *other* phases. | ||||
phases = set(allphases).difference(phases) | ||||
if not phases: | ||||
return smartset.fullreposet(repo) | ||||
# fast path: _phasesets contains the interesting sets, | ||||
# might only need a union and post-filtering. | ||||
Rodrigo Damazio Bovendorp
|
r44522 | revsneedscopy = False | ||
Rodrigo Damazio Bovendorp
|
r44521 | if len(phases) == 1: | ||
[p] = phases | ||||
revs = self._phasesets[p] | ||||
Rodrigo Damazio Bovendorp
|
r44522 | revsneedscopy = True # Don't modify _phasesets | ||
Rodrigo Damazio Bovendorp
|
r44521 | else: | ||
# revs has the revisions in all *other* phases. | ||||
revs = set.union(*[self._phasesets[p] for p in phases]) | ||||
def _addwdir(wdirsubset, wdirrevs): | ||||
if wdirrev in wdirsubset and repo[None].phase() in phases: | ||||
Rodrigo Damazio Bovendorp
|
r44522 | if revsneedscopy: | ||
wdirrevs = wdirrevs.copy() | ||||
Rodrigo Damazio Bovendorp
|
r44521 | # The working dir would never be in the # cache, but it was in | ||
# the subset being filtered for its phase (or filtered out, | ||||
# depending on publicphase), so add it to the output to be | ||||
# included (or filtered out). | ||||
wdirrevs.add(wdirrev) | ||||
return wdirrevs | ||||
if not publicphase: | ||||
Jun Wu
|
r31016 | if repo.changelog.filteredrevs: | ||
revs = revs - repo.changelog.filteredrevs | ||||
Rodrigo Damazio Bovendorp
|
r44456 | |||
Jun Wu
|
r35331 | if subset is None: | ||
return smartset.baseset(revs) | ||||
else: | ||||
Rodrigo Damazio Bovendorp
|
r44521 | revs = _addwdir(subset, revs) | ||
Jun Wu
|
r35331 | return subset & smartset.baseset(revs) | ||
Jun Wu
|
r31016 | else: | ||
Jun Wu
|
r35331 | if subset is None: | ||
subset = smartset.fullreposet(repo) | ||||
Rodrigo Damazio Bovendorp
|
r44456 | |||
Rodrigo Damazio Bovendorp
|
r44521 | revs = _addwdir(subset, revs) | ||
Rodrigo Damazio Bovendorp
|
r44456 | |||
Joerg Sonnenberger
|
r35310 | if not revs: | ||
Jun Wu
|
r35331 | return subset | ||
return subset.filter(lambda r: r not in revs) | ||||
Jun Wu
|
r31016 | |||
Patrick Mezard
|
r16658 | def copy(self): | ||
# Shallow copy meant to ensure isolation in | ||||
# advance/retractboundary(), nothing more. | ||||
Eric Sumner
|
r23631 | ph = self.__class__(None, None, _load=False) | ||
r52296 | ph._phaseroots = self._phaseroots.copy() | |||
Patrick Mezard
|
r16658 | ph.dirty = self.dirty | ||
Yuya Nishihara
|
r35457 | ph._loadedrevslen = self._loadedrevslen | ||
r52311 | if self._phasesets is None: | |||
ph._phasesets = None | ||||
else: | ||||
ph._phasesets = self._phasesets.copy() | ||||
Patrick Mezard
|
r16658 | return ph | ||
def replace(self, phcache): | ||||
Pierre-Yves David
|
r25613 | """replace all values in 'self' with content of phcache""" | ||
Augie Fackler
|
r43346 | for a in ( | ||
r52296 | '_phaseroots', | |||
r51485 | 'dirty', | |||
'_loadedrevslen', | ||||
'_phasesets', | ||||
Augie Fackler
|
r43346 | ): | ||
Patrick Mezard
|
r16658 | setattr(self, a, getattr(phcache, a)) | ||
Patrick Mezard
|
r16657 | |||
Laurent Charignon
|
r24599 | def _getphaserevsnative(self, repo): | ||
Laurent Charignon
|
r24444 | repo = repo.unfiltered() | ||
r52296 | return repo.changelog.computephases(self._phaseroots) | |||
Laurent Charignon
|
r24444 | |||
Laurent Charignon
|
r24599 | def _computephaserevspure(self, repo): | ||
Laurent Charignon
|
r24519 | repo = repo.unfiltered() | ||
Joerg Sonnenberger
|
r35310 | cl = repo.changelog | ||
Joerg Sonnenberger
|
r45691 | self._phasesets = {phase: set() for phase in allphases} | ||
Boris Feld
|
r39307 | lowerroots = set() | ||
for phase in reversed(trackedphases): | ||||
r52299 | roots = self._phaseroots[phase] | |||
Boris Feld
|
r39307 | if roots: | ||
ps = set(cl.descendants(roots)) | ||||
for root in roots: | ||||
ps.add(root) | ||||
ps.difference_update(lowerroots) | ||||
lowerroots.update(ps) | ||||
self._phasesets[phase] = ps | ||||
Yuya Nishihara
|
r35457 | self._loadedrevslen = len(cl) | ||
Laurent Charignon
|
r24519 | |||
r52308 | def _ensure_phase_sets(self, repo: "localrepo.localrepository") -> None: | |||
r52312 | """ensure phase information is loaded in the object""" | |||
assert repo.filtername is None | ||||
update = -1 | ||||
cl = repo.changelog | ||||
cl_size = len(cl) | ||||
Joerg Sonnenberger
|
r35310 | if self._phasesets is None: | ||
r52312 | update = 0 | |||
else: | ||||
if cl_size > self._loadedrevslen: | ||||
# check if an incremental update is worth it. | ||||
# note we need a tradeoff here because the whole logic is not | ||||
# stored and implemented in native code nd datastructure. | ||||
# Otherwise the incremental update woul always be a win. | ||||
missing = cl_size - self._loadedrevslen | ||||
if missing <= INCREMENTAL_PHASE_SETS_UPDATE_MAX_UPDATE: | ||||
update = self._loadedrevslen | ||||
else: | ||||
update = 0 | ||||
if update == 0: | ||||
Laurent Charignon
|
r24444 | try: | ||
Jun Wu
|
r31152 | res = self._getphaserevsnative(repo) | ||
Yuya Nishihara
|
r35457 | self._loadedrevslen, self._phasesets = res | ||
Laurent Charignon
|
r24444 | except AttributeError: | ||
Laurent Charignon
|
r24599 | self._computephaserevspure(repo) | ||
r52308 | assert self._loadedrevslen == len(repo.changelog) | |||
r52312 | elif update > 0: | |||
# good candidate for native code | ||||
assert update == self._loadedrevslen | ||||
if self.hasnonpublicphases(repo): | ||||
start = self._loadedrevslen | ||||
get_phase = self.phase | ||||
rev_phases = [0] * missing | ||||
parents = cl.parentrevs | ||||
sets = {phase: set() for phase in self._phasesets} | ||||
for phase, roots in self._phaseroots.items(): | ||||
# XXX should really store the max somewhere | ||||
for r in roots: | ||||
if r >= start: | ||||
rev_phases[r - start] = phase | ||||
for rev in range(start, cl_size): | ||||
phase = rev_phases[rev - start] | ||||
p1, p2 = parents(rev) | ||||
if p1 == nullrev: | ||||
p1_phase = public | ||||
elif p1 >= start: | ||||
p1_phase = rev_phases[p1 - start] | ||||
else: | ||||
p1_phase = max(phase, get_phase(repo, p1)) | ||||
if p2 == nullrev: | ||||
p2_phase = public | ||||
elif p2 >= start: | ||||
p2_phase = rev_phases[p2 - start] | ||||
else: | ||||
p2_phase = max(phase, get_phase(repo, p2)) | ||||
phase = max(phase, p1_phase, p2_phase) | ||||
if phase > public: | ||||
rev_phases[rev - start] = phase | ||||
sets[phase].add(rev) | ||||
# Be careful to preserve shallow-copied values: do not update | ||||
# phaseroots values, replace them. | ||||
for phase, extra in sets.items(): | ||||
if extra: | ||||
self._phasesets[phase] = self._phasesets[phase] | extra | ||||
self._loadedrevslen = cl_size | ||||
Durham Goode
|
r22894 | |||
Durham Goode
|
r22893 | def invalidate(self): | ||
Yuya Nishihara
|
r35458 | self._loadedrevslen = 0 | ||
Pierre-Yves David
|
r25593 | self._phasesets = None | ||
Patrick Mezard
|
r16657 | |||
r52180 | def phase(self, repo: "localrepo.localrepository", rev: int) -> int: | |||
Joerg Sonnenberger
|
r35310 | # We need a repo argument here to be able to build _phasesets | ||
Patrick Mezard
|
r16657 | # if necessary. The repository instance is not stored in | ||
# phasecache to avoid reference cycles. The changelog instance | ||||
# is not stored because it is a filecache() property and can | ||||
# be replaced without us being notified. | ||||
if rev == nullrev: | ||||
return public | ||||
Durham Goode
|
r19984 | if rev < nullrev: | ||
Augie Fackler
|
r43347 | raise ValueError(_(b'cannot lookup negative revision')) | ||
r52308 | # double check self._loadedrevslen to avoid an extra method call as | |||
# python is slow for that. | ||||
Yuya Nishihara
|
r35457 | if rev >= self._loadedrevslen: | ||
r52310 | self._ensure_phase_sets(repo.unfiltered()) | |||
Joerg Sonnenberger
|
r35310 | for phase in trackedphases: | ||
if rev in self._phasesets[phase]: | ||||
return phase | ||||
return public | ||||
Patrick Mezard
|
r16657 | |||
r52297 | def write(self, repo): | |||
Patrick Mezard
|
r16657 | if not self.dirty: | ||
return | ||||
r52297 | f = repo.svfs(b'phaseroots', b'w', atomictemp=True, checkambig=True) | |||
Patrick Mezard
|
r16657 | try: | ||
r52297 | self._write(repo.unfiltered(), f) | |||
Patrick Mezard
|
r16657 | finally: | ||
f.close() | ||||
Pierre-Yves David
|
r22079 | |||
r52297 | def _write(self, repo, fp): | |||
assert repo.filtername is None | ||||
r52299 | to_node = repo.changelog.node | |||
r52296 | for phase, roots in self._phaseroots.items(): | |||
r52299 | for r in sorted(roots): | |||
h = to_node(r) | ||||
Augie Fackler
|
r43347 | fp.write(b'%i %s\n' % (phase, hex(h))) | ||
Patrick Mezard
|
r16657 | self.dirty = False | ||
Pierre-Yves David
|
r15454 | |||
r52313 | def _updateroots(self, repo, phase, newroots, tr, invalidate=True): | |||
r52296 | self._phaseroots[phase] = newroots | |||
Patrick Mezard
|
r16658 | self.dirty = True | ||
r52313 | if invalidate: | |||
self.invalidate() | ||||
Patrick Mezard
|
r16658 | |||
r52297 | assert repo.filtername is None | |||
wrepo = weakref.ref(repo) | ||||
def tr_write(fp): | ||||
repo = wrepo() | ||||
assert repo is not None | ||||
self._write(repo, fp) | ||||
tr.addfilegenerator(b'phase', (b'phaseroots',), tr_write) | ||||
Augie Fackler
|
r43347 | tr.hookargs[b'phases_moved'] = b'1' | ||
Pierre-Yves David
|
r22080 | |||
Joerg Sonnenberger
|
r46375 | def registernew(self, repo, tr, targetphase, revs): | ||
Boris Feld
|
r33453 | repo = repo.unfiltered() | ||
Joerg Sonnenberger
|
r46375 | self._retractboundary(repo, tr, targetphase, [], revs=revs) | ||
Augie Fackler
|
r43347 | if tr is not None and b'phases' in tr.changes: | ||
phasetracking = tr.changes[b'phases'] | ||||
Boris Feld
|
r33453 | phase = self.phase | ||
Joerg Sonnenberger
|
r46375 | for rev in sorted(revs): | ||
Boris Feld
|
r33453 | revphase = phase(repo, rev) | ||
_trackphasechange(phasetracking, rev, None, revphase) | ||||
repo.invalidatevolatilesets() | ||||
Joerg Sonnenberger
|
r46374 | def advanceboundary( | ||
r52299 | self, repo, tr, targetphase, nodes=None, revs=None, dryrun=None | |||
Joerg Sonnenberger
|
r46374 | ): | ||
Boris Feld
|
r33450 | """Set all 'nodes' to phase 'targetphase' | ||
Nodes with a phase lower than 'targetphase' are not affected. | ||||
Sushil khanchi
|
r38218 | |||
If dryrun is True, no actions will be performed | ||||
Returns a set of revs whose phase is changed or should be changed | ||||
Boris Feld
|
r33450 | """ | ||
r52301 | if targetphase == public and not self.hasnonpublicphases(repo): | |||
return set() | ||||
r52315 | repo = repo.unfiltered() | |||
cl = repo.changelog | ||||
torev = cl.index.rev | ||||
Patrick Mezard
|
r16658 | # Be careful to preserve shallow-copied values: do not update | ||
# phaseroots values, replace them. | ||||
r52315 | new_revs = set() | |||
if revs is not None: | ||||
new_revs.update(revs) | ||||
if nodes is not None: | ||||
new_revs.update(torev(node) for node in nodes) | ||||
if not new_revs: # bail out early to avoid the loadphaserevs call | ||||
return ( | ||||
set() | ||||
) # note: why do people call advanceboundary with nothing? | ||||
Boris Feld
|
r33451 | if tr is None: | ||
phasetracking = None | ||||
else: | ||||
Augie Fackler
|
r43347 | phasetracking = tr.changes.get(b'phases') | ||
Patrick Mezard
|
r16658 | |||
r52316 | affectable_phases = sorted( | |||
p for p in allphases if p > targetphase and self._phaseroots[p] | ||||
) | ||||
# filter revision already in the right phases | ||||
candidates = new_revs | ||||
new_revs = set() | ||||
r52315 | self._ensure_phase_sets(repo) | |||
r52316 | for phase in affectable_phases: | |||
found = candidates & self._phasesets[phase] | ||||
new_revs |= found | ||||
candidates -= found | ||||
if not candidates: | ||||
break | ||||
r52315 | if not new_revs: | |||
return set() | ||||
Boris Feld
|
r33451 | |||
r52315 | # search for affected high phase changesets and roots | |||
r52398 | seen = set(new_revs) | |||
r52316 | push = heapq.heappush | |||
pop = heapq.heappop | ||||
parents = cl.parentrevs | ||||
get_phase = self.phase | ||||
changed = {} # set of revisions to be changed | ||||
# set of root deleted by this path | ||||
delroots = set() | ||||
new_roots = {p: set() for p in affectable_phases} | ||||
new_target_roots = set() | ||||
# revision to walk down | ||||
revs = [-r for r in new_revs] | ||||
heapq.heapify(revs) | ||||
while revs: | ||||
current = -pop(revs) | ||||
current_phase = get_phase(repo, current) | ||||
changed[current] = current_phase | ||||
p1, p2 = parents(current) | ||||
if p1 == nullrev: | ||||
p1_phase = public | ||||
else: | ||||
p1_phase = get_phase(repo, p1) | ||||
if p2 == nullrev: | ||||
p2_phase = public | ||||
else: | ||||
p2_phase = get_phase(repo, p2) | ||||
# do we have a root ? | ||||
if current_phase != p1_phase and current_phase != p2_phase: | ||||
# do not record phase, because we could have "duplicated" | ||||
# roots, were one root is shadowed by the very same roots of an | ||||
# higher phases | ||||
delroots.add(current) | ||||
# schedule a walk down if needed | ||||
r52398 | if p1_phase > targetphase and p1 not in seen: | |||
seen.add(p1) | ||||
r52316 | push(revs, -p1) | |||
r52398 | if p2_phase > targetphase and p2 not in seen: | |||
seen.add(p2) | ||||
r52316 | push(revs, -p2) | |||
if p1_phase < targetphase and p2_phase < targetphase: | ||||
new_target_roots.add(current) | ||||
Boris Feld
|
r33451 | |||
r52316 | # the last iteration was done with the smallest value | |||
min_current = current | ||||
# do we have unwalked children that might be new roots | ||||
if (min_current + len(changed)) < len(cl): | ||||
for r in range(min_current, len(cl)): | ||||
if r in changed: | ||||
continue | ||||
phase = get_phase(repo, r) | ||||
if phase <= targetphase: | ||||
continue | ||||
p1, p2 = parents(r) | ||||
if not (p1 in changed or p2 in changed): | ||||
continue # not affected | ||||
if p1 != nullrev and p1 not in changed: | ||||
p1_phase = get_phase(repo, p1) | ||||
if p1_phase == phase: | ||||
continue # not a root | ||||
if p2 != nullrev and p2 not in changed: | ||||
p2_phase = get_phase(repo, p2) | ||||
if p2_phase == phase: | ||||
continue # not a root | ||||
new_roots[phase].add(r) | ||||
Boris Feld
|
r33450 | |||
r52316 | # apply the changes | |||
Sushil khanchi
|
r38218 | if not dryrun: | ||
r52316 | for r, p in changed.items(): | |||
_trackphasechange(phasetracking, r, p, targetphase) | ||||
r52317 | if targetphase > public: | |||
self._phasesets[targetphase].update(changed) | ||||
r52316 | for phase in affectable_phases: | |||
roots = self._phaseroots[phase] | ||||
removed = roots & delroots | ||||
if removed or new_roots[phase]: | ||||
r52317 | self._phasesets[phase].difference_update(changed) | |||
r52316 | # Be careful to preserve shallow-copied values: do not | |||
# update phaseroots values, replace them. | ||||
final_roots = roots - delroots | new_roots[phase] | ||||
r52317 | self._updateroots( | |||
repo, phase, final_roots, tr, invalidate=False | ||||
) | ||||
r52316 | if new_target_roots: | |||
# Thanks for previous filtering, we can't replace existing | ||||
# roots | ||||
new_target_roots |= self._phaseroots[targetphase] | ||||
r52317 | self._updateroots( | |||
repo, targetphase, new_target_roots, tr, invalidate=False | ||||
) | ||||
Sushil khanchi
|
r38218 | repo.invalidatevolatilesets() | ||
r52316 | return changed | |||
Patrick Mezard
|
r16658 | |||
Pierre-Yves David
|
r22070 | def retractboundary(self, repo, tr, targetphase, nodes): | ||
Boris Feld
|
r33458 | if tr is None: | ||
phasetracking = None | ||||
else: | ||||
Augie Fackler
|
r43347 | phasetracking = tr.changes.get(b'phases') | ||
Boris Feld
|
r33458 | repo = repo.unfiltered() | ||
r52299 | retracted = self._retractboundary(repo, tr, targetphase, nodes) | |||
if retracted and phasetracking is not None: | ||||
r52303 | for r, old_phase in sorted(retracted.items()): | |||
_trackphasechange(phasetracking, r, old_phase, targetphase) | ||||
Boris Feld
|
r33452 | repo.invalidatevolatilesets() | ||
r52299 | def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None): | |||
r52300 | if targetphase == public: | |||
r52302 | return {} | |||
r50345 | if ( | |||
targetphase == internal | ||||
and not supportinternal(repo) | ||||
or targetphase == archived | ||||
and not supportarchived(repo) | ||||
): | ||||
Boris Feld
|
r40463 | name = phasenames[targetphase] | ||
Augie Fackler
|
r43347 | msg = b'this repository does not support the %s phase' % name | ||
Boris Feld
|
r39335 | raise error.ProgrammingError(msg) | ||
r52302 | assert repo.filtername is None | |||
cl = repo.changelog | ||||
torev = cl.index.rev | ||||
new_revs = set() | ||||
if revs is not None: | ||||
new_revs.update(revs) | ||||
if nodes is not None: | ||||
new_revs.update(torev(node) for node in nodes) | ||||
if not new_revs: # bail out early to avoid the loadphaserevs call | ||||
return {} # note: why do people call retractboundary with nothing ? | ||||
Patrick Mezard
|
r16658 | |||
r52302 | if nullrev in new_revs: | |||
raise error.Abort(_(b'cannot change null revision phase')) | ||||
r52314 | # Filter revision that are already in the right phase | |||
self._ensure_phase_sets(repo) | ||||
for phase, revs in self._phasesets.items(): | ||||
if phase >= targetphase: | ||||
new_revs -= revs | ||||
if not new_revs: # all revisions already in the right phases | ||||
return {} | ||||
r52302 | # Compute change in phase roots by walking the graph | |||
# | ||||
# note: If we had a cheap parent → children mapping we could do | ||||
# something even cheaper/more-bounded | ||||
# | ||||
# The idea would be to walk from item in new_revs stopping at | ||||
# descendant with phases >= target_phase. | ||||
# | ||||
# 1) This detect new_revs that are not new_roots (either already >= | ||||
# target_phase or reachable though another new_revs | ||||
# 2) This detect replaced current_roots as we reach them | ||||
# 3) This can avoid walking to the tip if we retract over a small | ||||
# branch. | ||||
# | ||||
# So instead, we do a variation of this, we walk from the smaller new | ||||
# revision to the tip to avoid missing any potential children. | ||||
# | ||||
# The following code would be a good candidate for native code… if only | ||||
# we could knew the phase of a changeset efficiently in native code. | ||||
parents = cl.parentrevs | ||||
phase = self.phase | ||||
new_roots = set() # roots added by this phases | ||||
changed_revs = {} # revision affected by this call | ||||
replaced_roots = set() # older roots replaced by this call | ||||
r52299 | currentroots = self._phaseroots[targetphase] | |||
r52302 | start = min(new_revs) | |||
end = len(cl) | ||||
rev_phases = [None] * (end - start) | ||||
r52410 | ||||
this_phase_set = self._phasesets[targetphase] | ||||
r52302 | for r in range(start, end): | |||
# gather information about the current_rev | ||||
r_phase = phase(repo, r) | ||||
p_phase = None # phase inherited from parents | ||||
p1, p2 = parents(r) | ||||
if p1 >= start: | ||||
p1_phase = rev_phases[p1 - start] | ||||
if p1_phase is not None: | ||||
p_phase = p1_phase | ||||
if p2 >= start: | ||||
p2_phase = rev_phases[p2 - start] | ||||
if p2_phase is not None: | ||||
if p_phase is not None: | ||||
p_phase = max(p_phase, p2_phase) | ||||
else: | ||||
p_phase = p2_phase | ||||
Durham Goode
|
r26909 | |||
r52302 | # assess the situation | |||
if r in new_revs and r_phase < targetphase: | ||||
if p_phase is None or p_phase < targetphase: | ||||
new_roots.add(r) | ||||
rev_phases[r - start] = targetphase | ||||
changed_revs[r] = r_phase | ||||
r52410 | this_phase_set.add(r) | |||
r52302 | elif p_phase is None: | |||
rev_phases[r - start] = r_phase | ||||
else: | ||||
if p_phase > r_phase: | ||||
rev_phases[r - start] = p_phase | ||||
else: | ||||
rev_phases[r - start] = r_phase | ||||
if p_phase == targetphase: | ||||
if p_phase > r_phase: | ||||
changed_revs[r] = r_phase | ||||
r52410 | this_phase_set.add(r) | |||
r52302 | elif r in currentroots: | |||
replaced_roots.add(r) | ||||
r52313 | sets = self._phasesets | |||
r52409 | if targetphase > draft: | |||
for r, old in changed_revs.items(): | ||||
if old > public: | ||||
sets[old].discard(r) | ||||
Durham Goode
|
r26909 | |||
r52302 | if new_roots: | |||
assert changed_revs | ||||
r52313 | ||||
r52302 | final_roots = new_roots | currentroots - replaced_roots | |||
r52313 | self._updateroots( | |||
repo, | ||||
targetphase, | ||||
final_roots, | ||||
tr, | ||||
invalidate=False, | ||||
) | ||||
r52302 | if targetphase > 1: | |||
retracted = set(changed_revs) | ||||
for lower_phase in range(1, targetphase): | ||||
lower_roots = self._phaseroots.get(lower_phase) | ||||
if lower_roots is None: | ||||
continue | ||||
if lower_roots & retracted: | ||||
simpler_roots = lower_roots - retracted | ||||
r52313 | self._updateroots( | |||
repo, | ||||
lower_phase, | ||||
simpler_roots, | ||||
tr, | ||||
invalidate=False, | ||||
) | ||||
r52302 | return changed_revs | |||
else: | ||||
assert not changed_revs | ||||
assert not replaced_roots | ||||
return {} | ||||
Patrick Mezard
|
r16658 | |||
r52294 | def register_strip( | |||
self, | ||||
r52299 | repo, | |||
r52294 | tr, | |||
strip_rev: int, | ||||
): | ||||
"""announce a strip to the phase cache | ||||
Any roots higher than the stripped revision should be dropped. | ||||
""" | ||||
r52299 | for targetphase, roots in list(self._phaseroots.items()): | |||
filtered = {r for r in roots if r >= strip_rev} | ||||
r52294 | if filtered: | |||
r52299 | self._updateroots(repo, targetphase, roots - filtered, tr) | |||
r52294 | self.invalidate() | |||
Augie Fackler
|
r43346 | |||
Joerg Sonnenberger
|
r46374 | def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None): | ||
Pierre-Yves David
|
r15454 | """Add nodes to a phase changing other nodes phases if necessary. | ||
Martin Geisler
|
r16725 | This function move boundary *forward* this means that all nodes | ||
are set in the target phase or kept in a *lower* phase. | ||||
Pierre-Yves David
|
r15454 | |||
Sushil khanchi
|
r38218 | Simplify boundary to contains phase roots only. | ||
If dryrun is True, no actions will be performed | ||||
Returns a set of revs whose phase is changed or should be changed | ||||
""" | ||||
Joerg Sonnenberger
|
r46374 | if revs is None: | ||
revs = [] | ||||
Patrick Mezard
|
r16658 | phcache = repo._phasecache.copy() | ||
Augie Fackler
|
r43346 | changes = phcache.advanceboundary( | ||
Joerg Sonnenberger
|
r46374 | repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun | ||
Augie Fackler
|
r43346 | ) | ||
Sushil khanchi
|
r38218 | if not dryrun: | ||
repo._phasecache.replace(phcache) | ||||
return changes | ||||
Pierre-Yves David
|
r15482 | |||
Augie Fackler
|
r43346 | |||
Pierre-Yves David
|
r22070 | def retractboundary(repo, tr, targetphase, nodes): | ||
Martin Geisler
|
r16725 | """Set nodes back to a phase changing other nodes phases if | ||
necessary. | ||||
Pierre-Yves David
|
r15482 | |||
Martin Geisler
|
r16725 | This function move boundary *backward* this means that all nodes | ||
are set in the target phase or kept in a *higher* phase. | ||||
Pierre-Yves David
|
r15482 | |||
Simplify boundary to contains phase roots only.""" | ||||
Patrick Mezard
|
r16658 | phcache = repo._phasecache.copy() | ||
Pierre-Yves David
|
r22070 | phcache.retractboundary(repo, tr, targetphase, nodes) | ||
Patrick Mezard
|
r16658 | repo._phasecache.replace(phcache) | ||
Pierre-Yves David
|
r15648 | |||
Augie Fackler
|
r43346 | |||
Joerg Sonnenberger
|
r46375 | def registernew(repo, tr, targetphase, revs): | ||
Boris Feld
|
r33453 | """register a new revision and its phase | ||
Code adding revisions to the repository should use this function to | ||||
set new changeset in their target phase (or higher). | ||||
""" | ||||
phcache = repo._phasecache.copy() | ||||
Joerg Sonnenberger
|
r46375 | phcache.registernew(repo, tr, targetphase, revs) | ||
Boris Feld
|
r33453 | repo._phasecache.replace(phcache) | ||
Augie Fackler
|
r43346 | |||
r52180 | def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]: | |||
Martin Geisler
|
r16724 | """List phases root for serialization over pushkey""" | ||
Gregory Szorc
|
r32000 | # Use ordered dictionary so behavior is deterministic. | ||
keys = util.sortdict() | ||||
Augie Fackler
|
r43347 | value = b'%i' % draft | ||
Boris Feld
|
r34817 | cl = repo.unfiltered().changelog | ||
r52299 | to_node = cl.node | |||
r52296 | for root in repo._phasecache._phaseroots[draft]: | |||
r52299 | if repo._phasecache.phase(repo, root) <= draft: | |||
keys[hex(to_node(root))] = value | ||||
Pierre-Yves David
|
r15892 | |||
Matt Mackall
|
r25624 | if repo.publishing(): | ||
Martin Geisler
|
r16725 | # Add an extra data to let remote know we are a publishing | ||
# repo. Publishing repo can't just pretend they are old repo. | ||||
# When pushing to a publishing repo, the client still need to | ||||
# push phase boundary | ||||
Pierre-Yves David
|
r15648 | # | ||
Martin Geisler
|
r16725 | # Push do not only push changeset. It also push phase data. | ||
# New phase data may apply to common changeset which won't be | ||||
# push (as they are common). Here is a very simple example: | ||||
Pierre-Yves David
|
r15648 | # | ||
# 1) repo A push changeset X as draft to repo B | ||||
# 2) repo B make changeset X public | ||||
Martin Geisler
|
r16725 | # 3) repo B push to repo A. X is not pushed but the data that | ||
# X as now public should | ||||
Pierre-Yves David
|
r15648 | # | ||
Martin Geisler
|
r16725 | # The server can't handle it on it's own as it has no idea of | ||
# client phase data. | ||||
Augie Fackler
|
r43347 | keys[b'publishing'] = b'True' | ||
Pierre-Yves David
|
r15648 | return keys | ||
Augie Fackler
|
r43346 | |||
r52180 | def pushphase( | |||
repo: "localrepo.localrepository", | ||||
nhex: bytes, | ||||
oldphasestr: bytes, | ||||
newphasestr: bytes, | ||||
) -> bool: | ||||
timeless@mozdev.org
|
r17535 | """List phases root for serialization over pushkey""" | ||
Pierre-Yves David
|
r18002 | repo = repo.unfiltered() | ||
Bryan O'Sullivan
|
r27861 | with repo.lock(): | ||
Pierre-Yves David
|
r15648 | currentphase = repo[nhex].phase() | ||
Augie Fackler
|
r43346 | newphase = abs(int(newphasestr)) # let's avoid negative index surprise | ||
oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise | ||||
Pierre-Yves David
|
r15648 | if currentphase == oldphase and newphase < oldphase: | ||
Augie Fackler
|
r43347 | with repo.transaction(b'pushkey-phase') as tr: | ||
Bryan O'Sullivan
|
r27861 | advanceboundary(repo, tr, newphase, [bin(nhex)]) | ||
Martin von Zweigbergk
|
r32822 | return True | ||
Matt Mackall
|
r16051 | elif currentphase == newphase: | ||
# raced, but got correct result | ||||
Martin von Zweigbergk
|
r32822 | return True | ||
Pierre-Yves David
|
r15648 | else: | ||
Martin von Zweigbergk
|
r32822 | return False | ||
Pierre-Yves David
|
r15649 | |||
Augie Fackler
|
r43346 | |||
Martin von Zweigbergk
|
r33031 | def subsetphaseheads(repo, subset): | ||
"""Finds the phase heads for a subset of a history | ||||
Returns a list indexed by phase number where each item is a list of phase | ||||
head nodes. | ||||
""" | ||||
cl = repo.changelog | ||||
Joerg Sonnenberger
|
r45677 | headsbyphase = {i: [] for i in allphases} | ||
Jason R. Coombs , Pierre-Yves David pierre-yves.david@octobus.net
|
r51206 | for phase in allphases: | ||
revset = b"heads(%%ln & _phase(%d))" % phase | ||||
Martin von Zweigbergk
|
r33031 | headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)] | ||
return headsbyphase | ||||
Augie Fackler
|
r43346 | |||
Boris Feld
|
r34322 | def updatephases(repo, trgetter, headsbyphase): | ||
Martin von Zweigbergk
|
r33031 | """Updates the repo with the given phase heads""" | ||
Joerg Sonnenberger
|
r45676 | # Now advance phase boundaries of all phases | ||
Boris Feld
|
r34322 | # | ||
# run the update (and fetch transaction) only if there are actually things | ||||
# to update. This avoid creating empty transaction during no-op operation. | ||||
Joerg Sonnenberger
|
r45676 | for phase in allphases: | ||
Augie Fackler
|
r43347 | revset = b'%ln - _phase(%s)' | ||
Boris Feld
|
r39329 | heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)] | ||
Boris Feld
|
r34322 | if heads: | ||
advanceboundary(repo, trgetter(), phase, heads) | ||||
Martin von Zweigbergk
|
r33031 | |||
Augie Fackler
|
r43346 | |||
r52474 | def analyze_remote_phases( | |||
repo, | ||||
subset: Collection[int], | ||||
roots: Dict[bytes, bytes], | ||||
) -> Tuple[Collection[int], Collection[int]]: | ||||
Pierre-Yves David
|
r15649 | """Compute phases heads and root in a subset of node from root dict | ||
* subset is heads of the subset | ||||
* roots is {<nodeid> => phase} mapping. key and value are string. | ||||
Accept unknown element input | ||||
""" | ||||
Pierre-Yves David
|
r18002 | repo = repo.unfiltered() | ||
Pierre-Yves David
|
r15649 | # build list from dictionary | ||
r52472 | draft_roots = [] | |||
to_rev = repo.changelog.index.get_rev | ||||
Gregory Szorc
|
r49768 | for nhex, phase in roots.items(): | ||
Augie Fackler
|
r43347 | if nhex == b'publishing': # ignore data related to publish option | ||
Pierre-Yves David
|
r15649 | continue | ||
node = bin(nhex) | ||||
phase = int(phase) | ||||
Gregory Szorc
|
r28174 | if phase == public: | ||
Joerg Sonnenberger
|
r47771 | if node != repo.nullid: | ||
r52471 | msg = _(b'ignoring inconsistent public root from remote: %s\n') | |||
repo.ui.warn(msg % nhex) | ||||
Gregory Szorc
|
r28174 | elif phase == draft: | ||
r52472 | rev = to_rev(node) | |||
if rev is not None: # to filter unknown nodes | ||||
draft_roots.append(rev) | ||||
Pierre-Yves David
|
r15892 | else: | ||
r52471 | msg = _(b'ignoring unexpected root from remote: %i %s\n') | |||
repo.ui.warn(msg % (phase, nhex)) | ||||
Pierre-Yves David
|
r15649 | # compute heads | ||
r52474 | public_heads = new_heads(repo, subset, draft_roots) | |||
return public_heads, draft_roots | ||||
Pierre-Yves David
|
r15649 | |||
Augie Fackler
|
r43346 | |||
r52476 | class RemotePhasesSummary: | |||
Boris Feld
|
r34820 | """summarize phase information on the remote side | ||
:publishing: True is the remote is publishing | ||||
r52476 | :public_heads: list of remote public phase heads (revs) | |||
:draft_heads: list of remote draft phase heads (revs) | ||||
:draft_roots: list of remote draft phase root (revs) | ||||
Boris Feld
|
r34820 | """ | ||
r52476 | def __init__( | |||
self, | ||||
repo, | ||||
remote_subset: Collection[int], | ||||
remote_roots: Dict[bytes, bytes], | ||||
): | ||||
Boris Feld
|
r34820 | unfi = repo.unfiltered() | ||
r52476 | self._allremoteroots: Dict[bytes, bytes] = remote_roots | |||
Boris Feld
|
r34820 | |||
r52476 | self.publishing: bool = bool(remote_roots.get(b'publishing', False)) | |||
Boris Feld
|
r34820 | |||
r52476 | heads, roots = analyze_remote_phases(repo, remote_subset, remote_roots) | |||
self.public_heads: Collection[int] = heads | ||||
self.draft_roots: Collection[int] = roots | ||||
Boris Feld
|
r34820 | # Get the list of all "heads" revs draft on remote | ||
r52475 | dheads = unfi.revs(b'heads(%ld::%ld)', roots, remote_subset) | |||
r52476 | self.draft_heads: Collection[int] = dheads | |||
Boris Feld
|
r34820 | |||
Augie Fackler
|
r43346 | |||
r52473 | def new_heads( | |||
repo, | ||||
heads: Collection[int], | ||||
roots: Collection[int], | ||||
) -> Collection[int]: | ||||
Pierre-Yves David
|
r15954 | """compute new head of a subset minus another | ||
* `heads`: define the first subset | ||||
Mads Kiilerich
|
r17425 | * `roots`: define the second we subtract from the first""" | ||
Boris Feld
|
r39182 | # prevent an import cycle | ||
# phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases | ||||
from . import dagop | ||||
if not roots: | ||||
return heads | ||||
r52473 | if not heads or heads == [nullrev]: | |||
Boris Feld
|
r39182 | return [] | ||
# The logic operated on revisions, convert arguments early for convenience | ||||
r52473 | # PERF-XXX: maybe heads could directly comes as a set without impacting | |||
# other user of that value | ||||
new_heads = set(heads) | ||||
new_heads.discard(nullrev) | ||||
Boris Feld
|
r39182 | # compute the area we need to remove | ||
Augie Fackler
|
r43347 | affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads) | ||
Boris Feld
|
r39182 | # heads in the area are no longer heads | ||
new_heads.difference_update(affected_zone) | ||||
# revisions in the area have children outside of it, | ||||
# They might be new heads | ||||
Augie Fackler
|
r43346 | candidates = repo.revs( | ||
Augie Fackler
|
r43347 | b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone | ||
Augie Fackler
|
r43346 | ) | ||
Boris Feld
|
r39182 | candidates -= affected_zone | ||
if new_heads or candidates: | ||||
# remove candidate that are ancestors of other heads | ||||
new_heads.update(candidates) | ||||
Augie Fackler
|
r43347 | prunestart = repo.revs(b"parents(%ld) and not null", new_heads) | ||
Boris Feld
|
r39182 | pruned = dagop.reachableroots(repo, candidates, prunestart) | ||
new_heads.difference_update(pruned) | ||||
r52473 | # PERF-XXX: do we actually need a sorted list here? Could we simply return | |||
# a set? | ||||
return sorted(new_heads) | ||||
Pierre-Yves David
|
r16030 | |||
Augie Fackler
|
r43346 | |||
r52180 | def newcommitphase(ui: "uimod.ui") -> int: | |||
Pierre-Yves David
|
r16030 | """helper to get the target phase of new commit | ||
Handle all possible values for the phases.new-commit options. | ||||
""" | ||||
Augie Fackler
|
r43347 | v = ui.config(b'phases', b'new-commit') | ||
Pierre-Yves David
|
r16030 | try: | ||
Joerg Sonnenberger
|
r45677 | return phasenumber2[v] | ||
except KeyError: | ||||
raise error.ConfigError( | ||||
_(b"phases.new-commit: not a valid phase name ('%s')") % v | ||||
) | ||||
Pierre-Yves David
|
r16030 | |||
Augie Fackler
|
r43346 | |||
r52180 | def hassecret(repo: "localrepo.localrepository") -> bool: | |||
Pierre-Yves David
|
r17671 | """utility function that check if a repo have any secret changeset.""" | ||
r52296 | return bool(repo._phasecache._phaseroots[secret]) | |||
Boris Feld
|
r34711 | |||
Augie Fackler
|
r43346 | |||
r52180 | def preparehookargs( | |||
node: bytes, | ||||
old: Optional[int], | ||||
new: Optional[int], | ||||
) -> Dict[bytes, bytes]: | ||||
Boris Feld
|
r34711 | if old is None: | ||
Augie Fackler
|
r43347 | old = b'' | ||
Boris Feld
|
r34711 | else: | ||
Kevin Bullock
|
r34877 | old = phasenames[old] | ||
Augie Fackler
|
r43347 | return {b'node': node, b'oldphase': old, b'phase': phasenames[new]} | ||