obsolete.py
527 lines
| 17.0 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. | ||||
"""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. | ||||
Pierre-Yves David
|
r17776 | The old obsoleted changeset is called "precursor" and possible replacements are | ||
called "successors". Markers that used changeset X as a precursors 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
|
r17775 | Examples: | ||
- When changeset A is replacement by a changeset A', one marker is stored: | ||||
(A, (A')) | ||||
- When changesets A and B are folded into a new changeset C two markers are | ||||
stored: | ||||
(A, (C,)) and (B, (C,)) | ||||
- When changeset A is simply "pruned" from the graph, a marker in create: | ||||
(A, ()) | ||||
- When changeset A is split into B and C, a single marker are used: | ||||
(A, (C, C)) | ||||
We use a single marker to distinct the "split" case from the "divergence" | ||||
case. If two independants operation rewrite the same changeset A in to A' and | ||||
A'' when have an error case: divergent rewriting. We can detect it because | ||||
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. | ||||
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'. | ||||
""" | ||||
Adrian Buehlmann
|
r17200 | import struct | ||
Pierre-Yves David
|
r17774 | import util, base85, node | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | from i18n import _ | ||
_pack = struct.pack | ||||
_unpack = struct.unpack | ||||
Mads Kiilerich
|
r17429 | _SEEK_END = 2 # os.SEEK_END was introduced in Python 2.5 | ||
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 | |||
# data used for parsing and writing | ||||
_fmversion = 0 | ||||
_fmfixed = '>BIB20s' | ||||
_fmnode = '20s' | ||||
_fmfsize = struct.calcsize(_fmfixed) | ||||
_fnodesize = struct.calcsize(_fmnode) | ||||
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,)>. | ||||
# This flag mean that the successors are an interdiff that fix the bumped | ||||
# situation, breaking the transitivity of "bumped" here. | ||||
bumpedfix = 1 | ||||
Pierre-Yves.David@ens-lyon.org
|
r17070 | 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') | ||||
Pierre-Yves David
|
r17253 | % (mdsize, len(metadata))) | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | 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 | ||||
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 | ||||
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])) | ||||
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@ens-lyon.org
|
r17070 | """ | ||
Pierre-Yves David
|
r17124 | def __init__(self, sopener): | ||
Pierre-Yves David
|
r17469 | # caches for various obsolescence related cache | ||
self.caches = {} | ||||
Pierre-Yves.David@ens-lyon.org
|
r17070 | self._all = [] | ||
# new markers to serialize | ||||
self.precursors = {} | ||||
self.successors = {} | ||||
Pierre-Yves David
|
r17124 | self.sopener = sopener | ||
data = sopener.tryread('obsstore') | ||||
if data: | ||||
Pierre-Yves.David@ens-lyon.org
|
r17220 | self._load(_readmarkers(data)) | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | |||
Pierre-Yves.David@ens-lyon.org
|
r17073 | def __iter__(self): | ||
return iter(self._all) | ||||
Pierre-Yves.David@ens-lyon.org
|
r17075 | def __nonzero__(self): | ||
return bool(self._all) | ||||
Pierre-Yves David
|
r17126 | def create(self, transaction, prec, succs=(), flag=0, metadata=None): | ||
Pierre-Yves.David@ens-lyon.org
|
r17071 | """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: | ||||
Pierre-Yves David
|
r17117 | raise ValueError(succ) | ||
Pierre-Yves.David@ens-lyon.org
|
r17071 | marker = (str(prec), tuple(succs), int(flag), encodemeta(metadata)) | ||
Pierre-Yves.David@ens-lyon.org
|
r17220 | self.add(transaction, [marker]) | ||
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.""" | ||||
Pierre-Yves David
|
r17296 | if not _enabled: | ||
raise util.Abort('obsolete feature is not enabled on this repo') | ||||
Pierre-Yves.David@ens-lyon.org
|
r17220 | new = [m for m in markers if m not in self._all] | ||
if new: | ||||
Pierre-Yves David
|
r17125 | f = self.sopener('obsstore', 'ab') | ||
Pierre-Yves David
|
r17124 | try: | ||
Adrian Buehlmann
|
r17195 | # 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). | ||||
Mads Kiilerich
|
r17429 | f.seek(0, _SEEK_END) | ||
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@ens-lyon.org
|
r17220 | for bytes in _encodemarkers(new, offset == 0): | ||
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() | ||
Pierre-Yves.David@ens-lyon.org
|
r17220 | self._load(new) | ||
Pierre-Yves David
|
r17469 | # new marker *may* have changed several set. invalidate the cache. | ||
self.caches.clear() | ||||
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@ens-lyon.org
|
r17220 | markers = _readmarkers(data) | ||
timeless@mozdev.org
|
r17524 | self.add(transaction, markers) | ||
Pierre-Yves.David@ens-lyon.org
|
r17070 | |||
Pierre-Yves.David@ens-lyon.org
|
r17220 | def _load(self, markers): | ||
for mark in markers: | ||||
self._all.append(mark) | ||||
pre, sucs = mark[:2] | ||||
Pierre-Yves David
|
r17776 | self.successors.setdefault(pre, set()).add(mark) | ||
Pierre-Yves.David@ens-lyon.org
|
r17220 | for suc in sucs: | ||
Pierre-Yves David
|
r17776 | self.precursors.setdefault(suc, set()).add(mark) | ||
if node.nullid in self.precursors: | ||||
Pierre-Yves David
|
r17774 | raise util.Abort(_('bad obsolescence marker detected: ' | ||
'invalid successors nullid')) | ||||
Pierre-Yves.David@ens-lyon.org
|
r17070 | |||
Pierre-Yves.David@ens-lyon.org
|
r17219 | def _encodemarkers(markers, addheader=False): | ||
Pierre-Yves David
|
r17125 | # Kept separate from flushmarkers(), it will be reused for | ||
# markers exchange. | ||||
Pierre-Yves.David@ens-lyon.org
|
r17219 | if addheader: | ||
yield _pack('>B', _fmversion) | ||||
Pierre-Yves David
|
r17125 | for marker in markers: | ||
Pierre-Yves David
|
r17295 | 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 | ||||
Pierre-Yves.David@ens-lyon.org
|
r17075 | |||
def listmarkers(repo): | ||||
"""List markers over pushkey""" | ||||
if not repo.obsstore: | ||||
return {} | ||||
Pierre-Yves David
|
r17295 | 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) | ||||
Pierre-Yves David
|
r17304 | currentlen += len(nextdata) | ||
Pierre-Yves David
|
r17295 | for idx, part in enumerate(reversed(parts)): | ||
data = ''.join([_pack('>B', _fmversion)] + part) | ||||
keys['dump%i' % idx] = base85.b85encode(data) | ||||
return keys | ||||
Pierre-Yves.David@ens-lyon.org
|
r17073 | |||
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: | ||||
repo.ui.warn(_('unexpected old value') % key) | ||||
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 | |||
def allmarkers(repo): | ||||
"""all obsolete markers known in a repository""" | ||||
for markerdata in repo.obsstore: | ||||
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""" | ||
Pierre-Yves.David@ens-lyon.org
|
r17076 | for data in ctx._repo.obsstore.precursors.get(ctx.node(), ()): | ||
yield marker(ctx._repo, data) | ||||
def successormarkers(ctx): | ||||
Pierre-Yves David
|
r17776 | """obsolete marker making this changeset obsolete""" | ||
Pierre-Yves.David@ens-lyon.org
|
r17076 | for data in ctx._repo.obsstore.successors.get(ctx.node(), ()): | ||
yield marker(ctx._repo, data) | ||||
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 | |||
Bryan O'Sullivan
|
r17537 | This is a linear yield unsuited to detecting split changesets.""" | ||
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
|
r17831 | # ignore marker flagged with with specified flag | ||
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
|
r17828 | def _knownrevs(repo, nodes): | ||
"""yield revision numbers of known nodes passed in parameters | ||||
Unknown revisions are silently ignored.""" | ||||
torev = repo.changelog.nodemap.get | ||||
for n in nodes: | ||||
rev = torev(n) | ||||
if rev is not None: | ||||
yield rev | ||||
# 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: | ||
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 | ||||
Pierre-Yves David
|
r17776 | for node in repo.obsstore.successors: | ||
rev = nm.get(node) | ||||
Pierre-Yves David
|
r17469 | 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()')) | ||||
Pierre-Yves David
|
r17474 | |||
Pierre-Yves David
|
r17828 | |||
@cachefor('bumped') | ||||
def _computebumpedset(repo): | ||||
"""the set of revs trying to obsolete public revisions""" | ||||
# get all possible bumped changesets | ||||
tonode = repo.changelog.node | ||||
publicnodes = (tonode(r) for r in repo.revs('public()')) | ||||
Pierre-Yves David
|
r17831 | successors = allsuccessors(repo.obsstore, publicnodes, | ||
ignoreflags=bumpedfix) | ||||
Pierre-Yves David
|
r17828 | # revision public or already obsolete don't count as bumped | ||
query = '%ld - obsolete() - public()' | ||||
return set(repo.revs(query, _knownrevs(repo, successors))) | ||||
Pierre-Yves David
|
r17474 | 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() | ||||