|
|
# 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.
|
|
|
|
|
|
"""Obsolete markers handling
|
|
|
|
|
|
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
|
|
|
transformations performed by history rewriting operations, and help
|
|
|
building new tools to reconciliate conflicting rewriting actions. To
|
|
|
facilitate conflicts resolution, markers include various annotations
|
|
|
besides old and news changeset identifiers, such as creation date or
|
|
|
author name.
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
The header is followed by the markers. Each marker is made of:
|
|
|
|
|
|
- 1 unsigned byte: number of new changesets "R", could be zero.
|
|
|
|
|
|
- 1 unsigned 32-bits integer: metadata size "M" in bytes.
|
|
|
|
|
|
- 1 byte: a bit field. It is reserved for flags used in obsolete
|
|
|
markers common 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 color ':', without
|
|
|
additional encoding. Keys cannot contain '\0' or ':' and values
|
|
|
cannot contain '\0'.
|
|
|
"""
|
|
|
import struct
|
|
|
import util, base85
|
|
|
from i18n import _
|
|
|
|
|
|
_pack = struct.pack
|
|
|
_unpack = struct.unpack
|
|
|
|
|
|
_SEEK_END = 2 # os.SEEK_END was introduced in Python 2.5
|
|
|
|
|
|
# the obsolete feature is not mature enough to be enabled by default.
|
|
|
# you have to rely on third party extension extension to enable this.
|
|
|
_enabled = False
|
|
|
|
|
|
# data used for parsing and writing
|
|
|
_fmversion = 0
|
|
|
_fmfixed = '>BIB20s'
|
|
|
_fmnode = '20s'
|
|
|
_fmfsize = struct.calcsize(_fmfixed)
|
|
|
_fnodesize = struct.calcsize(_fmnode)
|
|
|
|
|
|
def _readmarkers(data):
|
|
|
"""Read and enumerate markers from raw data"""
|
|
|
off = 0
|
|
|
diskversion = _unpack('>B', data[off:off + 1])[0]
|
|
|
off += 1
|
|
|
if diskversion != _fmversion:
|
|
|
raise util.Abort(_('parsing obsolete marker: unknown version %r')
|
|
|
% diskversion)
|
|
|
|
|
|
# Loop on markers
|
|
|
l = len(data)
|
|
|
while off + _fmfsize <= l:
|
|
|
# read fixed part
|
|
|
cur = data[off:off + _fmfsize]
|
|
|
off += _fmfsize
|
|
|
nbsuc, mdsize, flags, pre = _unpack(_fmfixed, cur)
|
|
|
# read replacement
|
|
|
sucs = ()
|
|
|
if nbsuc:
|
|
|
s = (_fnodesize * nbsuc)
|
|
|
cur = data[off:off + s]
|
|
|
sucs = _unpack(_fmnode * nbsuc, cur)
|
|
|
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')
|
|
|
% (mdsize, len(metadata)))
|
|
|
off += mdsize
|
|
|
yield (pre, sucs, flags, metadata)
|
|
|
|
|
|
def encodemeta(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("':' are forbidden in metadata value'")
|
|
|
return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
|
|
|
|
|
|
def decodemeta(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
|
|
|
|
|
|
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
|
|
|
|
|
|
def precnode(self):
|
|
|
"""Precursor changeset node identifier"""
|
|
|
return self._data[0]
|
|
|
|
|
|
def succnodes(self):
|
|
|
"""List of successor changesets node identifiers"""
|
|
|
return self._data[1]
|
|
|
|
|
|
def metadata(self):
|
|
|
"""Decoded metadata dictionary"""
|
|
|
if self._decodedmeta is None:
|
|
|
self._decodedmeta = decodemeta(self._data[3])
|
|
|
return self._decodedmeta
|
|
|
|
|
|
def date(self):
|
|
|
"""Creation date as (unixtime, offset)"""
|
|
|
parts = self.metadata()['date'].split(' ')
|
|
|
return (float(parts[0]), int(parts[1]))
|
|
|
|
|
|
class obsstore(object):
|
|
|
"""Store obsolete markers
|
|
|
|
|
|
Markers can be accessed with two mappings:
|
|
|
- precursors: old -> set(new)
|
|
|
- successors: new -> set(old)
|
|
|
"""
|
|
|
|
|
|
def __init__(self, sopener):
|
|
|
# caches for various obsolescence related cache
|
|
|
self.caches = {}
|
|
|
self._all = []
|
|
|
# new markers to serialize
|
|
|
self.precursors = {}
|
|
|
self.successors = {}
|
|
|
self.sopener = sopener
|
|
|
data = sopener.tryread('obsstore')
|
|
|
if data:
|
|
|
self._load(_readmarkers(data))
|
|
|
|
|
|
def __iter__(self):
|
|
|
return iter(self._all)
|
|
|
|
|
|
def __nonzero__(self):
|
|
|
return bool(self._all)
|
|
|
|
|
|
def create(self, transaction, prec, succs=(), flag=0, metadata=None):
|
|
|
"""obsolete: add a new obsolete marker
|
|
|
|
|
|
* ensuring it is hashable
|
|
|
* check mandatory metadata
|
|
|
* encode metadata
|
|
|
"""
|
|
|
if metadata is None:
|
|
|
metadata = {}
|
|
|
if len(prec) != 20:
|
|
|
raise ValueError(prec)
|
|
|
for succ in succs:
|
|
|
if len(succ) != 20:
|
|
|
raise ValueError(succ)
|
|
|
marker = (str(prec), tuple(succs), int(flag), encodemeta(metadata))
|
|
|
self.add(transaction, [marker])
|
|
|
|
|
|
def add(self, transaction, markers):
|
|
|
"""Add new markers to the store
|
|
|
|
|
|
Take care of filtering duplicate.
|
|
|
Return the number of new marker."""
|
|
|
if not _enabled:
|
|
|
raise util.Abort('obsolete feature is not enabled on this repo')
|
|
|
new = [m for m in markers if m not in self._all]
|
|
|
if new:
|
|
|
f = self.sopener('obsstore', 'ab')
|
|
|
try:
|
|
|
# Whether the file's current position is at the begin or at
|
|
|
# the end after opening a file for appending is implementation
|
|
|
# defined. So we must seek to the end before calling tell(),
|
|
|
# or we may get a zero offset for non-zero sized files on
|
|
|
# some platforms (issue3543).
|
|
|
f.seek(0, _SEEK_END)
|
|
|
offset = f.tell()
|
|
|
transaction.add('obsstore', offset)
|
|
|
# offset == 0: new file - add the version header
|
|
|
for bytes in _encodemarkers(new, offset == 0):
|
|
|
f.write(bytes)
|
|
|
finally:
|
|
|
# XXX: f.close() == filecache invalidation == obsstore rebuilt.
|
|
|
# call 'filecacheentry.refresh()' here
|
|
|
f.close()
|
|
|
self._load(new)
|
|
|
# new marker *may* have changed several set. invalidate the cache.
|
|
|
self.caches.clear()
|
|
|
return len(new)
|
|
|
|
|
|
def mergemarkers(self, transaction, data):
|
|
|
markers = _readmarkers(data)
|
|
|
self.add(transaction, markers)
|
|
|
|
|
|
def _load(self, markers):
|
|
|
for mark in markers:
|
|
|
self._all.append(mark)
|
|
|
pre, sucs = mark[:2]
|
|
|
self.precursors.setdefault(pre, set()).add(mark)
|
|
|
for suc in sucs:
|
|
|
self.successors.setdefault(suc, set()).add(mark)
|
|
|
|
|
|
def _encodemarkers(markers, addheader=False):
|
|
|
# Kept separate from flushmarkers(), it will be reused for
|
|
|
# markers exchange.
|
|
|
if addheader:
|
|
|
yield _pack('>B', _fmversion)
|
|
|
for marker in markers:
|
|
|
yield _encodeonemarker(marker)
|
|
|
|
|
|
|
|
|
def _encodeonemarker(marker):
|
|
|
pre, sucs, flags, metadata = marker
|
|
|
nbsuc = len(sucs)
|
|
|
format = _fmfixed + (_fmnode * nbsuc)
|
|
|
data = [nbsuc, len(metadata), flags, pre]
|
|
|
data.extend(sucs)
|
|
|
return _pack(format, *data) + metadata
|
|
|
|
|
|
# arbitrary picked to fit into 8K limit from HTTP server
|
|
|
# you have to take in account:
|
|
|
# - the version header
|
|
|
# - the base85 encoding
|
|
|
_maxpayload = 5300
|
|
|
|
|
|
def listmarkers(repo):
|
|
|
"""List markers over pushkey"""
|
|
|
if not repo.obsstore:
|
|
|
return {}
|
|
|
keys = {}
|
|
|
parts = []
|
|
|
currentlen = _maxpayload * 2 # ensure we create a new part
|
|
|
for marker in repo.obsstore:
|
|
|
nextdata = _encodeonemarker(marker)
|
|
|
if (len(nextdata) + currentlen > _maxpayload):
|
|
|
currentpart = []
|
|
|
currentlen = 0
|
|
|
parts.append(currentpart)
|
|
|
currentpart.append(nextdata)
|
|
|
currentlen += len(nextdata)
|
|
|
for idx, part in enumerate(reversed(parts)):
|
|
|
data = ''.join([_pack('>B', _fmversion)] + part)
|
|
|
keys['dump%i' % idx] = base85.b85encode(data)
|
|
|
return keys
|
|
|
|
|
|
def pushmarker(repo, key, old, new):
|
|
|
"""Push markers over pushkey"""
|
|
|
if not key.startswith('dump'):
|
|
|
repo.ui.warn(_('unknown key: %r') % key)
|
|
|
return 0
|
|
|
if old:
|
|
|
repo.ui.warn(_('unexpected old value') % key)
|
|
|
return 0
|
|
|
data = base85.b85decode(new)
|
|
|
lock = repo.lock()
|
|
|
try:
|
|
|
tr = repo.transaction('pushkey: obsolete markers')
|
|
|
try:
|
|
|
repo.obsstore.mergemarkers(tr, data)
|
|
|
tr.close()
|
|
|
return 1
|
|
|
finally:
|
|
|
tr.release()
|
|
|
finally:
|
|
|
lock.release()
|
|
|
|
|
|
def allmarkers(repo):
|
|
|
"""all obsolete markers known in a repository"""
|
|
|
for markerdata in repo.obsstore:
|
|
|
yield marker(repo, markerdata)
|
|
|
|
|
|
def precursormarkers(ctx):
|
|
|
"""obsolete marker making this changeset obsolete"""
|
|
|
for data in ctx._repo.obsstore.precursors.get(ctx.node(), ()):
|
|
|
yield marker(ctx._repo, data)
|
|
|
|
|
|
def successormarkers(ctx):
|
|
|
"""obsolete marker marking this changeset as a successors"""
|
|
|
for data in ctx._repo.obsstore.successors.get(ctx.node(), ()):
|
|
|
yield marker(ctx._repo, data)
|
|
|
|
|
|
def anysuccessors(obsstore, node):
|
|
|
"""Yield every successor of <node>
|
|
|
|
|
|
This is a linear yield unsuited to detecting split changesets."""
|
|
|
remaining = set([node])
|
|
|
seen = set(remaining)
|
|
|
while remaining:
|
|
|
current = remaining.pop()
|
|
|
yield current
|
|
|
for mark in obsstore.precursors.get(current, ()):
|
|
|
for suc in mark[1]:
|
|
|
if suc not in seen:
|
|
|
seen.add(suc)
|
|
|
remaining.add(suc)
|
|
|
|
|
|
# mapping of 'set-name' -> <function to computer this set>
|
|
|
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
|
|
|
|
|
|
def getobscache(repo, name):
|
|
|
"""Return the set of revision that belong to the <name> set
|
|
|
|
|
|
Such access may compute the set and cache it for future use"""
|
|
|
if not repo.obsstore:
|
|
|
return ()
|
|
|
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()
|
|
|
nm = repo.changelog.nodemap
|
|
|
for prec in repo.obsstore.precursors:
|
|
|
rev = nm.get(prec)
|
|
|
if rev is not None:
|
|
|
obs.add(rev)
|
|
|
return set(repo.revs('%ld - public()', obs))
|
|
|
|
|
|
@cachefor('unstable')
|
|
|
def _computeunstableset(repo):
|
|
|
"""the set of non obsolete revisions with obsolete parents"""
|
|
|
return set(repo.revs('(obsolete()::) - obsolete()'))
|
|
|
|
|
|
@cachefor('suspended')
|
|
|
def _computesuspendedset(repo):
|
|
|
"""the set of obsolete parents with non obsolete descendants"""
|
|
|
return set(repo.revs('obsolete() and obsolete()::unstable()'))
|
|
|
|
|
|
@cachefor('extinct')
|
|
|
def _computeextinctset(repo):
|
|
|
"""the set of obsolete parents without non obsolete descendants"""
|
|
|
return set(repo.revs('obsolete() - obsolete()::unstable()'))
|
|
|
|
|
|
def createmarkers(repo, relations, flag=0, metadata=None):
|
|
|
"""Add obsolete markers between changesets in a repo
|
|
|
|
|
|
<relations> must be an iterable of (<old>, (<new>, ...)) tuple.
|
|
|
`old` and `news` are changectx.
|
|
|
|
|
|
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 'date' not in metadata:
|
|
|
metadata['date'] = '%i %i' % util.makedate()
|
|
|
if 'user' not in metadata:
|
|
|
metadata['user'] = repo.ui.username()
|
|
|
tr = repo.transaction('add-obsolescence-marker')
|
|
|
try:
|
|
|
for prec, sucs in relations:
|
|
|
if not prec.mutable():
|
|
|
raise util.Abort("cannot obsolete immutable changeset: %s"
|
|
|
% prec)
|
|
|
nprec = prec.node()
|
|
|
nsucs = tuple(s.node() for s in sucs)
|
|
|
if nprec in nsucs:
|
|
|
raise util.Abort("changeset %s cannot obsolete itself" % prec)
|
|
|
repo.obsstore.create(tr, nprec, nsucs, flag, metadata)
|
|
|
tr.close()
|
|
|
finally:
|
|
|
tr.release()
|
|
|
|