obsolete.py
1150 lines
| 36.5 KiB
| text/x-python
|
PythonLexer
/ mercurial / obsolete.py
|
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. | ||||
|
r21164 | """Obsolete marker handling | ||
|
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 | ||||
|
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 | ||||
|
r17070 | besides old and news changeset identifiers, such as creation date or | ||
author name. | ||||
|
r33700 | The old obsoleted changeset is called a "predecessor" and possible | ||
|
r21164 | replacements are called "successors". Markers that used changeset X as | ||
|
r33700 | a predecessor are called "successor markers of X" because they hold | ||
|
r21164 | information about the successors of X. Markers that use changeset Y as | ||
|
r33700 | a successors are call "predecessor markers of Y" because they hold | ||
information about the predecessors of Y. | ||||
|
r17776 | |||
|
r17775 | Examples: | ||
|
r21164 | - When changeset A is replaced by changeset A', one marker is stored: | ||
|
r17775 | |||
|
r21166 | (A, (A',)) | ||
|
r17775 | |||
|
r21164 | - When changesets A and B are folded into a new changeset C, two markers are | ||
|
r17775 | stored: | ||
(A, (C,)) and (B, (C,)) | ||||
|
r21164 | - When changeset A is simply "pruned" from the graph, a marker is created: | ||
|
r17775 | |||
(A, ()) | ||||
|
r29894 | - When changeset A is split into B and C, a single marker is used: | ||
|
r17775 | |||
|
r29894 | (A, (B, C)) | ||
|
r17775 | |||
|
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 | ||||
|
r17775 | two markers will be created independently: | ||
(A, (B,)) and (A, (C,)) | ||||
|
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. | ||||
|
r22612 | The header is followed by the markers. Marker format depend of the version. See | ||
comment associated with each format for details. | ||||
|
r21164 | |||
|
r17070 | """ | ||
|
r27332 | from __future__ import absolute_import | ||
import errno | ||||
import struct | ||||
from .i18n import _ | ||||
|
r47771 | from .node import ( | ||
bin, | ||||
hex, | ||||
) | ||||
|
r43359 | from .pycompat import getattr | ||
|
r46729 | from .node import ( | ||
bin, | ||||
hex, | ||||
) | ||||
|
r27332 | from . import ( | ||
|
r38729 | encoding, | ||
|
r27332 | error, | ||
r33143 | obsutil, | |||
|
r27332 | phases, | ||
|
r32372 | policy, | ||
|
r38730 | pycompat, | ||
|
r27332 | util, | ||
) | ||||
|
r44517 | from .utils import ( | ||
dateutil, | ||||
hashutil, | ||||
) | ||||
|
r17070 | |||
|
r43906 | parsers = policy.importmod('parsers') | ||
|
r32372 | |||
|
r17070 | _pack = struct.pack | ||
_unpack = struct.unpack | ||||
|
r23498 | _calcsize = struct.calcsize | ||
|
r24046 | propertycache = util.propertycache | ||
|
r17070 | |||
|
r22951 | # Options for obsolescence | ||
|
r43347 | createmarkersopt = b'createmarkers' | ||
allowunstableopt = b'allowunstable' | ||||
|
r47784 | allowdivergenceopt = b'allowdivergence' | ||
|
r43347 | exchangeopt = b'exchange' | ||
|
r22951 | |||
|
r43346 | |||
|
r34865 | def _getoptionvalue(repo, option): | ||
"""Returns True if the given repository has the given obsolete option | ||||
enabled. | ||||
""" | ||||
|
r43347 | configkey = b'evolution.%s' % option | ||
newconfig = repo.ui.configbool(b'experimental', configkey) | ||||
|
r34865 | |||
# Return the value only if defined | ||||
if newconfig is not None: | ||||
return newconfig | ||||
# Fallback on generic option | ||||
try: | ||||
|
r43347 | return repo.ui.configbool(b'experimental', b'evolution') | ||
|
r34865 | except (error.ConfigError, AttributeError): | ||
# Fallback on old-fashion config | ||||
# inconsistent config: experimental.evolution | ||||
|
r43347 | result = set(repo.ui.configlist(b'experimental', b'evolution')) | ||
|
r34865 | |||
|
r43347 | if b'all' in result: | ||
|
r34865 | return True | ||
# Temporary hack for next check | ||||
|
r43347 | newconfig = repo.ui.config(b'experimental', b'evolution.createmarkers') | ||
|
r34865 | if newconfig: | ||
|
r43347 | result.add(b'createmarkers') | ||
|
r34865 | |||
return option in result | ||||
|
r43346 | |||
|
r37149 | def getoptions(repo): | ||
"""Returns dicts showing state of obsolescence features.""" | ||||
createmarkersvalue = _getoptionvalue(repo, createmarkersopt) | ||||
r48599 | if createmarkersvalue: | |||
unstablevalue = _getoptionvalue(repo, allowunstableopt) | ||||
divergencevalue = _getoptionvalue(repo, allowdivergenceopt) | ||||
exchangevalue = _getoptionvalue(repo, exchangeopt) | ||||
else: | ||||
# if we cannot create obsolescence markers, we shouldn't exchange them | ||||
# or perform operations that lead to instability or divergence | ||||
unstablevalue = False | ||||
divergencevalue = False | ||||
exchangevalue = False | ||||
|
r37149 | |||
return { | ||||
createmarkersopt: createmarkersvalue, | ||||
allowunstableopt: unstablevalue, | ||||
|
r47784 | allowdivergenceopt: divergencevalue, | ||
|
r37149 | exchangeopt: exchangevalue, | ||
} | ||||
|
r43346 | |||
r32333 | def isenabled(repo, option): | |||
"""Returns True if the given repository has the given obsolete option | ||||
enabled. | ||||
""" | ||||
|
r37149 | return getoptions(repo)[option] | ||
r32333 | ||||
|
r43346 | |||
r37035 | # Creating aliases for marker flags because evolve extension looks for | |||
# bumpedfix in obsolete.py | ||||
r36971 | bumpedfix = obsutil.bumpedfix | |||
usingsha256 = obsutil.usingsha256 | ||||
|
r17831 | |||
|
r22612 | ## Parsing and writing of version "0" | ||
# | ||||
# The header is followed by the markers. Each marker is made of: | ||||
# | ||||
|
r22849 | # - 1 uint8 : number of new changesets "N", can be zero. | ||
|
r22612 | # | ||
|
r22849 | # - 1 uint32: metadata size "M" in bytes. | ||
|
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 | ||||
|
r43347 | _fm0fixed = b'>BIB20s' | ||
_fm0node = b'20s' | ||||
|
r23498 | _fm0fsize = _calcsize(_fm0fixed) | ||
_fm0fnodesize = _calcsize(_fm0node) | ||||
|
r22334 | |||
|
r43346 | |||
|
r33504 | def _fm0readmarkers(data, off, stop): | ||
|
r17070 | # Loop on markers | ||
|
r33504 | while off < stop: | ||
|
r17070 | # read fixed part | ||
|
r43346 | cur = data[off : off + _fm0fsize] | ||
|
r22327 | off += _fm0fsize | ||
|
r22685 | numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur) | ||
|
r17070 | # read replacement | ||
sucs = () | ||||
|
r22685 | if numsuc: | ||
|
r43346 | s = _fm0fnodesize * numsuc | ||
cur = data[off : off + s] | ||||
|
r22685 | sucs = _unpack(_fm0node * numsuc, cur) | ||
|
r17070 | off += s | ||
# read metadata | ||||
# (metadata will be decoded on demand) | ||||
|
r43346 | metadata = data[off : off + mdsize] | ||
|
r17070 | if len(metadata) != mdsize: | ||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b'parsing obsolete marker: metadata is too ' | ||
b'short, %d bytes expected, got %d' | ||||
|
r43346 | ) | ||
% (mdsize, len(metadata)) | ||||
) | ||||
|
r17070 | off += mdsize | ||
|
r22847 | metadata = _fm0decodemeta(metadata) | ||
|
r22222 | try: | ||
|
r43347 | when, offset = metadata.pop(b'date', b'0 0').split(b' ') | ||
|
r22309 | date = float(when), int(offset) | ||
except ValueError: | ||||
|
r43346 | date = (0.0, 0) | ||
|
r22258 | parents = None | ||
|
r43347 | if b'p2' in metadata: | ||
parents = (metadata.pop(b'p1', None), metadata.pop(b'p2', None)) | ||||
elif b'p1' in metadata: | ||||
parents = (metadata.pop(b'p1', None),) | ||||
elif b'p0' in metadata: | ||||
|
r22258 | parents = () | ||
if parents is not None: | ||||
try: | ||||
|
r46729 | parents = tuple(bin(p) for p in parents) | ||
|
r22258 | # 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 | ||||
|
r43376 | metadata = tuple(sorted(pycompat.iteritems(metadata))) | ||
|
r22222 | |||
|
r22258 | yield (pre, sucs, flags, metadata, date, parents) | ||
|
r17070 | |||
|
r43346 | |||
|
r22330 | def _fm0encodeonemarker(marker): | ||
pre, sucs, flags, metadata, date, parents = marker | ||||
|
r22850 | if flags & usingsha256: | ||
|
r43347 | raise error.Abort(_(b'cannot handle sha256 with old obsstore format')) | ||
|
r22845 | metadata = dict(metadata) | ||
|
r23002 | time, tz = date | ||
|
r43347 | metadata[b'date'] = b'%r %i' % (time, tz) | ||
|
r22330 | if parents is not None: | ||
if not parents: | ||||
# mark that we explicitly recorded no parents | ||||
|
r43347 | metadata[b'p0'] = b'' | ||
|
r32278 | for i, p in enumerate(parents, 1): | ||
|
r46729 | metadata[b'p%i' % i] = hex(p) | ||
|
r22846 | metadata = _fm0encodemeta(metadata) | ||
|
r22685 | numsuc = len(sucs) | ||
format = _fm0fixed + (_fm0node * numsuc) | ||||
data = [numsuc, len(metadata), flags, pre] | ||||
|
r22330 | data.extend(sucs) | ||
return _pack(format, *data) + metadata | ||||
|
r43346 | |||
|
r22848 | def _fm0encodemeta(meta): | ||
"""Return encoded metadata string to string mapping. | ||||
Assume no ':' in key and no '\0' in both key and value.""" | ||||
|
r43376 | for key, value in pycompat.iteritems(meta): | ||
|
r43347 | if b':' in key or b'\0' in key: | ||
raise ValueError(b"':' and '\0' are forbidden in metadata key'") | ||||
if b'\0' in value: | ||||
raise ValueError(b"':' is forbidden in metadata value'") | ||||
return b'\0'.join([b'%s:%s' % (k, meta[k]) for k in sorted(meta)]) | ||||
|
r22848 | |||
|
r43346 | |||
|
r22848 | def _fm0decodemeta(data): | ||
"""Return string to string dictionary from encoded version.""" | ||||
d = {} | ||||
|
r43347 | for l in data.split(b'\0'): | ||
|
r22848 | if l: | ||
|
r43347 | key, value = l.split(b':', 1) | ||
|
r22848 | d[key] = value | ||
return d | ||||
|
r43346 | |||
|
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 | ||||
# | ||||
|
r33700 | # - 20 or 32 bytes: predecessor changeset identifier. | ||
|
r22851 | # | ||
# - N*(20 or 32) bytes: successors changesets identifiers. | ||||
# | ||||
|
r33700 | # - P*(20 or 32) bytes: parents of the predecessors changesets. | ||
|
r22851 | # | ||
# - M*(uint8, uint8): size of all metadata entries (key and value) | ||||
# | ||||
# - remaining bytes: the metadata, each (key, value) pair after the other. | ||||
_fm1version = 1 | ||||
|
r46035 | _fm1fixed = b'>IdhHBBB' | ||
|
r43347 | _fm1nodesha1 = b'20s' | ||
_fm1nodesha256 = b'32s' | ||||
|
r23499 | _fm1nodesha1size = _calcsize(_fm1nodesha1) | ||
_fm1nodesha256size = _calcsize(_fm1nodesha256) | ||||
|
r23498 | _fm1fsize = _calcsize(_fm1fixed) | ||
|
r22851 | _fm1parentnone = 3 | ||
_fm1parentshift = 14 | ||||
|
r43346 | _fm1parentmask = _fm1parentnone << _fm1parentshift | ||
|
r43347 | _fm1metapair = b'BB' | ||
|
r33633 | _fm1metapairsize = _calcsize(_fm1metapair) | ||
|
r22851 | |||
|
r43346 | |||
|
r33504 | def _fm1purereadmarkers(data, off, stop): | ||
|
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 | ||||
|
r22851 | # Loop on markers | ||
|
r25211 | ufixed = struct.Struct(_fm1fixed).unpack | ||
|
r24018 | |||
|
r33504 | while off < stop: | ||
|
r22851 | # read fixed part | ||
|
r23803 | o1 = off + fsize | ||
|
r46035 | t, secs, tz, flags, numsuc, numpar, nummeta = ufixed(data[off:o1]) | ||
|
r23792 | |||
|
r23803 | if flags & sha2flag: | ||
|
r46035 | nodefmt = sha2fmt | ||
nodesize = sha2size | ||||
else: | ||||
nodefmt = sha1fmt | ||||
nodesize = sha1size | ||||
|
r23805 | |||
|
r46035 | (prec,) = unpack(nodefmt, data[o1 : o1 + nodesize]) | ||
o1 += nodesize | ||||
|
r23792 | |||
|
r46035 | # read 0 or more successors | ||
if numsuc == 1: | ||||
o2 = o1 + nodesize | ||||
sucs = (data[o1:o2],) | ||||
|
r23801 | else: | ||
|
r46035 | o2 = o1 + nodesize * numsuc | ||
sucs = unpack(nodefmt * numsuc, data[o1:o2]) | ||||
|
r23792 | |||
|
r46035 | # read parents | ||
if numpar == noneflag: | ||||
o3 = o2 | ||||
parents = None | ||||
elif numpar == 1: | ||||
o3 = o2 + nodesize | ||||
parents = (data[o2:o3],) | ||||
else: | ||||
o3 = o2 + nodesize * numpar | ||||
parents = unpack(nodefmt * numpar, data[o2:o3]) | ||||
|
r23792 | |||
|
r22851 | # read metadata | ||
|
r23803 | off = o3 + metasize * nummeta | ||
|
r43347 | metapairsize = unpack(b'>' + (metafmt * nummeta), data[o3:off]) | ||
|
r22851 | metadata = [] | ||
|
r38806 | for idx in pycompat.xrange(0, len(metapairsize), 2): | ||
|
r23798 | o1 = off + metapairsize[idx] | ||
o2 = o1 + metapairsize[idx + 1] | ||||
metadata.append((data[off:o1], data[o1:o2])) | ||||
off = o2 | ||||
|
r22851 | |||
|
r23800 | yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents) | ||
|
r22851 | |||
|
r43346 | |||
|
r22851 | def _fm1encodeonemarker(marker): | ||
pre, sucs, flags, metadata, date, parents = marker | ||||
# determine node size | ||||
_fm1node = _fm1nodesha1 | ||||
if flags & usingsha256: | ||||
_fm1node = _fm1nodesha256 | ||||
numsuc = len(sucs) | ||||
|
r46035 | numextranodes = 1 + numsuc | ||
|
r22851 | 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 | ||||
|
r43346 | tz = date[1] // 60 | ||
|
r22851 | data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre] | ||
data.extend(sucs) | ||||
if parents is not None: | ||||
data.extend(parents) | ||||
|
r23498 | totalsize = _calcsize(format) | ||
|
r22851 | for key, value in metadata: | ||
lk = len(key) | ||||
lv = len(value) | ||||
|
r34408 | if lk > 255: | ||
|
r43346 | msg = ( | ||
|
r43347 | b'obsstore metadata key cannot be longer than 255 bytes' | ||
b' (key "%s" is %u bytes)' | ||||
|
r43346 | ) % (key, lk) | ||
|
r34408 | raise error.ProgrammingError(msg) | ||
if lv > 255: | ||||
|
r43346 | msg = ( | ||
|
r43347 | b'obsstore metadata value cannot be longer than 255 bytes' | ||
b' (value "%s" for key "%s" is %u bytes)' | ||||
|
r43346 | ) % (value, key, lv) | ||
|
r34408 | raise error.ProgrammingError(msg) | ||
|
r22851 | 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) | ||||
|
r43347 | return b''.join(data) | ||
|
r22848 | |||
|
r43346 | |||
|
r33504 | def _fm1readmarkers(data, off, stop): | ||
|
r24019 | native = getattr(parsers, 'fm1readmarkers', None) | ||
if not native: | ||||
|
r33504 | return _fm1purereadmarkers(data, off, stop) | ||
|
r24019 | return native(data, off, stop) | ||
|
r43346 | |||
|
r22331 | # mapping to read/write various marker formats | ||
# <version> -> (decoder, encoder) | ||||
|
r43346 | formats = { | ||
_fm0version: (_fm0readmarkers, _fm0encodeonemarker), | ||||
_fm1version: (_fm1readmarkers, _fm1encodeonemarker), | ||||
} | ||||
|
r22331 | |||
|
r32689 | def _readmarkerversion(data): | ||
|
r43347 | return _unpack(b'>B', data[0:1])[0] | ||
|
r32689 | |||
|
r43346 | |||
|
r23497 | @util.nogc | ||
|
r33504 | def _readmarkers(data, off=None, stop=None): | ||
|
r22612 | """Read and enumerate markers from raw data""" | ||
|
r32689 | diskversion = _readmarkerversion(data) | ||
|
r33504 | if not off: | ||
off = 1 # skip 1 byte version number | ||||
if stop is None: | ||||
stop = len(data) | ||||
|
r22612 | if diskversion not in formats: | ||
|
r43347 | msg = _(b'parsing obsolete marker: unknown version %r') % diskversion | ||
r32591 | raise error.UnknownVersion(msg, version=diskversion) | |||
|
r33504 | return diskversion, formats[diskversion][0](data, off, stop) | ||
|
r22612 | |||
|
r43346 | |||
|
r32692 | def encodeheader(version=_fm0version): | ||
|
r43347 | return _pack(b'>B', version) | ||
|
r32692 | |||
|
r43346 | |||
|
r22612 | def encodemarkers(markers, addheader=False, version=_fm0version): | ||
# Kept separate from flushmarkers(), it will be reused for | ||||
# markers exchange. | ||||
encodeone = formats[version][1] | ||||
if addheader: | ||||
|
r32692 | yield encodeheader(version) | ||
|
r22612 | for marker in markers: | ||
yield encodeone(marker) | ||||
|
r43346 | |||
|
r24044 | @util.nogc | ||
def _addsuccessors(successors, markers): | ||||
for mark in markers: | ||||
successors.setdefault(mark[0], set()).add(mark) | ||||
|
r43346 | |||
|
r24044 | @util.nogc | ||
|
r33698 | def _addpredecessors(predecessors, markers): | ||
|
r24044 | for mark in markers: | ||
for suc in mark[1]: | ||||
|
r33698 | predecessors.setdefault(suc, set()).add(mark) | ||
|
r24044 | |||
|
r43346 | |||
|
r24044 | @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) | ||||
|
r43346 | |||
|
r47771 | def _checkinvalidmarkers(repo, markers): | ||
|
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. | ||||
""" | ||||
|
r24045 | for mark in markers: | ||
|
r47771 | if repo.nullid in mark[1]: | ||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b'bad obsolescence marker detected: ' | ||
b'invalid successors nullid' | ||||
|
r43346 | ) | ||
) | ||||
|
r23973 | |||
|
r17070 | class obsstore(object): | ||
"""Store obsolete markers | ||||
Markers can be accessed with two mappings: | ||||
|
r33700 | - predecessors[x] -> set(markers on predecessors edges of x) | ||
|
r17776 | - successors[x] -> set(markers on successors edges of x) | ||
|
r33700 | - children[x] -> set(markers on predecessors edges of children(x) | ||
|
r17070 | """ | ||
|
r43347 | fields = (b'prec', b'succs', b'flag', b'meta', b'date', b'parents') | ||
|
r33700 | # prec: nodeid, predecessors changesets | ||
|
r22254 | # succs: tuple of nodeid, successor changesets (0-N length) | ||
# flag: integer, flag field carrying modifier for the markers (see doc) | ||||
|
r38729 | # meta: binary blob in UTF-8, encoded metadata dictionary | ||
|
r22254 | # date: (float, int) tuple, date of marker creation | ||
|
r33700 | # parents: (tuple of nodeid) or None, parents of predecessors | ||
|
r22254 | # None is used when no data has been recorded | ||
|
r22221 | |||
|
r47538 | def __init__(self, repo, svfs, defaultformat=_fm1version, readonly=False): | ||
|
r17469 | # caches for various obsolescence related cache | ||
self.caches = {} | ||||
|
r25669 | self.svfs = svfs | ||
|
r47538 | self.repo = repo | ||
|
r32691 | self._defaultformat = defaultformat | ||
|
r22950 | self._readonly = readonly | ||
|
r17070 | |||
|
r17073 | def __iter__(self): | ||
return iter(self._all) | ||||
|
r20585 | def __len__(self): | ||
return len(self._all) | ||||
|
r17075 | def __nonzero__(self): | ||
|
r43906 | if not self._cached('_all'): | ||
|
r26310 | try: | ||
|
r43347 | return self.svfs.stat(b'obsstore').st_size > 1 | ||
|
r26310 | 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 | ||||
|
r17075 | return bool(self._all) | ||
|
r31476 | __bool__ = __nonzero__ | ||
|
r26684 | @property | ||
def readonly(self): | ||||
"""True if marker creation is disabled | ||||
Remove me in the future when obsolete marker is always on.""" | ||||
return self._readonly | ||||
|
r43346 | def create( | ||
self, | ||||
transaction, | ||||
prec, | ||||
succs=(), | ||||
flag=0, | ||||
parents=None, | ||||
date=None, | ||||
metadata=None, | ||||
ui=None, | ||||
): | ||||
|
r17071 | """obsolete: add a new obsolete marker | ||
* ensuring it is hashable | ||||
* check mandatory metadata | ||||
* encode metadata | ||||
|
r20516 | |||
If you are a human writing code creating marker you want to use the | ||||
`createmarkers` function in this module instead. | ||||
|
r20584 | |||
return True if a new marker have been added, False if the markers | ||||
already existed (no op). | ||||
|
r17071 | """ | ||
|
r46035 | flag = int(flag) | ||
|
r17071 | if metadata is None: | ||
metadata = {} | ||||
|
r22222 | if date is None: | ||
|
r43347 | if b'date' in metadata: | ||
|
r22222 | # as a courtesy for out-of-tree extensions | ||
|
r43347 | date = dateutil.parsedate(metadata.pop(b'date')) | ||
|
r32411 | elif ui is not None: | ||
|
r43347 | date = ui.configdate(b'devel', b'default-date') | ||
|
r32411 | if date is None: | ||
|
r36625 | date = dateutil.makedate() | ||
|
r22222 | else: | ||
|
r36625 | date = dateutil.makedate() | ||
|
r46035 | if flag & usingsha256: | ||
if len(prec) != 32: | ||||
raise ValueError(prec) | ||||
for succ in succs: | ||||
if len(succ) != 32: | ||||
raise ValueError(succ) | ||||
else: | ||||
if len(prec) != 20: | ||||
raise ValueError(prec) | ||||
for succ in succs: | ||||
if len(succ) != 20: | ||||
raise ValueError(succ) | ||||
|
r22177 | if prec in succs: | ||
|
r40200 | raise ValueError( | ||
|
r46729 | 'in-marker cycle with %s' % pycompat.sysstr(hex(prec)) | ||
|
r43346 | ) | ||
|
r22845 | |||
|
r43376 | metadata = tuple(sorted(pycompat.iteritems(metadata))) | ||
|
r38730 | for k, v in metadata: | ||
try: | ||||
# might be better to reject non-ASCII keys | ||||
k.decode('utf-8') | ||||
v.decode('utf-8') | ||||
except UnicodeDecodeError: | ||||
raise error.ProgrammingError( | ||||
|
r43347 | b'obsstore metadata must be valid UTF-8 sequence ' | ||
b'(key = %r, value = %r)' | ||||
|
r43346 | % (pycompat.bytestr(k), pycompat.bytestr(v)) | ||
) | ||||
|
r22845 | |||
|
r46035 | marker = (bytes(prec), tuple(succs), flag, metadata, date, parents) | ||
|
r20584 | return bool(self.add(transaction, [marker])) | ||
|
r17220 | |||
def add(self, transaction, markers): | ||||
"""Add new markers to the store | ||||
|
r17070 | |||
|
r17220 | Take care of filtering duplicate. | ||
Return the number of new marker.""" | ||||
|
r22950 | if self._readonly: | ||
|
r43346 | raise error.Abort( | ||
|
r43387 | _(b'creating obsolete markers is not enabled on this repo') | ||
|
r43346 | ) | ||
|
r32774 | known = set() | ||
getsuccessors = self.successors.get | ||||
|
r20030 | new = [] | ||
for m in markers: | ||||
|
r32774 | if m not in getsuccessors(m[0], ()) and m not in known: | ||
|
r20030 | known.add(m) | ||
new.append(m) | ||||
|
r17220 | if new: | ||
|
r43347 | f = self.svfs(b'obsstore', b'ab') | ||
|
r17124 | try: | ||
|
r17126 | offset = f.tell() | ||
|
r43347 | transaction.add(b'obsstore', offset) | ||
|
r17219 | # offset == 0: new file - add the version header | ||
|
r33479 | data = b''.join(encodemarkers(new, offset == 0, self._version)) | ||
f.write(data) | ||||
|
r17126 | finally: | ||
# XXX: f.close() == filecache invalidation == obsstore rebuilt. | ||||
# call 'filecacheentry.refresh()' here | ||||
|
r17124 | f.close() | ||
|
r43347 | addedmarkers = transaction.changes.get(b'obsmarkers') | ||
r33248 | if addedmarkers is not None: | |||
addedmarkers.update(new) | ||||
|
r33479 | self._addmarkers(new, data) | ||
|
r17469 | # new marker *may* have changed several set. invalidate the cache. | ||
self.caches.clear() | ||||
|
r22339 | # records the number of new markers for the transaction hooks | ||
|
r43347 | previous = int(transaction.hookargs.get(b'new_obsmarkers', b'0')) | ||
transaction.hookargs[b'new_obsmarkers'] = b'%d' % (previous + len(new)) | ||||
|
r17220 | return len(new) | ||
|
r17126 | |||
|
r17524 | def mergemarkers(self, transaction, data): | ||
|
r22325 | """merge a binary stream of markers inside the obsstore | ||
Returns the number of new markers added.""" | ||||
|
r22332 | version, markers = _readmarkers(data) | ||
|
r22325 | return self.add(transaction, markers) | ||
|
r17070 | |||
|
r24046 | @propertycache | ||
|
r32690 | def _data(self): | ||
|
r43347 | return self.svfs.tryread(b'obsstore') | ||
|
r32690 | |||
@propertycache | ||||
|
r32691 | def _version(self): | ||
if len(self._data) >= 1: | ||||
return _readmarkerversion(self._data) | ||||
else: | ||||
return self._defaultformat | ||||
@propertycache | ||||
|
r26309 | def _all(self): | ||
|
r32690 | data = self._data | ||
|
r26309 | if not data: | ||
return [] | ||||
self._version, markers = _readmarkers(data) | ||||
markers = list(markers) | ||||
|
r47771 | _checkinvalidmarkers(self.repo, markers) | ||
|
r26309 | return markers | ||
@propertycache | ||||
|
r24046 | def successors(self): | ||
successors = {} | ||||
_addsuccessors(successors, self._all) | ||||
return successors | ||||
@propertycache | ||||
|
r33699 | def predecessors(self): | ||
|
r33698 | predecessors = {} | ||
_addpredecessors(predecessors, self._all) | ||||
return predecessors | ||||
|
r24046 | |||
@propertycache | ||||
def children(self): | ||||
children = {} | ||||
_addchildren(children, self._all) | ||||
return children | ||||
def _cached(self, attr): | ||||
return attr in self.__dict__ | ||||
|
r33479 | def _addmarkers(self, markers, rawdata): | ||
|
r43346 | markers = list(markers) # to allow repeated iteration | ||
|
r33479 | self._data = self._data + rawdata | ||
|
r24044 | self._all.extend(markers) | ||
|
r43906 | if self._cached('successors'): | ||
|
r24046 | _addsuccessors(self.successors, markers) | ||
|
r43906 | if self._cached('predecessors'): | ||
|
r33699 | _addpredecessors(self.predecessors, markers) | ||
|
r43906 | if self._cached('children'): | ||
|
r24046 | _addchildren(self.children, markers) | ||
|
r47771 | _checkinvalidmarkers(self.repo, markers) | ||
|
r23973 | |||
|
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 | ||||
|
r33700 | - recursive application of the two rules on predecessors of these | ||
markers | ||||
|
r22271 | |||
It is a set so you cannot rely on order.""" | ||||
pendingnodes = set(nodes) | ||||
seenmarkers = set() | ||||
seennodes = set(pendingnodes) | ||||
|
r33699 | precursorsmarkers = self.predecessors | ||
r32488 | succsmarkers = self.successors | |||
|
r22271 | 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) | ||||
r32488 | pruned = [m for m in succsmarkers.get(current, ()) if not m[1]] | |||
direct.update(pruned) | ||||
|
r22271 | direct -= seenmarkers | ||
|
r42224 | pendingnodes = {m[0] for m in direct} | ||
|
r22271 | seenmarkers |= direct | ||
pendingnodes -= seennodes | ||||
seennodes |= pendingnodes | ||||
return seenmarkers | ||||
|
r17070 | |||
|
r43346 | |||
|
r32729 | def makestore(ui, repo): | ||
"""Create an obsstore instance from a repo.""" | ||||
# read default format for new obsstore. | ||||
# developer config: format.obsstore-version | ||||
|
r43347 | defaultformat = ui.configint(b'format', b'obsstore-version') | ||
|
r32729 | # rely on obsstore class default when possible. | ||
kwargs = {} | ||||
if defaultformat is not None: | ||||
|
r43906 | kwargs['defaultformat'] = defaultformat | ||
|
r32729 | readonly = not isenabled(repo, createmarkersopt) | ||
|
r47538 | store = obsstore(repo, repo.svfs, readonly=readonly, **kwargs) | ||
|
r32729 | if store and readonly: | ||
|
r43346 | ui.warn( | ||
|
r43347 | _(b'obsolete feature not enabled but %i markers found!\n') | ||
|
r43346 | % len(list(store)) | ||
) | ||||
|
r32729 | return store | ||
|
r43346 | |||
|
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 | ||||
|
r17295 | |||
|
r43346 | |||
|
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 | ||||
|
r17075 | |||
|
r43346 | |||
|
r20599 | def _pushkeyescape(markers): | ||
"""encode markers into a dict suitable for pushkey exchange | ||||
|
r21024 | - binary data is base85 encoded | ||
- split in chunks smaller than 5300 bytes""" | ||||
|
r17295 | keys = {} | ||
parts = [] | ||||
currentlen = _maxpayload * 2 # ensure we create a new part | ||||
|
r20599 | for marker in markers: | ||
|
r22329 | nextdata = _fm0encodeonemarker(marker) | ||
|
r43346 | if len(nextdata) + currentlen > _maxpayload: | ||
|
r17295 | currentpart = [] | ||
currentlen = 0 | ||||
parts.append(currentpart) | ||||
currentpart.append(nextdata) | ||||
|
r17304 | currentlen += len(nextdata) | ||
|
r17295 | for idx, part in enumerate(reversed(parts)): | ||
|
r43347 | data = b''.join([_pack(b'>B', _fm0version)] + part) | ||
keys[b'dump%i' % idx] = util.b85encode(data) | ||||
|
r17295 | return keys | ||
|
r17073 | |||
|
r43346 | |||
|
r20599 | def listmarkers(repo): | ||
"""List markers over pushkey""" | ||||
if not repo.obsstore: | ||||
return {} | ||||
|
r25118 | return _pushkeyescape(sorted(repo.obsstore)) | ||
|
r20599 | |||
|
r43346 | |||
|
r17075 | def pushmarker(repo, key, old, new): | ||
"""Push markers over pushkey""" | ||||
|
r43347 | if not key.startswith(b'dump'): | ||
repo.ui.warn(_(b'unknown key: %r') % key) | ||||
|
r32822 | return False | ||
|
r17075 | if old: | ||
|
r43347 | repo.ui.warn(_(b'unexpected old value for %r') % key) | ||
|
r32822 | return False | ||
|
r32200 | data = util.b85decode(new) | ||
|
r43347 | with repo.lock(), repo.transaction(b'pushkey: obsolete markers') as tr: | ||
|
r35592 | repo.obsstore.mergemarkers(tr, data) | ||
repo.invalidatevolatilesets() | ||||
return True | ||||
|
r17073 | |||
|
r43346 | |||
|
r17828 | # mapping of 'set-name' -> <function to compute this set> | ||
|
r17469 | cachefuncs = {} | ||
|
r43346 | |||
|
r17469 | def cachefor(name): | ||
"""Decorator to register a function as computing the cache for a set""" | ||||
|
r43346 | |||
|
r17469 | def decorator(func): | ||
|
r32884 | if name in cachefuncs: | ||
|
r43347 | msg = b"duplicated registration for volatileset '%s' (existing: %r)" | ||
|
r32884 | raise error.ProgrammingError(msg % (name, cachefuncs[name])) | ||
|
r17469 | cachefuncs[name] = func | ||
return func | ||||
|
r43346 | |||
|
r17469 | return decorator | ||
|
r43346 | |||
|
r17825 | def getrevs(repo, name): | ||
|
r17469 | """Return the set of revision that belong to the <name> set | ||
Such access may compute the set and cache it for future use""" | ||||
|
r18001 | repo = repo.unfiltered() | ||
|
r43534 | with util.timedcm('getrevs %s', name): | ||
if not repo.obsstore: | ||||
return frozenset() | ||||
if name not in repo.obsstore.caches: | ||||
repo.obsstore.caches[name] = cachefuncs[name](repo) | ||||
return repo.obsstore.caches[name] | ||||
|
r17469 | |||
|
r43346 | |||
|
r17469 | # 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 | ||||
|
r43347 | if b'obsstore' in repo._filecache: | ||
|
r17469 | repo.obsstore.caches.clear() | ||
|
r43346 | |||
r33124 | def _mutablerevs(repo): | |||
"""the set of mutable revision in the repository""" | ||||
|
r38174 | return repo._phasecache.getrevset(repo, phases.mutablephases) | ||
r33124 | ||||
|
r43346 | |||
|
r43347 | @cachefor(b'obsolete') | ||
|
r17469 | def _computeobsoleteset(repo): | ||
"""the set of obsolete revisions""" | ||||
|
r27784 | getnode = repo.changelog.node | ||
r33124 | notpublic = _mutablerevs(repo) | |||
|
r32688 | isobs = repo.obsstore.successors.__contains__ | ||
|
r44937 | obs = {r for r in notpublic if isobs(getnode(r))} | ||
|
r18271 | return obs | ||
|
r17469 | |||
|
r43346 | |||
|
r43347 | @cachefor(b'orphan') | ||
|
r33772 | def _computeorphanset(repo): | ||
|
r17469 | """the set of non obsolete revisions with obsolete parents""" | ||
r33125 | pfunc = repo.changelog.parentrevs | |||
mutable = _mutablerevs(repo) | ||||
|
r43347 | obsolete = getrevs(repo, b'obsolete') | ||
r33125 | others = mutable - obsolete | |||
|
r24928 | unstable = set() | ||
r33125 | for r in sorted(others): | |||
|
r24928 | # A rev is unstable if one of its parent is obsolete or unstable | ||
# this works since we traverse following growing rev order | ||||
r33125 | for p in pfunc(r): | |||
if p in obsolete or p in unstable: | ||||
unstable.add(r) | ||||
break | ||||
|
r24928 | return unstable | ||
|
r17469 | |||
|
r43346 | |||
|
r43347 | @cachefor(b'suspended') | ||
|
r17469 | def _computesuspendedset(repo): | ||
"""the set of obsolete parents with non obsolete descendants""" | ||||
|
r43347 | suspended = repo.changelog.ancestors(getrevs(repo, b'orphan')) | ||
|
r44937 | return {r for r in getrevs(repo, b'obsolete') if r in suspended} | ||
|
r17469 | |||
|
r43346 | |||
|
r43347 | @cachefor(b'extinct') | ||
|
r17469 | def _computeextinctset(repo): | ||
"""the set of obsolete parents without non obsolete descendants""" | ||||
|
r43347 | return getrevs(repo, b'obsolete') - getrevs(repo, b'suspended') | ||
|
r17474 | |||
|
r43346 | |||
|
r43347 | @cachefor(b'phasedivergent') | ||
|
r33774 | def _computephasedivergentset(repo): | ||
|
r17828 | """the set of revs trying to obsolete public revisions""" | ||
|
r20207 | bumped = set() | ||
|
r21024 | # util function (avoid attribute lookup in the loop) | ||
|
r43346 | phase = repo._phasecache.phase # would be faster to grab the full list | ||
|
r20207 | public = phases.public | ||
cl = repo.changelog | ||||
r43958 | torev = cl.index.get_rev | |||
|
r35134 | tonode = cl.node | ||
|
r40498 | obsstore = repo.obsstore | ||
|
r43347 | for rev in repo.revs(b'(not public()) and (not obsolete())'): | ||
|
r20207 | # We only evaluate mutable, non-obsolete revision | ||
|
r35134 | node = tonode(rev) | ||
|
r33700 | # (future) A cache of predecessors may worth if split is very common | ||
|
r43346 | for pnode in obsutil.allpredecessors( | ||
obsstore, [node], ignoreflags=bumpedfix | ||||
): | ||||
prev = torev(pnode) # unfiltered! but so is phasecache | ||||
|
r24927 | if (prev is not None) and (phase(repo, prev) <= public): | ||
|
r33700 | # we have a public predecessor | ||
|
r24927 | bumped.add(rev) | ||
|
r43346 | break # Next draft! | ||
|
r20207 | return bumped | ||
|
r17828 | |||
|
r43346 | |||
|
r43347 | @cachefor(b'contentdivergent') | ||
|
r33773 | def _computecontentdivergentset(repo): | ||
|
r46554 | """the set of rev that compete to be the final successors of some revision.""" | ||
|
r18070 | divergent = set() | ||
obsstore = repo.obsstore | ||||
newermap = {} | ||||
|
r35135 | tonode = repo.changelog.node | ||
|
r43347 | for rev in repo.revs(b'(not public()) - obsolete()'): | ||
|
r35135 | node = tonode(rev) | ||
mark = obsstore.predecessors.get(node, ()) | ||||
|
r18070 | toprocess = set(mark) | ||
|
r24393 | seen = set() | ||
|
r18070 | while toprocess: | ||
prec = toprocess.pop()[0] | ||||
|
r24393 | if prec in seen: | ||
|
r43346 | continue # emergency cycle hanging prevention | ||
|
r24393 | seen.add(prec) | ||
|
r18070 | if prec not in newermap: | ||
|
r33273 | obsutil.successorssets(repo, prec, cache=newermap) | ||
|
r18070 | newer = [n for n in newermap[prec] if n] | ||
if len(newer) > 1: | ||||
|
r35135 | divergent.add(rev) | ||
|
r18070 | break | ||
|
r33699 | toprocess.update(obsstore.predecessors.get(prec, ())) | ||
|
r18070 | return divergent | ||
|
r43346 | |||
|
r40078 | def makefoldid(relation, user): | ||
|
r44517 | folddigest = hashutil.sha1(user) | ||
|
r40078 | for p in relation[0] + relation[1]: | ||
|
r43347 | folddigest.update(b'%d' % p.rev()) | ||
|
r40078 | folddigest.update(p.node()) | ||
# Since fold only has to compete against fold for the same successors, it | ||||
# seems fine to use a small ID. Smaller ID save space. | ||||
|
r46729 | return hex(folddigest.digest())[:8] | ||
|
r18070 | |||
|
r43346 | |||
def createmarkers( | ||||
repo, relations, flag=0, date=None, metadata=None, operation=None | ||||
): | ||||
|
r17474 | """Add obsolete markers between changesets in a repo | ||
|
r39958 | <relations> must be an iterable of ((<old>,...), (<new>, ...)[,{metadata}]) | ||
|
r21024 | tuple. `old` and `news` are changectx. metadata is an optional dictionary | ||
|
r20517 | containing metadata for this marker only. It is merged with the global | ||
|
r38729 | metadata specified through the `metadata` argument of this function. | ||
Any string values in metadata must be UTF-8 bytes. | ||||
|
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 = {} | ||||
|
r43347 | if b'user' not in metadata: | ||
luser = ( | ||||
repo.ui.config(b'devel', b'user.obsmarker') or repo.ui.username() | ||||
) | ||||
metadata[b'user'] = encoding.fromlocal(luser) | ||||
|
r34389 | |||
# Operation metadata handling | ||||
|
r43346 | useoperation = repo.ui.configbool( | ||
|
r43347 | b'experimental', b'evolution.track-operation' | ||
|
r43346 | ) | ||
r32354 | if useoperation and operation: | |||
|
r43347 | metadata[b'operation'] = operation | ||
|
r34389 | |||
|
r34414 | # Effect flag metadata handling | ||
|
r43346 | saveeffectflag = repo.ui.configbool( | ||
|
r43347 | b'experimental', b'evolution.effect-flags' | ||
|
r43346 | ) | ||
|
r34414 | |||
|
r43347 | with repo.transaction(b'add-obsolescence-marker') as tr: | ||
|
r27984 | markerargs = [] | ||
|
r20517 | for rel in relations: | ||
|
r39958 | predecessors = rel[0] | ||
if not isinstance(predecessors, tuple): | ||||
# preserve compat with old API until all caller are migrated | ||||
predecessors = (predecessors,) | ||||
|
r40065 | if len(predecessors) > 1 and len(rel[1]) != 1: | ||
|
r43347 | msg = b'Fold markers can only have 1 successors, not %d' | ||
|
r39958 | raise error.ProgrammingError(msg % len(rel[1])) | ||
|
r40078 | foldid = None | ||
foldsize = len(predecessors) | ||||
if 1 < foldsize: | ||||
|
r43347 | foldid = makefoldid(rel, metadata[b'user']) | ||
|
r40078 | for foldidx, prec in enumerate(predecessors, 1): | ||
|
r39957 | sucs = rel[1] | ||
localmetadata = metadata.copy() | ||||
|
r40065 | if len(rel) > 2: | ||
|
r39957 | localmetadata.update(rel[2]) | ||
|
r40078 | if foldid is not None: | ||
|
r43347 | localmetadata[b'fold-id'] = foldid | ||
localmetadata[b'fold-idx'] = b'%d' % foldidx | ||||
localmetadata[b'fold-size'] = b'%d' % foldsize | ||||
|
r20517 | |||
|
r39957 | if not prec.mutable(): | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b"cannot obsolete public changeset: %s") % prec, | ||
hint=b"see 'hg help phases' for details", | ||||
|
r43346 | ) | ||
|
r39957 | nprec = prec.node() | ||
nsucs = tuple(s.node() for s in sucs) | ||||
npare = None | ||||
if not nsucs: | ||||
npare = tuple(p.node() for p in prec.parents()) | ||||
if nprec in nsucs: | ||||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b"changeset %s cannot obsolete itself") % prec | ||
|
r43346 | ) | ||
|
r27984 | |||
|
r39957 | # Effect flag can be different by relation | ||
if saveeffectflag: | ||||
# The effect flag is saved in a versioned field name for | ||||
# future evolution | ||||
effectflag = obsutil.geteffectflag(prec, sucs) | ||||
|
r43347 | localmetadata[obsutil.EFFECTFLAGFIELD] = b"%d" % effectflag | ||
|
r34414 | |||
|
r39957 | # Creating the marker causes the hidden cache to become | ||
# invalid, which causes recomputation when we ask for | ||||
# prec.parents() above. Resulting in n^2 behavior. So let's | ||||
# prepare all of the args first, then create the markers. | ||||
markerargs.append((nprec, nsucs, npare, localmetadata)) | ||||
|
r27984 | |||
for args in markerargs: | ||||
nprec, nsucs, npare, localmetadata = args | ||||
|
r43346 | repo.obsstore.create( | ||
tr, | ||||
nprec, | ||||
nsucs, | ||||
flag, | ||||
parents=npare, | ||||
date=date, | ||||
metadata=localmetadata, | ||||
ui=repo.ui, | ||||
) | ||||
|
r18101 | repo.filteredrevcache.clear() | ||