phases.py
897 lines
| 29.7 KiB
| text/x-python
|
PythonLexer
/ mercurial / phases.py
|
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> | ||||
|
r16725 | This software may be used and distributed according to the terms | ||
of the GNU General Public License version 2 or any later version. | ||||
|
r15659 | |||
--- | ||||
This module implements most phase logic in mercurial. | ||||
Basic Concept | ||||
============= | ||||
|
r16724 | A 'changeset phase' is an indicator that tells us how a changeset is | ||
|
r16725 | manipulated and communicated. The details of each phase is described | ||
below, here we describe the properties they have in common. | ||||
|
r15659 | |||
|
r16725 | Like bookmarks, phases are not stored in history and thus are not | ||
permanent and leave no audit trail. | ||||
|
r15659 | |||
|
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. | ||||
|
r15659 | |||
These phases share a hierarchy of traits: | ||||
immutable shared | ||||
public: X X | ||||
draft: X | ||||
|
r15705 | secret: | ||
|
r15659 | |||
|
r16724 | Local commits are draft by default. | ||
|
r15659 | |||
|
r16724 | Phase Movement and Exchange | ||
=========================== | ||||
|
r15659 | |||
|
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. | ||||
|
r15659 | |||
A small list of fact/rules define the exchange of phase: | ||||
* old client never changes server states | ||||
* pull never changes server states | ||||
|
r16724 | * publish and old server changesets are seen as public by client | ||
|
r16725 | * any secret changeset seen in another repository is lowered to at | ||
least draft | ||||
|
r15659 | |||
|
r16725 | Here is the final table summing up the 49 possible use cases of phase | ||
exchange: | ||||
|
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, | ||||
|
r16725 | * X = not tracked (i.e., the old client or server has no internal | ||
way of recording the phase.) | ||||
|
r15659 | |||
passive = only pushes | ||||
A cell here can be read like this: | ||||
|
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)." | ||||
|
r15659 | |||
|
r16724 | Note: old client behave as a publishing server with draft only content | ||
|
r15659 | - other people see it as public | ||
- content is pushed as draft | ||||
""" | ||||
|
r15417 | |||
|
r25966 | from __future__ import absolute_import | ||
|
r15419 | import errno | ||
|
r34320 | import struct | ||
|
r25966 | |||
from .i18n import _ | ||||
from .node import ( | ||||
bin, | ||||
hex, | ||||
nullid, | ||||
nullrev, | ||||
short, | ||||
|
r44456 | wdirrev, | ||
|
r25966 | ) | ||
|
r43359 | from .pycompat import ( | ||
getattr, | ||||
setattr, | ||||
) | ||||
|
r25966 | from . import ( | ||
error, | ||||
|
r35310 | pycompat, | ||
|
r31016 | smartset, | ||
|
r31053 | txnutil, | ||
|
r32000 | util, | ||
|
r25966 | ) | ||
|
r15418 | |||
|
r43347 | _fphasesentry = struct.Struct(b'>i20s') | ||
|
r34320 | |||
|
r43346 | INTERNAL_FLAG = 64 # Phases for mercurial internal usage only | ||
HIDEABLE_FLAG = 32 # Phases that are hideable | ||||
|
r39333 | |||
# record phase index | ||||
public, draft, secret = range(3) | ||||
internal = INTERNAL_FLAG | HIDEABLE_FLAG | ||||
|
r40463 | archived = HIDEABLE_FLAG | ||
|
r44129 | allphases = list(range(internal + 1)) | ||
|
r15417 | trackedphases = allphases[1:] | ||
|
r39333 | # record phase names | ||
|
r43347 | cmdphasenames = [b'public', b'draft', b'secret'] # known to `hg phase` command | ||
|
r39333 | phasenames = [None] * len(allphases) | ||
|
r43346 | phasenames[: len(cmdphasenames)] = cmdphasenames | ||
|
r43347 | phasenames[archived] = b'archived' | ||
phasenames[internal] = b'internal' | ||||
|
r39333 | # record phase property | ||
|
r38174 | mutablephases = tuple(allphases[1:]) | ||
|
r38175 | remotehiddenphases = tuple(allphases[2:]) | ||
|
r39333 | localhiddenphases = tuple(p for p in allphases if p & HIDEABLE_FLAG) | ||
|
r15418 | |||
|
r43346 | |||
|
r39335 | def supportinternal(repo): | ||
"""True if the internal phase can be used on a repository""" | ||||
|
r43347 | return b'internal-phase' in repo.requirements | ||
|
r39335 | |||
|
r43346 | |||
|
r16657 | def _readroots(repo, phasedefaults=None): | ||
|
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. | ||||
""" | ||||
|
r18002 | repo = repo.unfiltered() | ||
|
r16625 | dirty = False | ||
|
r15418 | roots = [set() for i in allphases] | ||
try: | ||||
|
r43347 | f, pending = txnutil.trypending(repo.root, repo.svfs, b'phaseroots') | ||
|
r15418 | try: | ||
for line in f: | ||||
|
r16588 | phase, nh = line.split() | ||
|
r15418 | roots[int(phase)].add(bin(nh)) | ||
finally: | ||||
f.close() | ||||
|
r25660 | except IOError as inst: | ||
|
r15419 | if inst.errno != errno.ENOENT: | ||
raise | ||||
|
r16625 | if phasedefaults: | ||
for f in phasedefaults: | ||||
roots = f(repo, roots) | ||||
dirty = True | ||||
return roots, dirty | ||||
|
r15418 | |||
|
r43346 | |||
|
r34320 | def binaryencode(phasemapping): | ||
"""encode a 'phase -> nodes' mapping into a binary stream | ||||
Since phases are integer the mapping is actually a python list: | ||||
[[PUBLIC_HEADS], [DRAFTS_HEADS], [SECRET_HEADS]] | ||||
""" | ||||
binarydata = [] | ||||
for phase, nodes in enumerate(phasemapping): | ||||
for head in nodes: | ||||
binarydata.append(_fphasesentry.pack(phase, head)) | ||||
|
r43347 | return b''.join(binarydata) | ||
|
r34320 | |||
|
r43346 | |||
|
r34321 | def binarydecode(stream): | ||
"""decode a binary stream into a 'phase -> nodes' mapping | ||||
Since phases are integer the mapping is actually a python list.""" | ||||
headsbyphase = [[] for i in allphases] | ||||
entrysize = _fphasesentry.size | ||||
while True: | ||||
entry = stream.read(entrysize) | ||||
if len(entry) < entrysize: | ||||
if entry: | ||||
|
r43347 | raise error.Abort(_(b'bad phase-heads stream')) | ||
|
r34321 | break | ||
phase, node = _fphasesentry.unpack(entry) | ||||
headsbyphase[phase].append(node) | ||||
return headsbyphase | ||||
|
r43346 | |||
|
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: | ||||
data[idx - 1] = (pycompat.xrange(r1[0], r2[-1] + 1), t) | ||||
data.pop(idx) | ||||
elif merge_before: | ||||
data[idx - 1] = (pycompat.xrange(r1[0], rev + 1), t) | ||||
elif merge_after: | ||||
data[idx] = (pycompat.xrange(rev, r2[-1] + 1), t) | ||||
else: | ||||
data.insert(idx, (pycompat.xrange(rev, rev + 1), t)) | ||||
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: | ||||
data[idx] = (pycompat.xrange(rev + 1, r1[-1] + 1), t1) | ||||
_sortedrange_insert(data, idx, rev, t) | ||||
elif r1[-1] == rev: | ||||
data[idx] = (pycompat.xrange(r1[0], rev), t1) | ||||
_sortedrange_insert(data, idx + 1, rev, t) | ||||
else: | ||||
data[idx : idx + 1] = [ | ||||
(pycompat.xrange(r1[0], rev), t1), | ||||
(pycompat.xrange(rev, rev + 1), t), | ||||
(pycompat.xrange(rev + 1, r1[-1] + 1), t1), | ||||
] | ||||
|
r33451 | def _trackphasechange(data, rev, old, new): | ||
|
r45036 | """add a phase move to the <data> list of ranges | ||
|
r33451 | |||
If data is None, nothing happens. | ||||
""" | ||||
if data is None: | ||||
return | ||||
|
r45036 | |||
# If data is empty, create a one-revision range and done | ||||
if not data: | ||||
data.insert(0, (pycompat.xrange(rev, rev + 1), (old, new))) | ||||
return | ||||
low = 0 | ||||
high = len(data) | ||||
t = (old, new) | ||||
while low < high: | ||||
mid = (low + high) // 2 | ||||
revs = data[mid][0] | ||||
if rev in revs: | ||||
_sortedrange_split(data, mid, rev, t) | ||||
return | ||||
if revs[0] == rev + 1: | ||||
if mid and data[mid - 1][0][-1] == rev: | ||||
_sortedrange_split(data, mid - 1, rev, t) | ||||
else: | ||||
_sortedrange_insert(data, mid, rev, t) | ||||
return | ||||
if revs[-1] == rev - 1: | ||||
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 | ||||
if revs[0] > rev: | ||||
high = mid | ||||
else: | ||||
low = mid + 1 | ||||
if low == len(data): | ||||
data.append((pycompat.xrange(rev, rev + 1), t)) | ||||
return | ||||
r1, t1 = data[low] | ||||
if r1[0] > rev: | ||||
data.insert(low, (pycompat.xrange(rev, rev + 1), t)) | ||||
else: | ||||
data.insert(low + 1, (pycompat.xrange(rev, rev + 1), t)) | ||||
|
r33451 | |||
|
r43346 | |||
|
r16657 | class phasecache(object): | ||
|
r16658 | def __init__(self, repo, phasedefaults, _load=True): | ||
if _load: | ||||
# Cheap trick to allow shallow-copy without copy module | ||||
self.phaseroots, self.dirty = _readroots(repo, phasedefaults) | ||||
|
r35458 | self._loadedrevslen = 0 | ||
|
r25190 | self._phasesets = None | ||
|
r18220 | self.filterunknown(repo) | ||
|
r23878 | self.opener = repo.svfs | ||
|
r16658 | |||
|
r35331 | def getrevset(self, repo, phases, subset=None): | ||
|
r31016 | """return a smartset for the given phases""" | ||
|
r43346 | self.loadphaserevs(repo) # ensure phase's sets are loaded | ||
|
r35310 | phases = set(phases) | ||
|
r44521 | publicphase = public in phases | ||
|
r44456 | |||
|
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. | ||||
|
r44522 | revsneedscopy = False | ||
|
r44521 | if len(phases) == 1: | ||
[p] = phases | ||||
revs = self._phasesets[p] | ||||
|
r44522 | revsneedscopy = True # Don't modify _phasesets | ||
|
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: | ||||
|
r44522 | if revsneedscopy: | ||
wdirrevs = wdirrevs.copy() | ||||
|
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: | ||||
|
r31016 | if repo.changelog.filteredrevs: | ||
revs = revs - repo.changelog.filteredrevs | ||||
|
r44456 | |||
|
r35331 | if subset is None: | ||
return smartset.baseset(revs) | ||||
else: | ||||
|
r44521 | revs = _addwdir(subset, revs) | ||
|
r35331 | return subset & smartset.baseset(revs) | ||
|
r31016 | else: | ||
|
r35331 | if subset is None: | ||
subset = smartset.fullreposet(repo) | ||||
|
r44456 | |||
|
r44521 | revs = _addwdir(subset, revs) | ||
|
r44456 | |||
|
r35310 | if not revs: | ||
|
r35331 | return subset | ||
return subset.filter(lambda r: r not in revs) | ||||
|
r31016 | |||
|
r16658 | def copy(self): | ||
# Shallow copy meant to ensure isolation in | ||||
# advance/retractboundary(), nothing more. | ||||
|
r23631 | ph = self.__class__(None, None, _load=False) | ||
|
r16658 | ph.phaseroots = self.phaseroots[:] | ||
ph.dirty = self.dirty | ||||
ph.opener = self.opener | ||||
|
r35457 | ph._loadedrevslen = self._loadedrevslen | ||
|
r25592 | ph._phasesets = self._phasesets | ||
|
r16658 | return ph | ||
def replace(self, phcache): | ||||
|
r25613 | """replace all values in 'self' with content of phcache""" | ||
|
r43346 | for a in ( | ||
|
r43347 | b'phaseroots', | ||
b'dirty', | ||||
b'opener', | ||||
b'_loadedrevslen', | ||||
b'_phasesets', | ||||
|
r43346 | ): | ||
|
r16658 | setattr(self, a, getattr(phcache, a)) | ||
|
r16657 | |||
|
r24599 | def _getphaserevsnative(self, repo): | ||
|
r24444 | repo = repo.unfiltered() | ||
nativeroots = [] | ||||
for phase in trackedphases: | ||||
|
r43346 | nativeroots.append( | ||
pycompat.maplist(repo.changelog.rev, self.phaseroots[phase]) | ||||
) | ||||
|
r25527 | return repo.changelog.computephases(nativeroots) | ||
|
r24444 | |||
|
r24599 | def _computephaserevspure(self, repo): | ||
|
r24519 | repo = repo.unfiltered() | ||
|
r35310 | cl = repo.changelog | ||
self._phasesets = [set() for phase in allphases] | ||||
|
r39307 | lowerroots = set() | ||
for phase in reversed(trackedphases): | ||||
roots = pycompat.maplist(cl.rev, self.phaseroots[phase]) | ||||
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 | ||||
|
r35457 | self._loadedrevslen = len(cl) | ||
|
r24519 | |||
|
r25611 | def loadphaserevs(self, repo): | ||
"""ensure phase information is loaded in the object""" | ||||
|
r35310 | if self._phasesets is None: | ||
|
r24444 | try: | ||
|
r31152 | res = self._getphaserevsnative(repo) | ||
|
r35457 | self._loadedrevslen, self._phasesets = res | ||
|
r24444 | except AttributeError: | ||
|
r24599 | self._computephaserevspure(repo) | ||
|
r22894 | |||
|
r22893 | def invalidate(self): | ||
|
r35458 | self._loadedrevslen = 0 | ||
|
r25593 | self._phasesets = None | ||
|
r16657 | |||
def phase(self, repo, rev): | ||||
|
r35310 | # We need a repo argument here to be able to build _phasesets | ||
|
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 | ||||
|
r19984 | if rev < nullrev: | ||
|
r43347 | raise ValueError(_(b'cannot lookup negative revision')) | ||
|
r35457 | if rev >= self._loadedrevslen: | ||
|
r22893 | self.invalidate() | ||
|
r25611 | self.loadphaserevs(repo) | ||
|
r35310 | for phase in trackedphases: | ||
if rev in self._phasesets[phase]: | ||||
return phase | ||||
return public | ||||
|
r16657 | |||
def write(self): | ||||
if not self.dirty: | ||||
return | ||||
|
r43347 | f = self.opener(b'phaseroots', b'w', atomictemp=True, checkambig=True) | ||
|
r16657 | try: | ||
|
r22079 | self._write(f) | ||
|
r16657 | finally: | ||
f.close() | ||||
|
r22079 | |||
def _write(self, fp): | ||||
for phase, roots in enumerate(self.phaseroots): | ||||
|
r36470 | for h in sorted(roots): | ||
|
r43347 | fp.write(b'%i %s\n' % (phase, hex(h))) | ||
|
r16657 | self.dirty = False | ||
|
r15454 | |||
|
r22080 | def _updateroots(self, phase, newroots, tr): | ||
|
r16658 | self.phaseroots[phase] = newroots | ||
|
r22893 | self.invalidate() | ||
|
r16658 | self.dirty = True | ||
|
r43347 | tr.addfilegenerator(b'phase', (b'phaseroots',), self._write) | ||
tr.hookargs[b'phases_moved'] = b'1' | ||||
|
r22080 | |||
|
r33453 | def registernew(self, repo, tr, targetphase, nodes): | ||
repo = repo.unfiltered() | ||||
self._retractboundary(repo, tr, targetphase, nodes) | ||||
|
r43347 | if tr is not None and b'phases' in tr.changes: | ||
phasetracking = tr.changes[b'phases'] | ||||
|
r33453 | torev = repo.changelog.rev | ||
phase = self.phase | ||||
|
r45036 | revs = [torev(node) for node in nodes] | ||
revs.sort() | ||||
for rev in revs: | ||||
|
r33453 | revphase = phase(repo, rev) | ||
_trackphasechange(phasetracking, rev, None, revphase) | ||||
repo.invalidatevolatilesets() | ||||
|
r38218 | def advanceboundary(self, repo, tr, targetphase, nodes, dryrun=None): | ||
|
r33450 | """Set all 'nodes' to phase 'targetphase' | ||
Nodes with a phase lower than 'targetphase' are not affected. | ||||
|
r38218 | |||
If dryrun is True, no actions will be performed | ||||
Returns a set of revs whose phase is changed or should be changed | ||||
|
r33450 | """ | ||
|
r16658 | # Be careful to preserve shallow-copied values: do not update | ||
# phaseroots values, replace them. | ||||
|
r33451 | if tr is None: | ||
phasetracking = None | ||||
else: | ||||
|
r43347 | phasetracking = tr.changes.get(b'phases') | ||
|
r16658 | |||
|
r18002 | repo = repo.unfiltered() | ||
|
r33451 | |||
|
r43346 | changes = set() # set of revisions to be changed | ||
delroots = [] # set of root deleted by this path | ||||
|
r38806 | for phase in pycompat.xrange(targetphase + 1, len(allphases)): | ||
|
r16658 | # filter nodes that are not in a compatible phase already | ||
|
r43346 | nodes = [ | ||
n for n in nodes if self.phase(repo, repo[n].rev()) >= phase | ||||
] | ||||
|
r16658 | if not nodes: | ||
|
r43346 | break # no roots to move anymore | ||
|
r33450 | |||
|
r16658 | olds = self.phaseroots[phase] | ||
|
r33451 | |||
|
r43347 | affected = repo.revs(b'%ln::%ln', olds, nodes) | ||
|
r38218 | changes.update(affected) | ||
if dryrun: | ||||
continue | ||||
|
r33451 | for r in affected: | ||
|
r43346 | _trackphasechange( | ||
phasetracking, r, self.phase(repo, r), targetphase | ||||
) | ||||
|
r33450 | |||
|
r44937 | roots = { | ||
|
r43346 | ctx.node() | ||
|
r43347 | for ctx in repo.set(b'roots((%ln::) - %ld)', olds, affected) | ||
|
r44937 | } | ||
|
r16658 | if olds != roots: | ||
|
r22080 | self._updateroots(phase, roots, tr) | ||
|
r16658 | # some roots may need to be declared for lower phases | ||
delroots.extend(olds - roots) | ||||
|
r38218 | if not dryrun: | ||
# declare deleted root in the target phase | ||||
if targetphase != 0: | ||||
self._retractboundary(repo, tr, targetphase, delroots) | ||||
repo.invalidatevolatilesets() | ||||
return changes | ||||
|
r16658 | |||
|
r22070 | def retractboundary(self, repo, tr, targetphase, nodes): | ||
|
r43346 | oldroots = self.phaseroots[: targetphase + 1] | ||
|
r33458 | if tr is None: | ||
phasetracking = None | ||||
else: | ||||
|
r43347 | phasetracking = tr.changes.get(b'phases') | ||
|
r33458 | repo = repo.unfiltered() | ||
|
r43346 | if ( | ||
self._retractboundary(repo, tr, targetphase, nodes) | ||||
and phasetracking is not None | ||||
): | ||||
|
r33458 | |||
# find the affected revisions | ||||
new = self.phaseroots[targetphase] | ||||
old = oldroots[targetphase] | ||||
|
r43347 | affected = set(repo.revs(b'(%ln::) - (%ln::)', new, old)) | ||
|
r33458 | |||
# find the phase of the affected revision | ||||
|
r38806 | for phase in pycompat.xrange(targetphase, -1, -1): | ||
|
r33458 | if phase: | ||
roots = oldroots[phase] | ||||
|
r43347 | revs = set(repo.revs(b'%ln::%ld', roots, affected)) | ||
|
r33458 | affected -= revs | ||
|
r43346 | else: # public phase | ||
|
r33458 | revs = affected | ||
|
r45036 | for r in sorted(revs): | ||
|
r33458 | _trackphasechange(phasetracking, r, phase, targetphase) | ||
|
r33452 | repo.invalidatevolatilesets() | ||
def _retractboundary(self, repo, tr, targetphase, nodes): | ||||
|
r16658 | # Be careful to preserve shallow-copied values: do not update | ||
# phaseroots values, replace them. | ||||
|
r40463 | if targetphase in (archived, internal) and not supportinternal(repo): | ||
name = phasenames[targetphase] | ||||
|
r43347 | msg = b'this repository does not support the %s phase' % name | ||
|
r39335 | raise error.ProgrammingError(msg) | ||
|
r16658 | |||
|
r18002 | repo = repo.unfiltered() | ||
|
r45521 | torev = repo.changelog.rev | ||
tonode = repo.changelog.node | ||||
currentroots = {torev(node) for node in self.phaseroots[targetphase]} | ||||
|
r33457 | finalroots = oldroots = set(currentroots) | ||
|
r45521 | newroots = [torev(node) for node in nodes] | ||
|
r43346 | newroots = [ | ||
|
r45521 | rev for rev in newroots if self.phase(repo, rev) < targetphase | ||
|
r43346 | ] | ||
|
r45521 | |||
|
r16658 | if newroots: | ||
|
r45521 | if nullrev in newroots: | ||
|
r43347 | raise error.Abort(_(b'cannot change null revision phase')) | ||
|
r16658 | currentroots.update(newroots) | ||
|
r26909 | |||
# Only compute new roots for revs above the roots that are being | ||||
# retracted. | ||||
|
r45521 | minnewroot = min(newroots) | ||
aboveroots = [rev for rev in currentroots if rev >= minnewroot] | ||||
|
r45526 | updatedroots = repo.revs(b'roots(%ld::)', aboveroots) | ||
|
r26909 | |||
|
r45521 | finalroots = {rev for rev in currentroots if rev < minnewroot} | ||
|
r45526 | finalroots.update(updatedroots) | ||
|
r33457 | if finalroots != oldroots: | ||
|
r45521 | self._updateroots( | ||
targetphase, {tonode(rev) for rev in finalroots}, tr | ||||
) | ||||
|
r33457 | return True | ||
return False | ||||
|
r16658 | |||
|
r18220 | def filterunknown(self, repo): | ||
"""remove unknown nodes from the phase boundary | ||||
Nothing is lost as unknown nodes only hold data for their descendants. | ||||
""" | ||||
filtered = False | ||||
r43939 | has_node = repo.changelog.index.has_node # to filter unknown nodes | |||
|
r18220 | for phase, nodes in enumerate(self.phaseroots): | ||
r43939 | missing = sorted(node for node in nodes if not has_node(node)) | |||
|
r18220 | if missing: | ||
for mnode in missing: | ||||
repo.ui.debug( | ||||
|
r43347 | b'removing unknown node %s from %i-phase boundary\n' | ||
|
r43346 | % (short(mnode), phase) | ||
) | ||||
|
r18220 | nodes.symmetric_difference_update(missing) | ||
filtered = True | ||||
if filtered: | ||||
self.dirty = True | ||||
|
r18983 | # filterunknown is called by repo.destroyed, we may have no changes in | ||
|
r35310 | # root but _phasesets contents is certainly invalid (or at least we | ||
|
r19951 | # have not proper way to check that). related to issue 3858. | ||
|
r18983 | # | ||
|
r35310 | # The other caller is __init__ that have no _phasesets initialized | ||
|
r18983 | # anyway. If this change we should consider adding a dedicated | ||
|
r19951 | # "destroyed" function to phasecache or a proper cache key mechanism | ||
|
r18983 | # (see branchmap one) | ||
|
r22893 | self.invalidate() | ||
|
r18220 | |||
|
r43346 | |||
|
r38218 | def advanceboundary(repo, tr, targetphase, nodes, dryrun=None): | ||
|
r15454 | """Add nodes to a phase changing other nodes phases if necessary. | ||
|
r16725 | This function move boundary *forward* this means that all nodes | ||
are set in the target phase or kept in a *lower* phase. | ||||
|
r15454 | |||
|
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 | ||||
""" | ||||
|
r16658 | phcache = repo._phasecache.copy() | ||
|
r43346 | changes = phcache.advanceboundary( | ||
repo, tr, targetphase, nodes, dryrun=dryrun | ||||
) | ||||
|
r38218 | if not dryrun: | ||
repo._phasecache.replace(phcache) | ||||
return changes | ||||
|
r15482 | |||
|
r43346 | |||
|
r22070 | def retractboundary(repo, tr, targetphase, nodes): | ||
|
r16725 | """Set nodes back to a phase changing other nodes phases if | ||
necessary. | ||||
|
r15482 | |||
|
r16725 | This function move boundary *backward* this means that all nodes | ||
are set in the target phase or kept in a *higher* phase. | ||||
|
r15482 | |||
Simplify boundary to contains phase roots only.""" | ||||
|
r16658 | phcache = repo._phasecache.copy() | ||
|
r22070 | phcache.retractboundary(repo, tr, targetphase, nodes) | ||
|
r16658 | repo._phasecache.replace(phcache) | ||
|
r15648 | |||
|
r43346 | |||
|
r33453 | def registernew(repo, tr, targetphase, nodes): | ||
"""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() | ||||
phcache.registernew(repo, tr, targetphase, nodes) | ||||
repo._phasecache.replace(phcache) | ||||
|
r43346 | |||
|
r15648 | def listphases(repo): | ||
|
r16724 | """List phases root for serialization over pushkey""" | ||
|
r32000 | # Use ordered dictionary so behavior is deterministic. | ||
keys = util.sortdict() | ||||
|
r43347 | value = b'%i' % draft | ||
|
r34817 | cl = repo.unfiltered().changelog | ||
|
r16657 | for root in repo._phasecache.phaseroots[draft]: | ||
|
r34817 | if repo._phasecache.phase(repo, cl.rev(root)) <= draft: | ||
keys[hex(root)] = value | ||||
|
r15892 | |||
|
r25624 | if repo.publishing(): | ||
|
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 | ||||
|
r15648 | # | ||
|
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: | ||||
|
r15648 | # | ||
# 1) repo A push changeset X as draft to repo B | ||||
# 2) repo B make changeset X public | ||||
|
r16725 | # 3) repo B push to repo A. X is not pushed but the data that | ||
# X as now public should | ||||
|
r15648 | # | ||
|
r16725 | # The server can't handle it on it's own as it has no idea of | ||
# client phase data. | ||||
|
r43347 | keys[b'publishing'] = b'True' | ||
|
r15648 | return keys | ||
|
r43346 | |||
|
r15648 | def pushphase(repo, nhex, oldphasestr, newphasestr): | ||
|
r17535 | """List phases root for serialization over pushkey""" | ||
|
r18002 | repo = repo.unfiltered() | ||
|
r27861 | with repo.lock(): | ||
|
r15648 | currentphase = repo[nhex].phase() | ||
|
r43346 | newphase = abs(int(newphasestr)) # let's avoid negative index surprise | ||
oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise | ||||
|
r15648 | if currentphase == oldphase and newphase < oldphase: | ||
|
r43347 | with repo.transaction(b'pushkey-phase') as tr: | ||
|
r27861 | advanceboundary(repo, tr, newphase, [bin(nhex)]) | ||
|
r32822 | return True | ||
|
r16051 | elif currentphase == newphase: | ||
# raced, but got correct result | ||||
|
r32822 | return True | ||
|
r15648 | else: | ||
|
r32822 | return False | ||
|
r15649 | |||
|
r43346 | |||
|
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 | ||||
headsbyphase = [[] for i in allphases] | ||||
# No need to keep track of secret phase; any heads in the subset that | ||||
# are not mentioned are implicitly secret. | ||||
|
r39308 | for phase in allphases[:secret]: | ||
|
r43347 | revset = b"heads(%%ln & %s())" % phasenames[phase] | ||
|
r33031 | headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)] | ||
return headsbyphase | ||||
|
r43346 | |||
|
r34322 | def updatephases(repo, trgetter, headsbyphase): | ||
|
r33031 | """Updates the repo with the given phase heads""" | ||
# Now advance phase boundaries of all but secret phase | ||||
|
r34322 | # | ||
# run the update (and fetch transaction) only if there are actually things | ||||
# to update. This avoid creating empty transaction during no-op operation. | ||||
|
r33031 | for phase in allphases[:-1]: | ||
|
r43347 | revset = b'%ln - _phase(%s)' | ||
|
r39329 | heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)] | ||
|
r34322 | if heads: | ||
advanceboundary(repo, trgetter(), phase, heads) | ||||
|
r33031 | |||
|
r43346 | |||
|
r15649 | def analyzeremotephases(repo, subset, roots): | ||
"""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 | ||||
""" | ||||
|
r18002 | repo = repo.unfiltered() | ||
|
r15649 | # build list from dictionary | ||
|
r15892 | draftroots = [] | ||
r43938 | has_node = repo.changelog.index.has_node # to filter unknown nodes | |||
|
r43376 | for nhex, phase in pycompat.iteritems(roots): | ||
|
r43347 | if nhex == b'publishing': # ignore data related to publish option | ||
|
r15649 | continue | ||
node = bin(nhex) | ||||
phase = int(phase) | ||||
|
r28174 | if phase == public: | ||
|
r15892 | if node != nullid: | ||
|
r43346 | repo.ui.warn( | ||
|
r43347 | _( | ||
b'ignoring inconsistent public root' | ||||
b' from remote: %s\n' | ||||
) | ||||
|
r43346 | % nhex | ||
) | ||||
|
r28174 | elif phase == draft: | ||
r43938 | if has_node(node): | |||
|
r15892 | draftroots.append(node) | ||
else: | ||||
|
r43346 | repo.ui.warn( | ||
|
r43347 | _(b'ignoring unexpected root from remote: %i %s\n') | ||
|
r43346 | % (phase, nhex) | ||
) | ||||
|
r15649 | # compute heads | ||
|
r15954 | publicheads = newheads(repo, subset, draftroots) | ||
|
r15892 | return publicheads, draftroots | ||
|
r15649 | |||
|
r43346 | |||
|
r34820 | class remotephasessummary(object): | ||
"""summarize phase information on the remote side | ||||
:publishing: True is the remote is publishing | ||||
:publicheads: list of remote public phase heads (nodes) | ||||
:draftheads: list of remote draft phase heads (nodes) | ||||
:draftroots: list of remote draft phase root (nodes) | ||||
""" | ||||
def __init__(self, repo, remotesubset, remoteroots): | ||||
unfi = repo.unfiltered() | ||||
self._allremoteroots = remoteroots | ||||
|
r43347 | self.publishing = remoteroots.get(b'publishing', False) | ||
|
r34820 | |||
ana = analyzeremotephases(repo, remotesubset, remoteroots) | ||||
self.publicheads, self.draftroots = ana | ||||
# Get the list of all "heads" revs draft on remote | ||||
|
r43347 | dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset) | ||
|
r34820 | self.draftheads = [c.node() for c in dheads] | ||
|
r43346 | |||
|
r15954 | def newheads(repo, heads, roots): | ||
"""compute new head of a subset minus another | ||||
* `heads`: define the first subset | ||||
|
r17425 | * `roots`: define the second we subtract from the first""" | ||
|
r39182 | # prevent an import cycle | ||
# phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases | ||||
from . import dagop | ||||
repo = repo.unfiltered() | ||||
cl = repo.changelog | ||||
r43956 | rev = cl.index.get_rev | |||
|
r39182 | if not roots: | ||
return heads | ||||
|
r39260 | if not heads or heads == [nullid]: | ||
|
r39182 | return [] | ||
# The logic operated on revisions, convert arguments early for convenience | ||||
|
r44937 | new_heads = {rev(n) for n in heads if n != nullid} | ||
|
r39182 | roots = [rev(n) for n in roots] | ||
# compute the area we need to remove | ||||
|
r43347 | affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads) | ||
|
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 | ||||
|
r43346 | candidates = repo.revs( | ||
|
r43347 | b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone | ||
|
r43346 | ) | ||
|
r39182 | candidates -= affected_zone | ||
if new_heads or candidates: | ||||
# remove candidate that are ancestors of other heads | ||||
new_heads.update(candidates) | ||||
|
r43347 | prunestart = repo.revs(b"parents(%ld) and not null", new_heads) | ||
|
r39182 | pruned = dagop.reachableroots(repo, candidates, prunestart) | ||
new_heads.difference_update(pruned) | ||||
return pycompat.maplist(cl.node, sorted(new_heads)) | ||||
|
r16030 | |||
|
r43346 | |||
|
r16030 | def newcommitphase(ui): | ||
"""helper to get the target phase of new commit | ||||
Handle all possible values for the phases.new-commit options. | ||||
""" | ||||
|
r43347 | v = ui.config(b'phases', b'new-commit') | ||
|
r16030 | try: | ||
return phasenames.index(v) | ||||
except ValueError: | ||||
try: | ||||
return int(v) | ||||
except ValueError: | ||||
|
r43347 | msg = _(b"phases.new-commit: not a valid phase name ('%s')") | ||
|
r16030 | raise error.ConfigError(msg % v) | ||
|
r43346 | |||
|
r17671 | def hassecret(repo): | ||
"""utility function that check if a repo have any secret changeset.""" | ||||
|
r45623 | return bool(repo._phasecache.phaseroots[secret]) | ||
|
r34711 | |||
|
r43346 | |||
|
r34711 | def preparehookargs(node, old, new): | ||
if old is None: | ||||
|
r43347 | old = b'' | ||
|
r34711 | else: | ||
|
r34877 | old = phasenames[old] | ||
|
r43347 | return {b'node': node, b'oldphase': old, b'phase': phasenames[new]} | ||