obsolete.py
1256 lines
| 43.6 KiB
| text/x-python
|
PythonLexer
/ mercurial / obsolete.py
Pierre-Yves.David@ens-lyon.org
|
r17070 | # obsolete.py - obsolete markers handling | ||
# | ||||
# Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org> | ||||
# Logilab SA <contact@logilab.fr> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
Martin Geisler
|
r21164 | """Obsolete marker handling | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | |||
An obsolete marker maps an old changeset to a list of new | ||||
changesets. If the list of new changesets is empty, the old changeset | ||||
is said to be "killed". Otherwise, the old changeset is being | ||||
"replaced" by the new changesets. | ||||
Obsolete markers can be used to record and distribute changeset graph | ||||
Martin Geisler
|
r21164 | transformations performed by history rewrite operations, and help | ||
building new tools to reconcile conflicting rewrite actions. To | ||||
facilitate conflict resolution, markers include various annotations | ||||
Pierre-Yves.David@ens-lyon.org
|
r17070 | besides old and news changeset identifiers, such as creation date or | ||
author name. | ||||
Martin Geisler
|
r21164 | The old obsoleted changeset is called a "precursor" and possible | ||
replacements are called "successors". Markers that used changeset X as | ||||
a precursor are called "successor markers of X" because they hold | ||||
information about the successors of X. Markers that use changeset Y as | ||||
a successors are call "precursor markers of Y" because they hold | ||||
information about the precursors of Y. | ||||
Pierre-Yves David
|
r17776 | |||
Pierre-Yves David
|
r17775 | Examples: | ||
Martin Geisler
|
r21164 | - When changeset A is replaced by changeset A', one marker is stored: | ||
Pierre-Yves David
|
r17775 | |||
Martin Geisler
|
r21166 | (A, (A',)) | ||
Pierre-Yves David
|
r17775 | |||
Martin Geisler
|
r21164 | - When changesets A and B are folded into a new changeset C, two markers are | ||
Pierre-Yves David
|
r17775 | stored: | ||
(A, (C,)) and (B, (C,)) | ||||
Martin Geisler
|
r21164 | - When changeset A is simply "pruned" from the graph, a marker is created: | ||
Pierre-Yves David
|
r17775 | |||
(A, ()) | ||||
- When changeset A is split into B and C, a single marker are used: | ||||
(A, (C, C)) | ||||
Martin Geisler
|
r21164 | We use a single marker to distinguish the "split" case from the "divergence" | ||
case. If two independent operations rewrite the same changeset A in to A' and | ||||
A'', we have an error case: divergent rewriting. We can detect it because | ||||
Pierre-Yves David
|
r17775 | two markers will be created independently: | ||
(A, (B,)) and (A, (C,)) | ||||
Pierre-Yves.David@ens-lyon.org
|
r17070 | |||
Format | ||||
------ | ||||
Markers are stored in an append-only file stored in | ||||
'.hg/store/obsstore'. | ||||
The file starts with a version header: | ||||
- 1 unsigned byte: version number, starting at zero. | ||||
Pierre-Yves David
|
r22612 | The header is followed by the markers. Marker format depend of the version. See | ||
comment associated with each format for details. | ||||
Martin Geisler
|
r21164 | |||
Pierre-Yves.David@ens-lyon.org
|
r17070 | """ | ||
Yuya Nishihara
|
r26310 | import errno, struct | ||
Augie Fackler
|
r24018 | import util, base85, node, parsers | ||
Pierre-Yves David
|
r20207 | import phases | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | from i18n import _ | ||
_pack = struct.pack | ||||
_unpack = struct.unpack | ||||
Pierre-Yves David
|
r23498 | _calcsize = struct.calcsize | ||
Martin von Zweigbergk
|
r24046 | propertycache = util.propertycache | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | |||
Mads Kiilerich
|
r17424 | # the obsolete feature is not mature enough to be enabled by default. | ||
Pierre-Yves David
|
r17296 | # you have to rely on third party extension extension to enable this. | ||
_enabled = False | ||||
Pierre-Yves.David@ens-lyon.org
|
r17070 | |||
Durham Goode
|
r22951 | # Options for obsolescence | ||
createmarkersopt = 'createmarkers' | ||||
Durham Goode
|
r22952 | allowunstableopt = 'allowunstable' | ||
Durham Goode
|
r22953 | exchangeopt = 'exchange' | ||
Durham Goode
|
r22951 | |||
Pierre-Yves David
|
r17831 | ### obsolescence marker flag | ||
## bumpedfix flag | ||||
# | ||||
# When a changeset A' succeed to a changeset A which became public, we call A' | ||||
# "bumped" because it's a successors of a public changesets | ||||
# | ||||
# o A' (bumped) | ||||
# |`: | ||||
# | o A | ||||
# |/ | ||||
# o Z | ||||
# | ||||
# The way to solve this situation is to create a new changeset Ad as children | ||||
# of A. This changeset have the same content than A'. So the diff from A to A' | ||||
# is the same than the diff from A to Ad. Ad is marked as a successors of A' | ||||
# | ||||
# o Ad | ||||
# |`: | ||||
# | x A' | ||||
# |'| | ||||
# o | A | ||||
# |/ | ||||
# o Z | ||||
# | ||||
# But by transitivity Ad is also a successors of A. To avoid having Ad marked | ||||
# as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>. | ||||
Mads Kiilerich
|
r18644 | # This flag mean that the successors express the changes between the public and | ||
# bumped version and fix the situation, breaking the transitivity of | ||||
# "bumped" here. | ||||
Pierre-Yves David
|
r17831 | bumpedfix = 1 | ||
Pierre-Yves David
|
r22850 | usingsha256 = 2 | ||
Pierre-Yves David
|
r17831 | |||
Pierre-Yves David
|
r22612 | ## Parsing and writing of version "0" | ||
# | ||||
# The header is followed by the markers. Each marker is made of: | ||||
# | ||||
Pierre-Yves David
|
r22849 | # - 1 uint8 : number of new changesets "N", can be zero. | ||
Pierre-Yves David
|
r22612 | # | ||
Pierre-Yves David
|
r22849 | # - 1 uint32: metadata size "M" in bytes. | ||
Pierre-Yves David
|
r22612 | # | ||
# - 1 byte: a bit field. It is reserved for flags used in common | ||||
# obsolete marker operations, to avoid repeated decoding of metadata | ||||
# entries. | ||||
# | ||||
# - 20 bytes: obsoleted changeset identifier. | ||||
# | ||||
# - N*20 bytes: new changesets identifiers. | ||||
# | ||||
# - M bytes: metadata as a sequence of nul-terminated strings. Each | ||||
# string contains a key and a value, separated by a colon ':', without | ||||
# additional encoding. Keys cannot contain '\0' or ':' and values | ||||
# cannot contain '\0'. | ||||
_fm0version = 0 | ||||
_fm0fixed = '>BIB20s' | ||||
_fm0node = '20s' | ||||
Pierre-Yves David
|
r23498 | _fm0fsize = _calcsize(_fm0fixed) | ||
_fm0fnodesize = _calcsize(_fm0node) | ||||
Pierre-Yves David
|
r22334 | |||
Augie Fackler
|
r24014 | def _fm0readmarkers(data, off): | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | # Loop on markers | ||
l = len(data) | ||||
Pierre-Yves David
|
r22327 | while off + _fm0fsize <= l: | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | # read fixed part | ||
Pierre-Yves David
|
r22327 | cur = data[off:off + _fm0fsize] | ||
off += _fm0fsize | ||||
Pierre-Yves David
|
r22685 | numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur) | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | # read replacement | ||
sucs = () | ||||
Pierre-Yves David
|
r22685 | if numsuc: | ||
s = (_fm0fnodesize * numsuc) | ||||
Pierre-Yves.David@ens-lyon.org
|
r17070 | cur = data[off:off + s] | ||
Pierre-Yves David
|
r22685 | sucs = _unpack(_fm0node * numsuc, cur) | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | off += s | ||
# read metadata | ||||
# (metadata will be decoded on demand) | ||||
metadata = data[off:off + mdsize] | ||||
if len(metadata) != mdsize: | ||||
raise util.Abort(_('parsing obsolete marker: metadata is too ' | ||||
'short, %d bytes expected, got %d') | ||||
Pierre-Yves David
|
r17253 | % (mdsize, len(metadata))) | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | off += mdsize | ||
Pierre-Yves David
|
r22847 | metadata = _fm0decodemeta(metadata) | ||
Pierre-Yves David
|
r22222 | try: | ||
Pierre-Yves David
|
r22845 | when, offset = metadata.pop('date', '0 0').split(' ') | ||
Gregory Szorc
|
r22309 | date = float(when), int(offset) | ||
except ValueError: | ||||
Pierre-Yves David
|
r22222 | date = (0., 0) | ||
Pierre-Yves David
|
r22258 | parents = None | ||
Pierre-Yves David
|
r22845 | if 'p2' in metadata: | ||
parents = (metadata.pop('p1', None), metadata.pop('p2', None)) | ||||
elif 'p1' in metadata: | ||||
parents = (metadata.pop('p1', None),) | ||||
elif 'p0' in metadata: | ||||
Pierre-Yves David
|
r22258 | parents = () | ||
if parents is not None: | ||||
try: | ||||
parents = tuple(node.bin(p) for p in parents) | ||||
# if parent content is not a nodeid, drop the data | ||||
for p in parents: | ||||
if len(p) != 20: | ||||
parents = None | ||||
break | ||||
except TypeError: | ||||
# if content cannot be translated to nodeid drop the data. | ||||
parents = None | ||||
Pierre-Yves David
|
r22845 | metadata = tuple(sorted(metadata.iteritems())) | ||
Pierre-Yves David
|
r22222 | |||
Pierre-Yves David
|
r22258 | yield (pre, sucs, flags, metadata, date, parents) | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | |||
Pierre-Yves David
|
r22330 | def _fm0encodeonemarker(marker): | ||
pre, sucs, flags, metadata, date, parents = marker | ||||
Pierre-Yves David
|
r22850 | if flags & usingsha256: | ||
raise util.Abort(_('cannot handle sha256 with old obsstore format')) | ||||
Pierre-Yves David
|
r22845 | metadata = dict(metadata) | ||
Pierre-Yves David
|
r23002 | time, tz = date | ||
metadata['date'] = '%r %i' % (time, tz) | ||||
Pierre-Yves David
|
r22330 | if parents is not None: | ||
if not parents: | ||||
# mark that we explicitly recorded no parents | ||||
metadata['p0'] = '' | ||||
for i, p in enumerate(parents): | ||||
metadata['p%i' % (i + 1)] = node.hex(p) | ||||
Pierre-Yves David
|
r22846 | metadata = _fm0encodemeta(metadata) | ||
Pierre-Yves David
|
r22685 | numsuc = len(sucs) | ||
format = _fm0fixed + (_fm0node * numsuc) | ||||
data = [numsuc, len(metadata), flags, pre] | ||||
Pierre-Yves David
|
r22330 | data.extend(sucs) | ||
return _pack(format, *data) + metadata | ||||
Pierre-Yves David
|
r22848 | def _fm0encodemeta(meta): | ||
"""Return encoded metadata string to string mapping. | ||||
Assume no ':' in key and no '\0' in both key and value.""" | ||||
for key, value in meta.iteritems(): | ||||
if ':' in key or '\0' in key: | ||||
raise ValueError("':' and '\0' are forbidden in metadata key'") | ||||
if '\0' in value: | ||||
raise ValueError("':' is forbidden in metadata value'") | ||||
return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)]) | ||||
def _fm0decodemeta(data): | ||||
"""Return string to string dictionary from encoded version.""" | ||||
d = {} | ||||
for l in data.split('\0'): | ||||
if l: | ||||
key, value = l.split(':') | ||||
d[key] = value | ||||
return d | ||||
Pierre-Yves David
|
r22851 | ## Parsing and writing of version "1" | ||
# | ||||
# The header is followed by the markers. Each marker is made of: | ||||
# | ||||
# - uint32: total size of the marker (including this field) | ||||
# | ||||
# - float64: date in seconds since epoch | ||||
# | ||||
# - int16: timezone offset in minutes | ||||
# | ||||
# - uint16: a bit field. It is reserved for flags used in common | ||||
# obsolete marker operations, to avoid repeated decoding of metadata | ||||
# entries. | ||||
# | ||||
# - uint8: number of successors "N", can be zero. | ||||
# | ||||
# - uint8: number of parents "P", can be zero. | ||||
# | ||||
# 0: parents data stored but no parent, | ||||
# 1: one parent stored, | ||||
# 2: two parents stored, | ||||
# 3: no parent data stored | ||||
# | ||||
# - uint8: number of metadata entries M | ||||
# | ||||
# - 20 or 32 bytes: precursor changeset identifier. | ||||
# | ||||
# - N*(20 or 32) bytes: successors changesets identifiers. | ||||
# | ||||
# - P*(20 or 32) bytes: parents of the precursors changesets. | ||||
# | ||||
# - M*(uint8, uint8): size of all metadata entries (key and value) | ||||
# | ||||
# - remaining bytes: the metadata, each (key, value) pair after the other. | ||||
_fm1version = 1 | ||||
_fm1fixed = '>IdhHBBB20s' | ||||
_fm1nodesha1 = '20s' | ||||
_fm1nodesha256 = '32s' | ||||
Pierre-Yves David
|
r23499 | _fm1nodesha1size = _calcsize(_fm1nodesha1) | ||
_fm1nodesha256size = _calcsize(_fm1nodesha256) | ||||
Pierre-Yves David
|
r23498 | _fm1fsize = _calcsize(_fm1fixed) | ||
Pierre-Yves David
|
r22851 | _fm1parentnone = 3 | ||
_fm1parentshift = 14 | ||||
_fm1parentmask = (_fm1parentnone << _fm1parentshift) | ||||
_fm1metapair = 'BB' | ||||
Pierre-Yves David
|
r23498 | _fm1metapairsize = _calcsize('BB') | ||
Pierre-Yves David
|
r22851 | |||
Martin von Zweigbergk
|
r24019 | def _fm1purereadmarkers(data, off): | ||
Matt Mackall
|
r23803 | # make some global constants local for performance | ||
noneflag = _fm1parentnone | ||||
sha2flag = usingsha256 | ||||
sha1size = _fm1nodesha1size | ||||
sha2size = _fm1nodesha256size | ||||
sha1fmt = _fm1nodesha1 | ||||
sha2fmt = _fm1nodesha256 | ||||
metasize = _fm1metapairsize | ||||
metafmt = _fm1metapair | ||||
fsize = _fm1fsize | ||||
unpack = _unpack | ||||
Pierre-Yves David
|
r22851 | # Loop on markers | ||
Matt Mackall
|
r23799 | stop = len(data) - _fm1fsize | ||
Pierre-Yves David
|
r25211 | ufixed = struct.Struct(_fm1fixed).unpack | ||
Augie Fackler
|
r24018 | |||
Matt Mackall
|
r23799 | while off <= stop: | ||
Pierre-Yves David
|
r22851 | # read fixed part | ||
Matt Mackall
|
r23803 | o1 = off + fsize | ||
Matt Mackall
|
r23800 | t, secs, tz, flags, numsuc, numpar, nummeta, prec = ufixed(data[off:o1]) | ||
Matt Mackall
|
r23792 | |||
Matt Mackall
|
r23803 | if flags & sha2flag: | ||
Matt Mackall
|
r23805 | # FIXME: prec was read as a SHA1, needs to be amended | ||
Matt Mackall
|
r23801 | # read 0 or more successors | ||
Matt Mackall
|
r23804 | if numsuc == 1: | ||
o2 = o1 + sha2size | ||||
sucs = (data[o1:o2],) | ||||
else: | ||||
o2 = o1 + sha2size * numsuc | ||||
sucs = unpack(sha2fmt * numsuc, data[o1:o2]) | ||||
Matt Mackall
|
r23792 | |||
Matt Mackall
|
r23801 | # read parents | ||
Matt Mackall
|
r23803 | if numpar == noneflag: | ||
Matt Mackall
|
r23801 | o3 = o2 | ||
parents = None | ||||
Matt Mackall
|
r23804 | elif numpar == 1: | ||
o3 = o2 + sha2size | ||||
parents = (data[o2:o3],) | ||||
Matt Mackall
|
r23801 | else: | ||
Matt Mackall
|
r23803 | o3 = o2 + sha2size * numpar | ||
parents = unpack(sha2fmt * numpar, data[o2:o3]) | ||||
Matt Mackall
|
r23801 | else: | ||
# read 0 or more successors | ||||
Matt Mackall
|
r23804 | if numsuc == 1: | ||
o2 = o1 + sha1size | ||||
sucs = (data[o1:o2],) | ||||
else: | ||||
o2 = o1 + sha1size * numsuc | ||||
sucs = unpack(sha1fmt * numsuc, data[o1:o2]) | ||||
Matt Mackall
|
r23792 | |||
Matt Mackall
|
r23801 | # read parents | ||
Matt Mackall
|
r23803 | if numpar == noneflag: | ||
Matt Mackall
|
r23801 | o3 = o2 | ||
parents = None | ||||
Matt Mackall
|
r23804 | elif numpar == 1: | ||
o3 = o2 + sha1size | ||||
parents = (data[o2:o3],) | ||||
Matt Mackall
|
r23801 | else: | ||
Matt Mackall
|
r23803 | o3 = o2 + sha1size * numpar | ||
parents = unpack(sha1fmt * numpar, data[o2:o3]) | ||||
Matt Mackall
|
r23792 | |||
Pierre-Yves David
|
r22851 | # read metadata | ||
Matt Mackall
|
r23803 | off = o3 + metasize * nummeta | ||
metapairsize = unpack('>' + (metafmt * nummeta), data[o3:off]) | ||||
Pierre-Yves David
|
r22851 | metadata = [] | ||
for idx in xrange(0, len(metapairsize), 2): | ||||
Matt Mackall
|
r23798 | o1 = off + metapairsize[idx] | ||
o2 = o1 + metapairsize[idx + 1] | ||||
metadata.append((data[off:o1], data[o1:o2])) | ||||
off = o2 | ||||
Pierre-Yves David
|
r22851 | |||
Matt Mackall
|
r23800 | yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents) | ||
Pierre-Yves David
|
r22851 | |||
def _fm1encodeonemarker(marker): | ||||
pre, sucs, flags, metadata, date, parents = marker | ||||
# determine node size | ||||
_fm1node = _fm1nodesha1 | ||||
if flags & usingsha256: | ||||
_fm1node = _fm1nodesha256 | ||||
numsuc = len(sucs) | ||||
numextranodes = numsuc | ||||
if parents is None: | ||||
numpar = _fm1parentnone | ||||
else: | ||||
numpar = len(parents) | ||||
numextranodes += numpar | ||||
formatnodes = _fm1node * numextranodes | ||||
formatmeta = _fm1metapair * len(metadata) | ||||
format = _fm1fixed + formatnodes + formatmeta | ||||
# tz is stored in minutes so we divide by 60 | ||||
tz = date[1]//60 | ||||
data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre] | ||||
data.extend(sucs) | ||||
if parents is not None: | ||||
data.extend(parents) | ||||
Pierre-Yves David
|
r23498 | totalsize = _calcsize(format) | ||
Pierre-Yves David
|
r22851 | for key, value in metadata: | ||
lk = len(key) | ||||
lv = len(value) | ||||
data.append(lk) | ||||
data.append(lv) | ||||
totalsize += lk + lv | ||||
data[0] = totalsize | ||||
data = [_pack(format, *data)] | ||||
for key, value in metadata: | ||||
data.append(key) | ||||
data.append(value) | ||||
return ''.join(data) | ||||
Pierre-Yves David
|
r22848 | |||
Martin von Zweigbergk
|
r24019 | def _fm1readmarkers(data, off): | ||
native = getattr(parsers, 'fm1readmarkers', None) | ||||
if not native: | ||||
return _fm1purereadmarkers(data, off) | ||||
stop = len(data) - _fm1fsize | ||||
return native(data, off, stop) | ||||
Pierre-Yves David
|
r22331 | # mapping to read/write various marker formats | ||
# <version> -> (decoder, encoder) | ||||
Pierre-Yves David
|
r22851 | formats = {_fm0version: (_fm0readmarkers, _fm0encodeonemarker), | ||
_fm1version: (_fm1readmarkers, _fm1encodeonemarker)} | ||||
Pierre-Yves David
|
r22331 | |||
Pierre-Yves David
|
r23497 | @util.nogc | ||
Pierre-Yves David
|
r22612 | def _readmarkers(data): | ||
"""Read and enumerate markers from raw data""" | ||||
off = 0 | ||||
diskversion = _unpack('>B', data[off:off + 1])[0] | ||||
off += 1 | ||||
if diskversion not in formats: | ||||
raise util.Abort(_('parsing obsolete marker: unknown version %r') | ||||
% diskversion) | ||||
return diskversion, formats[diskversion][0](data, off) | ||||
def encodemarkers(markers, addheader=False, version=_fm0version): | ||||
# Kept separate from flushmarkers(), it will be reused for | ||||
# markers exchange. | ||||
encodeone = formats[version][1] | ||||
if addheader: | ||||
Pierre-Yves David
|
r22613 | yield _pack('>B', version) | ||
Pierre-Yves David
|
r22612 | for marker in markers: | ||
yield encodeone(marker) | ||||
Pierre-Yves.David@ens-lyon.org
|
r17072 | class marker(object): | ||
"""Wrap obsolete marker raw data""" | ||||
def __init__(self, repo, data): | ||||
# the repo argument will be used to create changectx in later version | ||||
self._repo = repo | ||||
self._data = data | ||||
self._decodedmeta = None | ||||
Augie Fackler
|
r20031 | def __hash__(self): | ||
return hash(self._data) | ||||
def __eq__(self, other): | ||||
if type(other) != type(self): | ||||
return False | ||||
return self._data == other._data | ||||
Pierre-Yves.David@ens-lyon.org
|
r17072 | def precnode(self): | ||
"""Precursor changeset node identifier""" | ||||
return self._data[0] | ||||
def succnodes(self): | ||||
"""List of successor changesets node identifiers""" | ||||
return self._data[1] | ||||
Pierre-Yves David
|
r22259 | def parentnodes(self): | ||
"""Parents of the precursors (None if not recorded)""" | ||||
return self._data[5] | ||||
Pierre-Yves.David@ens-lyon.org
|
r17072 | def metadata(self): | ||
"""Decoded metadata dictionary""" | ||||
Pierre-Yves David
|
r22845 | return dict(self._data[3]) | ||
Pierre-Yves.David@ens-lyon.org
|
r17072 | |||
def date(self): | ||||
"""Creation date as (unixtime, offset)""" | ||||
Pierre-Yves David
|
r22222 | return self._data[4] | ||
Pierre-Yves.David@ens-lyon.org
|
r17072 | |||
Pierre-Yves David
|
r22215 | def flags(self): | ||
"""The flags field of the marker""" | ||||
return self._data[2] | ||||
Martin von Zweigbergk
|
r24044 | @util.nogc | ||
def _addsuccessors(successors, markers): | ||||
for mark in markers: | ||||
successors.setdefault(mark[0], set()).add(mark) | ||||
@util.nogc | ||||
def _addprecursors(precursors, markers): | ||||
for mark in markers: | ||||
for suc in mark[1]: | ||||
precursors.setdefault(suc, set()).add(mark) | ||||
@util.nogc | ||||
def _addchildren(children, markers): | ||||
for mark in markers: | ||||
parents = mark[5] | ||||
if parents is not None: | ||||
for p in parents: | ||||
children.setdefault(p, set()).add(mark) | ||||
Martin von Zweigbergk
|
r24045 | def _checkinvalidmarkers(markers): | ||
Pierre-Yves David
|
r23973 | """search for marker with invalid data and raise error if needed | ||
Exist as a separated function to allow the evolve extension for a more | ||||
subtle handling. | ||||
""" | ||||
Martin von Zweigbergk
|
r24045 | for mark in markers: | ||
if node.nullid in mark[1]: | ||||
raise util.Abort(_('bad obsolescence marker detected: ' | ||||
'invalid successors nullid')) | ||||
Pierre-Yves David
|
r23973 | |||
Pierre-Yves.David@ens-lyon.org
|
r17070 | class obsstore(object): | ||
"""Store obsolete markers | ||||
Markers can be accessed with two mappings: | ||||
Pierre-Yves David
|
r17776 | - precursors[x] -> set(markers on precursors edges of x) | ||
- successors[x] -> set(markers on successors edges of x) | ||||
Pierre-Yves David
|
r22270 | - children[x] -> set(markers on precursors edges of children(x) | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | """ | ||
Pierre-Yves David
|
r22254 | fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents') | ||
# prec: nodeid, precursor changesets | ||||
# succs: tuple of nodeid, successor changesets (0-N length) | ||||
# flag: integer, flag field carrying modifier for the markers (see doc) | ||||
# meta: binary blob, encoded metadata dictionary | ||||
# date: (float, int) tuple, date of marker creation | ||||
# parents: (tuple of nodeid) or None, parents of precursors | ||||
# None is used when no data has been recorded | ||||
Pierre-Yves David
|
r22221 | |||
Siddharth Agarwal
|
r25669 | def __init__(self, svfs, defaultformat=_fm1version, readonly=False): | ||
Pierre-Yves David
|
r17469 | # caches for various obsolescence related cache | ||
self.caches = {} | ||||
Siddharth Agarwal
|
r25669 | self.svfs = svfs | ||
Pierre-Yves David
|
r22852 | self._version = defaultformat | ||
Durham Goode
|
r22950 | self._readonly = readonly | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | |||
Pierre-Yves.David@ens-lyon.org
|
r17073 | def __iter__(self): | ||
return iter(self._all) | ||||
Pierre-Yves David
|
r20585 | def __len__(self): | ||
return len(self._all) | ||||
Pierre-Yves.David@ens-lyon.org
|
r17075 | def __nonzero__(self): | ||
Yuya Nishihara
|
r26310 | if not self._cached('_all'): | ||
try: | ||||
return self.svfs.stat('obsstore').st_size > 1 | ||||
except OSError as inst: | ||||
if inst.errno != errno.ENOENT: | ||||
raise | ||||
# just build an empty _all list if no obsstore exists, which | ||||
# avoids further stat() syscalls | ||||
pass | ||||
Pierre-Yves.David@ens-lyon.org
|
r17075 | return bool(self._all) | ||
Pierre-Yves David
|
r22255 | def create(self, transaction, prec, succs=(), flag=0, parents=None, | ||
date=None, metadata=None): | ||||
Pierre-Yves.David@ens-lyon.org
|
r17071 | """obsolete: add a new obsolete marker | ||
* ensuring it is hashable | ||||
* check mandatory metadata | ||||
* encode metadata | ||||
Pierre-Yves David
|
r20516 | |||
If you are a human writing code creating marker you want to use the | ||||
`createmarkers` function in this module instead. | ||||
Pierre-Yves David
|
r20584 | |||
return True if a new marker have been added, False if the markers | ||||
already existed (no op). | ||||
Pierre-Yves.David@ens-lyon.org
|
r17071 | """ | ||
if metadata is None: | ||||
metadata = {} | ||||
Pierre-Yves David
|
r22222 | if date is None: | ||
if 'date' in metadata: | ||||
# as a courtesy for out-of-tree extensions | ||||
date = util.parsedate(metadata.pop('date')) | ||||
else: | ||||
Pierre-Yves David
|
r22217 | date = util.makedate() | ||
Pierre-Yves.David@ens-lyon.org
|
r17071 | if len(prec) != 20: | ||
raise ValueError(prec) | ||||
for succ in succs: | ||||
if len(succ) != 20: | ||||
Pierre-Yves David
|
r17117 | raise ValueError(succ) | ||
Pierre-Yves David
|
r22177 | if prec in succs: | ||
raise ValueError(_('in-marker cycle with %s') % node.hex(prec)) | ||||
Pierre-Yves David
|
r22845 | |||
metadata = tuple(sorted(metadata.iteritems())) | ||||
marker = (str(prec), tuple(succs), int(flag), metadata, date, parents) | ||||
Pierre-Yves David
|
r20584 | return bool(self.add(transaction, [marker])) | ||
Pierre-Yves.David@ens-lyon.org
|
r17220 | |||
def add(self, transaction, markers): | ||||
"""Add new markers to the store | ||||
Pierre-Yves.David@ens-lyon.org
|
r17070 | |||
Pierre-Yves.David@ens-lyon.org
|
r17220 | Take care of filtering duplicate. | ||
Return the number of new marker.""" | ||||
Durham Goode
|
r22950 | if self._readonly: | ||
raise util.Abort('creating obsolete markers is not enabled on this ' | ||||
'repo') | ||||
Pierre-Yves David
|
r20028 | known = set(self._all) | ||
Pierre-Yves David
|
r20030 | new = [] | ||
for m in markers: | ||||
if m not in known: | ||||
known.add(m) | ||||
new.append(m) | ||||
Pierre-Yves.David@ens-lyon.org
|
r17220 | if new: | ||
Siddharth Agarwal
|
r25669 | f = self.svfs('obsstore', 'ab') | ||
Pierre-Yves David
|
r17124 | try: | ||
Pierre-Yves David
|
r17126 | offset = f.tell() | ||
transaction.add('obsstore', offset) | ||||
Pierre-Yves.David@ens-lyon.org
|
r17219 | # offset == 0: new file - add the version header | ||
Pierre-Yves David
|
r22335 | for bytes in encodemarkers(new, offset == 0, self._version): | ||
Pierre-Yves.David@ens-lyon.org
|
r17219 | f.write(bytes) | ||
Pierre-Yves David
|
r17126 | finally: | ||
# XXX: f.close() == filecache invalidation == obsstore rebuilt. | ||||
# call 'filecacheentry.refresh()' here | ||||
Pierre-Yves David
|
r17124 | f.close() | ||
Martin von Zweigbergk
|
r24046 | self._addmarkers(new) | ||
Pierre-Yves David
|
r17469 | # new marker *may* have changed several set. invalidate the cache. | ||
self.caches.clear() | ||||
Pierre-Yves David
|
r22339 | # records the number of new markers for the transaction hooks | ||
previous = int(transaction.hookargs.get('new_obsmarkers', '0')) | ||||
transaction.hookargs['new_obsmarkers'] = str(previous + len(new)) | ||||
Pierre-Yves.David@ens-lyon.org
|
r17220 | return len(new) | ||
Pierre-Yves David
|
r17126 | |||
timeless@mozdev.org
|
r17524 | def mergemarkers(self, transaction, data): | ||
Pierre-Yves David
|
r22325 | """merge a binary stream of markers inside the obsstore | ||
Returns the number of new markers added.""" | ||||
Pierre-Yves David
|
r22332 | version, markers = _readmarkers(data) | ||
Pierre-Yves David
|
r22325 | return self.add(transaction, markers) | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | |||
Martin von Zweigbergk
|
r24046 | @propertycache | ||
Yuya Nishihara
|
r26309 | def _all(self): | ||
data = self.svfs.tryread('obsstore') | ||||
if not data: | ||||
return [] | ||||
self._version, markers = _readmarkers(data) | ||||
markers = list(markers) | ||||
_checkinvalidmarkers(markers) | ||||
return markers | ||||
@propertycache | ||||
Martin von Zweigbergk
|
r24046 | def successors(self): | ||
successors = {} | ||||
_addsuccessors(successors, self._all) | ||||
return successors | ||||
@propertycache | ||||
def precursors(self): | ||||
precursors = {} | ||||
_addprecursors(precursors, self._all) | ||||
return precursors | ||||
@propertycache | ||||
def children(self): | ||||
children = {} | ||||
_addchildren(children, self._all) | ||||
return children | ||||
def _cached(self, attr): | ||||
return attr in self.__dict__ | ||||
def _addmarkers(self, markers): | ||||
Martin von Zweigbergk
|
r24044 | markers = list(markers) # to allow repeated iteration | ||
self._all.extend(markers) | ||||
Martin von Zweigbergk
|
r24046 | if self._cached('successors'): | ||
_addsuccessors(self.successors, markers) | ||||
if self._cached('precursors'): | ||||
_addprecursors(self.precursors, markers) | ||||
if self._cached('children'): | ||||
_addchildren(self.children, markers) | ||||
Martin von Zweigbergk
|
r24045 | _checkinvalidmarkers(markers) | ||
Pierre-Yves David
|
r23973 | |||
Pierre-Yves David
|
r22271 | def relevantmarkers(self, nodes): | ||
"""return a set of all obsolescence markers relevant to a set of nodes. | ||||
"relevant" to a set of nodes mean: | ||||
- marker that use this changeset as successor | ||||
- prune marker of direct children on this changeset | ||||
- recursive application of the two rules on precursors of these markers | ||||
It is a set so you cannot rely on order.""" | ||||
pendingnodes = set(nodes) | ||||
seenmarkers = set() | ||||
seennodes = set(pendingnodes) | ||||
precursorsmarkers = self.precursors | ||||
children = self.children | ||||
while pendingnodes: | ||||
direct = set() | ||||
for current in pendingnodes: | ||||
direct.update(precursorsmarkers.get(current, ())) | ||||
pruned = [m for m in children.get(current, ()) if not m[1]] | ||||
direct.update(pruned) | ||||
direct -= seenmarkers | ||||
pendingnodes = set([m[0] for m in direct]) | ||||
seenmarkers |= direct | ||||
pendingnodes -= seennodes | ||||
seennodes |= pendingnodes | ||||
return seenmarkers | ||||
Pierre-Yves.David@ens-lyon.org
|
r17070 | |||
Pierre-Yves David
|
r22345 | def commonversion(versions): | ||
"""Return the newest version listed in both versions and our local formats. | ||||
Returns None if no common version exists. | ||||
""" | ||||
versions.sort(reverse=True) | ||||
# search for highest version known on both side | ||||
for v in versions: | ||||
if v in formats: | ||||
return v | ||||
return None | ||||
Pierre-Yves David
|
r17295 | |||
# arbitrary picked to fit into 8K limit from HTTP server | ||||
# you have to take in account: | ||||
# - the version header | ||||
# - the base85 encoding | ||||
_maxpayload = 5300 | ||||
Pierre-Yves.David@ens-lyon.org
|
r17075 | |||
Pierre-Yves David
|
r20599 | def _pushkeyescape(markers): | ||
"""encode markers into a dict suitable for pushkey exchange | ||||
Mads Kiilerich
|
r21024 | - binary data is base85 encoded | ||
- split in chunks smaller than 5300 bytes""" | ||||
Pierre-Yves David
|
r17295 | keys = {} | ||
parts = [] | ||||
currentlen = _maxpayload * 2 # ensure we create a new part | ||||
Pierre-Yves David
|
r20599 | for marker in markers: | ||
Pierre-Yves David
|
r22329 | nextdata = _fm0encodeonemarker(marker) | ||
Pierre-Yves David
|
r17295 | if (len(nextdata) + currentlen > _maxpayload): | ||
currentpart = [] | ||||
currentlen = 0 | ||||
parts.append(currentpart) | ||||
currentpart.append(nextdata) | ||||
Pierre-Yves David
|
r17304 | currentlen += len(nextdata) | ||
Pierre-Yves David
|
r17295 | for idx, part in enumerate(reversed(parts)): | ||
Pierre-Yves David
|
r22327 | data = ''.join([_pack('>B', _fm0version)] + part) | ||
Pierre-Yves David
|
r17295 | keys['dump%i' % idx] = base85.b85encode(data) | ||
return keys | ||||
Pierre-Yves.David@ens-lyon.org
|
r17073 | |||
Pierre-Yves David
|
r20599 | def listmarkers(repo): | ||
"""List markers over pushkey""" | ||||
if not repo.obsstore: | ||||
return {} | ||||
Pierre-Yves David
|
r25118 | return _pushkeyescape(sorted(repo.obsstore)) | ||
Pierre-Yves David
|
r20599 | |||
Pierre-Yves.David@ens-lyon.org
|
r17075 | def pushmarker(repo, key, old, new): | ||
"""Push markers over pushkey""" | ||||
Pierre-Yves David
|
r17295 | if not key.startswith('dump'): | ||
Pierre-Yves.David@ens-lyon.org
|
r17075 | repo.ui.warn(_('unknown key: %r') % key) | ||
return 0 | ||||
if old: | ||||
FUJIWARA Katsunori
|
r21098 | repo.ui.warn(_('unexpected old value for %r') % key) | ||
Pierre-Yves.David@ens-lyon.org
|
r17075 | return 0 | ||
data = base85.b85decode(new) | ||||
lock = repo.lock() | ||||
try: | ||||
Pierre-Yves David
|
r17126 | tr = repo.transaction('pushkey: obsolete markers') | ||
try: | ||||
repo.obsstore.mergemarkers(tr, data) | ||||
tr.close() | ||||
return 1 | ||||
finally: | ||||
tr.release() | ||||
Pierre-Yves.David@ens-lyon.org
|
r17075 | finally: | ||
lock.release() | ||||
Pierre-Yves.David@ens-lyon.org
|
r17073 | |||
Pierre-Yves David
|
r22274 | def getmarkers(repo, nodes=None): | ||
"""returns markers known in a repository | ||||
If <nodes> is specified, only markers "relevant" to those nodes are are | ||||
returned""" | ||||
if nodes is None: | ||||
rawmarkers = repo.obsstore | ||||
else: | ||||
rawmarkers = repo.obsstore.relevantmarkers(nodes) | ||||
for markerdata in rawmarkers: | ||||
Pierre-Yves.David@ens-lyon.org
|
r17073 | yield marker(repo, markerdata) | ||
Pierre-Yves David
|
r22274 | def relevantmarkers(repo, node): | ||
"""all obsolete markers relevant to some revision""" | ||||
for markerdata in repo.obsstore.relevantmarkers(node): | ||||
yield marker(repo, markerdata) | ||||
Pierre-Yves.David@ens-lyon.org
|
r17076 | def precursormarkers(ctx): | ||
Pierre-Yves David
|
r17776 | """obsolete marker marking this changeset as a successors""" | ||
Matt Harbison
|
r24335 | for data in ctx.repo().obsstore.precursors.get(ctx.node(), ()): | ||
yield marker(ctx.repo(), data) | ||||
Pierre-Yves.David@ens-lyon.org
|
r17076 | |||
def successormarkers(ctx): | ||||
Pierre-Yves David
|
r17776 | """obsolete marker making this changeset obsolete""" | ||
Matt Harbison
|
r24335 | for data in ctx.repo().obsstore.successors.get(ctx.node(), ()): | ||
yield marker(ctx.repo(), data) | ||||
Pierre-Yves.David@ens-lyon.org
|
r17076 | |||
Pierre-Yves David
|
r17831 | def allsuccessors(obsstore, nodes, ignoreflags=0): | ||
Pierre-Yves David
|
r17827 | """Yield node for every successor of <nodes>. | ||
Some successors may be unknown locally. | ||||
Pierre-Yves David
|
r17213 | |||
Pierre-Yves David
|
r20204 | This is a linear yield unsuited to detecting split changesets. It includes | ||
initial nodes too.""" | ||||
Pierre-Yves David
|
r17827 | remaining = set(nodes) | ||
Pierre-Yves David
|
r17213 | seen = set(remaining) | ||
while remaining: | ||||
current = remaining.pop() | ||||
yield current | ||||
Pierre-Yves David
|
r17776 | for mark in obsstore.successors.get(current, ()): | ||
Pierre-Yves David
|
r20203 | # ignore marker flagged with specified flag | ||
Pierre-Yves David
|
r17831 | if mark[2] & ignoreflags: | ||
continue | ||||
Pierre-Yves David
|
r17213 | for suc in mark[1]: | ||
if suc not in seen: | ||||
seen.add(suc) | ||||
remaining.add(suc) | ||||
Pierre-Yves David
|
r17469 | |||
Pierre-Yves David
|
r20206 | def allprecursors(obsstore, nodes, ignoreflags=0): | ||
"""Yield node for every precursors of <nodes>. | ||||
Some precursors may be unknown locally. | ||||
This is a linear yield unsuited to detecting folded changesets. It includes | ||||
initial nodes too.""" | ||||
remaining = set(nodes) | ||||
seen = set(remaining) | ||||
while remaining: | ||||
current = remaining.pop() | ||||
yield current | ||||
for mark in obsstore.precursors.get(current, ()): | ||||
# ignore marker flagged with specified flag | ||||
if mark[2] & ignoreflags: | ||||
continue | ||||
suc = mark[0] | ||||
if suc not in seen: | ||||
seen.add(suc) | ||||
remaining.add(suc) | ||||
Pierre-Yves David
|
r18984 | def foreground(repo, nodes): | ||
"""return all nodes in the "foreground" of other node | ||||
The foreground of a revision is anything reachable using parent -> children | ||||
Mads Kiilerich
|
r19951 | or precursor -> successor relation. It is very similar to "descendant" but | ||
Pierre-Yves David
|
r18984 | augmented with obsolescence information. | ||
Mads Kiilerich
|
r19951 | Beware that possible obsolescence cycle may result if complex situation. | ||
Pierre-Yves David
|
r18984 | """ | ||
repo = repo.unfiltered() | ||||
foreground = set(repo.set('%ln::', nodes)) | ||||
if repo.obsstore: | ||||
# We only need this complicated logic if there is obsolescence | ||||
# XXX will probably deserve an optimised revset. | ||||
nm = repo.changelog.nodemap | ||||
plen = -1 | ||||
# compute the whole set of successors or descendants | ||||
while len(foreground) != plen: | ||||
plen = len(foreground) | ||||
succs = set(c.node() for c in foreground) | ||||
mutable = [c.node() for c in foreground if c.mutable()] | ||||
succs.update(allsuccessors(repo.obsstore, mutable)) | ||||
known = (n for n in succs if n in nm) | ||||
foreground = set(repo.set('%ln::', known)) | ||||
return set(c.node() for c in foreground) | ||||
Pierre-Yves David
|
r18068 | def successorssets(repo, initialnode, cache=None): | ||
Pierre-Yves David
|
r26265 | """Return set of all latest successors of initial nodes | ||
Pierre-Yves David
|
r18068 | |||
timeless@mozdev.org
|
r26204 | The successors set of a changeset A are the group of revisions that succeed | ||
Sean Farley
|
r20277 | A. It succeeds A as a consistent whole, each revision being only a partial | ||
replacement. The successors set contains non-obsolete changesets only. | ||||
Pierre-Yves David
|
r18068 | |||
Sean Farley
|
r20277 | This function returns the full list of successor sets which is why it | ||
returns a list of tuples and not just a single tuple. Each tuple is a valid | ||||
timeless@mozdev.org
|
r26204 | successors set. Note that (A,) may be a valid successors set for changeset A | ||
Sean Farley
|
r20277 | (see below). | ||
Pierre-Yves David
|
r18068 | |||
Sean Farley
|
r20277 | In most cases, a changeset A will have a single element (e.g. the changeset | ||
A is replaced by A') in its successors set. Though, it is also common for a | ||||
changeset A to have no elements in its successor set (e.g. the changeset | ||||
has been pruned). Therefore, the returned list of successors sets will be | ||||
[(A',)] or [], respectively. | ||||
Pierre-Yves David
|
r18068 | |||
Sean Farley
|
r20277 | When a changeset A is split into A' and B', however, it will result in a | ||
successors set containing more than a single element, i.e. [(A',B')]. | ||||
Divergent changesets will result in multiple successors sets, i.e. [(A',), | ||||
(A'')]. | ||||
Pierre-Yves David
|
r18068 | |||
Sean Farley
|
r20277 | If a changeset A is not obsolete, then it will conceptually have no | ||
successors set. To distinguish this from a pruned changeset, the successor | ||||
timeless@mozdev.org
|
r26204 | set will contain itself only, i.e. [(A,)]. | ||
Pierre-Yves David
|
r18068 | |||
Sean Farley
|
r20277 | Finally, successors unknown locally are considered to be pruned (obsoleted | ||
without any successors). | ||||
The optional `cache` parameter is a dictionary that may contain precomputed | ||||
successors sets. It is meant to reuse the computation of a previous call to | ||||
`successorssets` when multiple calls are made at the same time. The cache | ||||
timeless@mozdev.org
|
r26204 | dictionary is updated in place. The caller is responsible for its life | ||
span. Code that makes multiple calls to `successorssets` *must* use this | ||||
cache mechanism or suffer terrible performance. | ||||
Sean Farley
|
r20277 | """ | ||
Pierre-Yves David
|
r18068 | |||
succmarkers = repo.obsstore.successors | ||||
# Stack of nodes we search successors sets for | ||||
toproceed = [initialnode] | ||||
# set version of above list for fast loop detection | ||||
# element added to "toproceed" must be added here | ||||
stackedset = set(toproceed) | ||||
if cache is None: | ||||
cache = {} | ||||
# This while loop is the flattened version of a recursive search for | ||||
# successors sets | ||||
# | ||||
# def successorssets(x): | ||||
# successors = directsuccessors(x) | ||||
# ss = [[]] | ||||
# for succ in directsuccessors(x): | ||||
# # product as in itertools cartesian product | ||||
# ss = product(ss, successorssets(succ)) | ||||
# return ss | ||||
# | ||||
# But we can not use plain recursive calls here: | ||||
# - that would blow the python call stack | ||||
# - obsolescence markers may have cycles, we need to handle them. | ||||
# | ||||
# The `toproceed` list act as our call stack. Every node we search | ||||
# successors set for are stacked there. | ||||
# | ||||
# The `stackedset` is set version of this stack used to check if a node is | ||||
# already stacked. This check is used to detect cycles and prevent infinite | ||||
# loop. | ||||
# | ||||
# successors set of all nodes are stored in the `cache` dictionary. | ||||
# | ||||
# After this while loop ends we use the cache to return the successors sets | ||||
# for the node requested by the caller. | ||||
while toproceed: | ||||
# Every iteration tries to compute the successors sets of the topmost | ||||
# node of the stack: CURRENT. | ||||
# | ||||
# There are four possible outcomes: | ||||
# | ||||
# 1) We already know the successors sets of CURRENT: | ||||
# -> mission accomplished, pop it from the stack. | ||||
# 2) Node is not obsolete: | ||||
# -> the node is its own successors sets. Add it to the cache. | ||||
# 3) We do not know successors set of direct successors of CURRENT: | ||||
# -> We add those successors to the stack. | ||||
# 4) We know successors sets of all direct successors of CURRENT: | ||||
# -> We can compute CURRENT successors set and add it to the | ||||
# cache. | ||||
# | ||||
current = toproceed[-1] | ||||
if current in cache: | ||||
# case (1): We already know the successors sets | ||||
stackedset.remove(toproceed.pop()) | ||||
elif current not in succmarkers: | ||||
# case (2): The node is not obsolete. | ||||
if current in repo: | ||||
# We have a valid last successors. | ||||
cache[current] = [(current,)] | ||||
else: | ||||
# Final obsolete version is unknown locally. | ||||
# Do not count that as a valid successors | ||||
cache[current] = [] | ||||
else: | ||||
# cases (3) and (4) | ||||
# | ||||
# We proceed in two phases. Phase 1 aims to distinguish case (3) | ||||
# from case (4): | ||||
# | ||||
# For each direct successors of CURRENT, we check whether its | ||||
# successors sets are known. If they are not, we stack the | ||||
# unknown node and proceed to the next iteration of the while | ||||
# loop. (case 3) | ||||
# | ||||
# During this step, we may detect obsolescence cycles: a node | ||||
# with unknown successors sets but already in the call stack. | ||||
# In such a situation, we arbitrary set the successors sets of | ||||
# the node to nothing (node pruned) to break the cycle. | ||||
# | ||||
Mads Kiilerich
|
r18644 | # If no break was encountered we proceed to phase 2. | ||
Pierre-Yves David
|
r18068 | # | ||
# Phase 2 computes successors sets of CURRENT (case 4); see details | ||||
# in phase 2 itself. | ||||
# | ||||
# Note the two levels of iteration in each phase. | ||||
# - The first one handles obsolescence markers using CURRENT as | ||||
# precursor (successors markers of CURRENT). | ||||
# | ||||
# Having multiple entry here means divergence. | ||||
# | ||||
# - The second one handles successors defined in each marker. | ||||
# | ||||
# Having none means pruned node, multiple successors means split, | ||||
# single successors are standard replacement. | ||||
# | ||||
Mads Kiilerich
|
r18365 | for mark in sorted(succmarkers[current]): | ||
Pierre-Yves David
|
r18068 | for suc in mark[1]: | ||
if suc not in cache: | ||||
if suc in stackedset: | ||||
# cycle breaking | ||||
cache[suc] = [] | ||||
else: | ||||
# case (3) If we have not computed successors sets | ||||
# of one of those successors we add it to the | ||||
# `toproceed` stack and stop all work for this | ||||
# iteration. | ||||
toproceed.append(suc) | ||||
stackedset.add(suc) | ||||
break | ||||
else: | ||||
continue | ||||
break | ||||
else: | ||||
# case (4): we know all successors sets of all direct | ||||
# successors | ||||
# | ||||
# Successors set contributed by each marker depends on the | ||||
# successors sets of all its "successors" node. | ||||
# | ||||
# Each different marker is a divergence in the obsolescence | ||||
Mads Kiilerich
|
r18644 | # history. It contributes successors sets distinct from other | ||
Pierre-Yves David
|
r18068 | # markers. | ||
# | ||||
# Within a marker, a successor may have divergent successors | ||||
# sets. In such a case, the marker will contribute multiple | ||||
# divergent successors sets. If multiple successors have | ||||
Mads Kiilerich
|
r21024 | # divergent successors sets, a Cartesian product is used. | ||
Pierre-Yves David
|
r18069 | # | ||
# At the end we post-process successors sets to remove | ||||
# duplicated entry and successors set that are strict subset of | ||||
# another one. | ||||
Pierre-Yves David
|
r18068 | succssets = [] | ||
Mads Kiilerich
|
r18365 | for mark in sorted(succmarkers[current]): | ||
Pierre-Yves David
|
r18068 | # successors sets contributed by this marker | ||
markss = [[]] | ||||
for suc in mark[1]: | ||||
Pierre-Yves David
|
r18069 | # cardinal product with previous successors | ||
Pierre-Yves David
|
r18068 | productresult = [] | ||
for prefix in markss: | ||||
for suffix in cache[suc]: | ||||
newss = list(prefix) | ||||
for part in suffix: | ||||
# do not duplicated entry in successors set | ||||
# first entry wins. | ||||
if part not in newss: | ||||
newss.append(part) | ||||
productresult.append(newss) | ||||
markss = productresult | ||||
succssets.extend(markss) | ||||
Pierre-Yves David
|
r18069 | # remove duplicated and subset | ||
seen = [] | ||||
final = [] | ||||
candidate = sorted(((set(s), s) for s in succssets if s), | ||||
key=lambda x: len(x[1]), reverse=True) | ||||
for setversion, listversion in candidate: | ||||
for seenset in seen: | ||||
if setversion.issubset(seenset): | ||||
break | ||||
else: | ||||
final.append(listversion) | ||||
seen.append(setversion) | ||||
final.reverse() # put small successors set first | ||||
cache[current] = final | ||||
Pierre-Yves David
|
r18068 | return cache[initialnode] | ||
Pierre-Yves David
|
r17828 | # mapping of 'set-name' -> <function to compute this set> | ||
Pierre-Yves David
|
r17469 | cachefuncs = {} | ||
def cachefor(name): | ||||
"""Decorator to register a function as computing the cache for a set""" | ||||
def decorator(func): | ||||
assert name not in cachefuncs | ||||
cachefuncs[name] = func | ||||
return func | ||||
return decorator | ||||
Pierre-Yves David
|
r17825 | def getrevs(repo, name): | ||
Pierre-Yves David
|
r17469 | """Return the set of revision that belong to the <name> set | ||
Such access may compute the set and cache it for future use""" | ||||
Pierre-Yves David
|
r18001 | repo = repo.unfiltered() | ||
Pierre-Yves David
|
r17469 | if not repo.obsstore: | ||
Pierre-Yves David
|
r22507 | return frozenset() | ||
Pierre-Yves David
|
r17469 | if name not in repo.obsstore.caches: | ||
repo.obsstore.caches[name] = cachefuncs[name](repo) | ||||
return repo.obsstore.caches[name] | ||||
# To be simple we need to invalidate obsolescence cache when: | ||||
# | ||||
# - new changeset is added: | ||||
# - public phase is changed | ||||
# - obsolescence marker are added | ||||
# - strip is used a repo | ||||
def clearobscaches(repo): | ||||
"""Remove all obsolescence related cache from a repo | ||||
This remove all cache in obsstore is the obsstore already exist on the | ||||
repo. | ||||
(We could be smarter here given the exact event that trigger the cache | ||||
clearing)""" | ||||
# only clear cache is there is obsstore data in this repo | ||||
if 'obsstore' in repo._filecache: | ||||
repo.obsstore.caches.clear() | ||||
@cachefor('obsolete') | ||||
def _computeobsoleteset(repo): | ||||
"""the set of obsolete revisions""" | ||||
obs = set() | ||||
Pierre-Yves David
|
r18271 | getrev = repo.changelog.nodemap.get | ||
getphase = repo._phasecache.phase | ||||
Mads Kiilerich
|
r22200 | for n in repo.obsstore.successors: | ||
rev = getrev(n) | ||||
Pierre-Yves David
|
r18271 | if rev is not None and getphase(repo, rev): | ||
Pierre-Yves David
|
r17469 | obs.add(rev) | ||
Pierre-Yves David
|
r18271 | return obs | ||
Pierre-Yves David
|
r17469 | |||
@cachefor('unstable') | ||||
def _computeunstableset(repo): | ||||
"""the set of non obsolete revisions with obsolete parents""" | ||||
Laurent Charignon
|
r24928 | revs = [(ctx.rev(), ctx) for ctx in | ||
repo.set('(not public()) and (not obsolete())')] | ||||
revs.sort(key=lambda x:x[0]) | ||||
unstable = set() | ||||
for rev, ctx in revs: | ||||
# A rev is unstable if one of its parent is obsolete or unstable | ||||
# this works since we traverse following growing rev order | ||||
Augie Fackler
|
r25149 | if any((x.obsolete() or (x.rev() in unstable)) | ||
Laurent Charignon
|
r24928 | for x in ctx.parents()): | ||
unstable.add(rev) | ||||
return unstable | ||||
Pierre-Yves David
|
r17469 | |||
@cachefor('suspended') | ||||
def _computesuspendedset(repo): | ||||
"""the set of obsolete parents with non obsolete descendants""" | ||||
Pierre-Yves David
|
r18276 | suspended = repo.changelog.ancestors(getrevs(repo, 'unstable')) | ||
return set(r for r in getrevs(repo, 'obsolete') if r in suspended) | ||||
Pierre-Yves David
|
r17469 | |||
@cachefor('extinct') | ||||
def _computeextinctset(repo): | ||||
"""the set of obsolete parents without non obsolete descendants""" | ||||
Pierre-Yves David
|
r18277 | return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended') | ||
Pierre-Yves David
|
r17474 | |||
Pierre-Yves David
|
r17828 | |||
@cachefor('bumped') | ||||
def _computebumpedset(repo): | ||||
"""the set of revs trying to obsolete public revisions""" | ||||
Pierre-Yves David
|
r20207 | bumped = set() | ||
Mads Kiilerich
|
r21024 | # util function (avoid attribute lookup in the loop) | ||
Pierre-Yves David
|
r20207 | phase = repo._phasecache.phase # would be faster to grab the full list | ||
public = phases.public | ||||
cl = repo.changelog | ||||
torev = cl.nodemap.get | ||||
Laurent Charignon
|
r24927 | for ctx in repo.set('(not public()) and (not obsolete())'): | ||
rev = ctx.rev() | ||||
Pierre-Yves David
|
r20207 | # We only evaluate mutable, non-obsolete revision | ||
Laurent Charignon
|
r24927 | node = ctx.node() | ||
# (future) A cache of precursors may worth if split is very common | ||||
for pnode in allprecursors(repo.obsstore, [node], | ||||
ignoreflags=bumpedfix): | ||||
prev = torev(pnode) # unfiltered! but so is phasecache | ||||
if (prev is not None) and (phase(repo, prev) <= public): | ||||
# we have a public precursors | ||||
bumped.add(rev) | ||||
break # Next draft! | ||||
Pierre-Yves David
|
r20207 | return bumped | ||
Pierre-Yves David
|
r17828 | |||
Pierre-Yves David
|
r18070 | @cachefor('divergent') | ||
def _computedivergentset(repo): | ||||
"""the set of rev that compete to be the final successors of some revision. | ||||
""" | ||||
divergent = set() | ||||
obsstore = repo.obsstore | ||||
newermap = {} | ||||
for ctx in repo.set('(not public()) - obsolete()'): | ||||
mark = obsstore.precursors.get(ctx.node(), ()) | ||||
toprocess = set(mark) | ||||
Pierre-Yves David
|
r24393 | seen = set() | ||
Pierre-Yves David
|
r18070 | while toprocess: | ||
prec = toprocess.pop()[0] | ||||
Pierre-Yves David
|
r24393 | if prec in seen: | ||
continue # emergency cycle hanging prevention | ||||
seen.add(prec) | ||||
Pierre-Yves David
|
r18070 | if prec not in newermap: | ||
successorssets(repo, prec, newermap) | ||||
newer = [n for n in newermap[prec] if n] | ||||
if len(newer) > 1: | ||||
divergent.add(ctx.rev()) | ||||
break | ||||
toprocess.update(obsstore.precursors.get(prec, ())) | ||||
return divergent | ||||
Pierre-Yves David
|
r22219 | def createmarkers(repo, relations, flag=0, date=None, metadata=None): | ||
Pierre-Yves David
|
r17474 | """Add obsolete markers between changesets in a repo | ||
Pierre-Yves David
|
r20517 | <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}]) | ||
Mads Kiilerich
|
r21024 | tuple. `old` and `news` are changectx. metadata is an optional dictionary | ||
Pierre-Yves David
|
r20517 | containing metadata for this marker only. It is merged with the global | ||
metadata specified through the `metadata` argument of this function, | ||||
Pierre-Yves David
|
r17474 | |||
Trying to obsolete a public changeset will raise an exception. | ||||
Current user and date are used except if specified otherwise in the | ||||
metadata attribute. | ||||
This function operates within a transaction of its own, but does | ||||
not take any lock on the repo. | ||||
""" | ||||
# prepare metadata | ||||
if metadata is None: | ||||
metadata = {} | ||||
if 'user' not in metadata: | ||||
metadata['user'] = repo.ui.username() | ||||
tr = repo.transaction('add-obsolescence-marker') | ||||
try: | ||||
Pierre-Yves David
|
r20517 | for rel in relations: | ||
prec = rel[0] | ||||
sucs = rel[1] | ||||
localmetadata = metadata.copy() | ||||
if 2 < len(rel): | ||||
localmetadata.update(rel[2]) | ||||
Pierre-Yves David
|
r17474 | if not prec.mutable(): | ||
Jordi Gutiérrez Hermoso
|
r25411 | raise util.Abort("cannot obsolete public changeset: %s" | ||
Jordi Gutiérrez Hermoso
|
r25412 | % prec, | ||
hint='see "hg help phases" for details') | ||||
Pierre-Yves David
|
r17474 | nprec = prec.node() | ||
nsucs = tuple(s.node() for s in sucs) | ||||
Pierre-Yves David
|
r22256 | npare = None | ||
if not nsucs: | ||||
npare = tuple(p.node() for p in prec.parents()) | ||||
Pierre-Yves David
|
r17474 | if nprec in nsucs: | ||
raise util.Abort("changeset %s cannot obsolete itself" % prec) | ||||
Pierre-Yves David
|
r22256 | repo.obsstore.create(tr, nprec, nsucs, flag, parents=npare, | ||
date=date, metadata=localmetadata) | ||||
Pierre-Yves David
|
r18101 | repo.filteredrevcache.clear() | ||
Pierre-Yves David
|
r17474 | tr.close() | ||
finally: | ||||
tr.release() | ||||
Durham Goode
|
r22949 | |||
def isenabled(repo, option): | ||||
"""Returns True if the given repository has the given obsolete option | ||||
enabled. | ||||
""" | ||||
result = set(repo.ui.configlist('experimental', 'evolution')) | ||||
if 'all' in result: | ||||
return True | ||||
# For migration purposes, temporarily return true if the config hasn't been | ||||
# set but _enabled is true. | ||||
if len(result) == 0 and _enabled: | ||||
return True | ||||
Durham Goode
|
r22954 | # createmarkers must be enabled if other options are enabled | ||
if ((allowunstableopt in result or exchangeopt in result) and | ||||
not createmarkersopt in result): | ||||
raise util.Abort(_("'createmarkers' obsolete option must be enabled " | ||||
"if other obsolete options are enabled")) | ||||
Durham Goode
|
r22949 | return option in result | ||