changelog.py
543 lines
| 17.9 KiB
| text/x-python
|
PythonLexer
/ mercurial / changelog.py
|
r1095 | # changelog.py - changelog class for mercurial | ||
|
r1089 | # | ||
|
r4635 | # Copyright 2005-2007 Matt Mackall <mpm@selenic.com> | ||
|
r1089 | # | ||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r1089 | |||
|
r25922 | from __future__ import absolute_import | ||
|
r28487 | import collections | ||
|
r25922 | from .i18n import _ | ||
from .node import ( | ||||
bin, | ||||
hex, | ||||
nullid, | ||||
) | ||||
from . import ( | ||||
encoding, | ||||
error, | ||||
revlog, | ||||
util, | ||||
) | ||||
|
r1089 | |||
|
r16267 | _defaultextra = {'branch': 'default'} | ||
|
r3232 | def _string_escape(text): | ||
""" | ||||
>>> d = {'nl': chr(10), 'bs': chr(92), 'cr': chr(13), 'nul': chr(0)} | ||||
>>> s = "ab%(nl)scd%(bs)s%(bs)sn%(nul)sab%(cr)scd%(bs)s%(nl)s" % d | ||||
>>> s | ||||
'ab\\ncd\\\\\\\\n\\x00ab\\rcd\\\\\\n' | ||||
>>> res = _string_escape(s) | ||||
|
r31484 | >>> s == util.unescapestr(res) | ||
|
r3232 | True | ||
""" | ||||
# subset of the string_escape codec | ||||
text = text.replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r') | ||||
return text.replace('\0', '\\0') | ||||
|
r8443 | def decodeextra(text): | ||
|
r15661 | """ | ||
|
r18379 | >>> sorted(decodeextra(encodeextra({'foo': 'bar', 'baz': chr(0) + '2'}) | ||
... ).iteritems()) | ||||
[('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')] | ||||
>>> sorted(decodeextra(encodeextra({'foo': 'bar', | ||||
... 'baz': chr(92) + chr(0) + '2'}) | ||||
... ).iteritems()) | ||||
[('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')] | ||||
|
r15661 | """ | ||
|
r16267 | extra = _defaultextra.copy() | ||
|
r8443 | for l in text.split('\0'): | ||
if l: | ||||
|
r15661 | if '\\0' in l: | ||
# fix up \0 without getting into trouble with \\0 | ||||
l = l.replace('\\\\', '\\\\\n') | ||||
l = l.replace('\\0', '\0') | ||||
l = l.replace('\n', '') | ||||
|
r31484 | k, v = util.unescapestr(l).split(':', 1) | ||
|
r8443 | extra[k] = v | ||
return extra | ||||
def encodeextra(d): | ||||
# keys must be sorted to produce a deterministic changelog entry | ||||
items = [_string_escape('%s:%s' % (k, d[k])) for k in sorted(d)] | ||||
return "\0".join(items) | ||||
|
r17810 | def stripdesc(desc): | ||
"""strip trailing whitespace and leading and trailing empty lines""" | ||||
return '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n') | ||||
|
r8778 | class appender(object): | ||
|
r7807 | '''the changelog index must be updated last on disk, so we use this class | ||
|
r4261 | to delay writes to it''' | ||
|
r19899 | def __init__(self, vfs, name, mode, buf): | ||
|
r4261 | self.data = buf | ||
|
r19899 | fp = vfs(name, mode) | ||
|
r4261 | self.fp = fp | ||
self.offset = fp.tell() | ||||
|
r19899 | self.size = vfs.fstat(fp).st_size | ||
|
r30596 | self._end = self.size | ||
|
r4261 | |||
def end(self): | ||||
|
r30596 | return self._end | ||
|
r4261 | def tell(self): | ||
return self.offset | ||||
def flush(self): | ||||
pass | ||||
def close(self): | ||||
|
r4961 | self.fp.close() | ||
|
r4261 | |||
def seek(self, offset, whence=0): | ||||
'''virtual file offset spans real file and data''' | ||||
if whence == 0: | ||||
self.offset = offset | ||||
elif whence == 1: | ||||
self.offset += offset | ||||
elif whence == 2: | ||||
self.offset = self.end() + offset | ||||
if self.offset < self.size: | ||||
self.fp.seek(self.offset) | ||||
def read(self, count=-1): | ||||
'''only trick here is reads that span real file and data''' | ||||
ret = "" | ||||
if self.offset < self.size: | ||||
s = self.fp.read(count) | ||||
ret = s | ||||
self.offset += len(s) | ||||
if count > 0: | ||||
count -= len(s) | ||||
if count != 0: | ||||
doff = self.offset - self.size | ||||
self.data.insert(0, "".join(self.data)) | ||||
del self.data[1:] | ||||
|
r10282 | s = self.data[0][doff:doff + count] | ||
|
r4261 | self.offset += len(s) | ||
ret += s | ||||
return ret | ||||
def write(self, s): | ||||
|
r31642 | self.data.append(bytes(s)) | ||
|
r4261 | self.offset += len(s) | ||
|
r30596 | self._end += len(s) | ||
|
r4261 | |||
|
r23201 | def _divertopener(opener, target): | ||
"""build an opener that writes in 'target.a' instead of 'target'""" | ||||
|
r29997 | def _divert(name, mode='r', checkambig=False): | ||
|
r9166 | if name != target: | ||
return opener(name, mode) | ||||
|
r23201 | return opener(name + ".a", mode) | ||
return _divert | ||||
def _delayopener(opener, target, buf): | ||||
"""build an opener that stores chunks in 'buf' instead of 'target'""" | ||||
|
r29997 | def _delay(name, mode='r', checkambig=False): | ||
|
r23201 | if name != target: | ||
return opener(name, mode) | ||||
|
r19899 | return appender(opener, name, mode, buf) | ||
|
r23201 | return _delay | ||
|
r9166 | |||
|
r29696 | _changelogrevision = collections.namedtuple(u'changelogrevision', | ||
(u'manifest', u'user', u'date', | ||||
u'files', u'description', | ||||
u'extra')) | ||||
|
r28487 | |||
class changelogrevision(object): | ||||
"""Holds results of a parsed changelog revision. | ||||
Changelog revisions consist of multiple pieces of data, including | ||||
the manifest node, user, and date. This object exposes a view into | ||||
the parsed object. | ||||
""" | ||||
__slots__ = ( | ||||
|
r29696 | u'_offsets', | ||
u'_text', | ||||
|
r28487 | ) | ||
def __new__(cls, text): | ||||
if not text: | ||||
return _changelogrevision( | ||||
manifest=nullid, | ||||
user='', | ||||
date=(0, 0), | ||||
files=[], | ||||
description='', | ||||
extra=_defaultextra, | ||||
) | ||||
self = super(changelogrevision, cls).__new__(cls) | ||||
# We could return here and implement the following as an __init__. | ||||
# But doing it here is equivalent and saves an extra function call. | ||||
# format used: | ||||
# nodeid\n : manifest node in ascii | ||||
# user\n : user, no \n or \r allowed | ||||
# time tz extra\n : date (time is int or float, timezone is int) | ||||
# : extra is metadata, encoded and separated by '\0' | ||||
# : older versions ignore it | ||||
# files\n\n : files modified by the cset, no \n or \r allowed | ||||
# (.*) : comment (free text, ideally utf-8) | ||||
# | ||||
# changelog v0 doesn't use extra | ||||
|
r28490 | nl1 = text.index('\n') | ||
|
r28491 | nl2 = text.index('\n', nl1 + 1) | ||
|
r28492 | nl3 = text.index('\n', nl2 + 1) | ||
|
r28487 | |||
|
r28493 | # The list of files may be empty. Which means nl3 is the first of the | ||
# double newline that precedes the description. | ||||
|
r32153 | if text[nl3 + 1:nl3 + 2] == '\n': | ||
|
r28495 | doublenl = nl3 | ||
|
r28493 | else: | ||
|
r28494 | doublenl = text.index('\n\n', nl3 + 1) | ||
|
r28495 | |||
self._offsets = (nl1, nl2, nl3, doublenl) | ||||
self._text = text | ||||
|
r28487 | |||
return self | ||||
|
r28489 | @property | ||
|
r28490 | def manifest(self): | ||
|
r28495 | return bin(self._text[0:self._offsets[0]]) | ||
|
r28490 | |||
@property | ||||
|
r28491 | def user(self): | ||
|
r28495 | off = self._offsets | ||
return encoding.tolocal(self._text[off[0] + 1:off[1]]) | ||||
|
r28491 | |||
@property | ||||
|
r28492 | def _rawdate(self): | ||
|
r28495 | off = self._offsets | ||
dateextra = self._text[off[1] + 1:off[2]] | ||||
return dateextra.split(' ', 2)[0:2] | ||||
|
r28492 | |||
@property | ||||
def _rawextra(self): | ||||
|
r28495 | off = self._offsets | ||
dateextra = self._text[off[1] + 1:off[2]] | ||||
fields = dateextra.split(' ', 2) | ||||
|
r28492 | if len(fields) != 3: | ||
return None | ||||
return fields[2] | ||||
@property | ||||
def date(self): | ||||
raw = self._rawdate | ||||
time = float(raw[0]) | ||||
# Various tools did silly things with the timezone. | ||||
try: | ||||
timezone = int(raw[1]) | ||||
except ValueError: | ||||
timezone = 0 | ||||
return time, timezone | ||||
@property | ||||
def extra(self): | ||||
raw = self._rawextra | ||||
if raw is None: | ||||
return _defaultextra | ||||
return decodeextra(raw) | ||||
@property | ||||
|
r28493 | def files(self): | ||
|
r28495 | off = self._offsets | ||
if off[2] == off[3]: | ||||
|
r28493 | return [] | ||
|
r28495 | return self._text[off[2] + 1:off[3]].split('\n') | ||
|
r28493 | |||
@property | ||||
|
r28489 | def description(self): | ||
|
r28495 | return encoding.tolocal(self._text[self._offsets[3] + 2:]) | ||
|
r28489 | |||
|
r7634 | class changelog(revlog.revlog): | ||
|
r32292 | def __init__(self, opener, trypending=False): | ||
"""Load a changelog revlog using an opener. | ||||
If ``trypending`` is true, we attempt to load the index from a | ||||
``00changelog.i.a`` file instead of the default ``00changelog.i``. | ||||
The ``00changelog.i.a`` file contains index (and possibly inline | ||||
revision) data for a transaction that hasn't been finalized yet. | ||||
It exists in a separate file to facilitate readers (such as | ||||
hooks processes) accessing data before a transaction is finalized. | ||||
""" | ||||
if trypending and opener.exists('00changelog.i.a'): | ||||
indexfile = '00changelog.i.a' | ||||
else: | ||||
indexfile = '00changelog.i' | ||||
|
r32307 | datafile = '00changelog.d' | ||
revlog.revlog.__init__(self, opener, indexfile, datafile=datafile, | ||||
checkambig=True) | ||||
|
r32292 | |||
|
r14334 | if self._initempty: | ||
# changelogs don't benefit from generaldelta | ||||
self.version &= ~revlog.REVLOGGENERALDELTA | ||||
self._generaldelta = False | ||||
|
r30155 | |||
# Delta chains for changelogs tend to be very small because entries | ||||
# tend to be small and don't delta well with each. So disable delta | ||||
# chains. | ||||
|
r30210 | self.storedeltachains = False | ||
|
r30155 | |||
|
r8644 | self._realopener = opener | ||
self._delayed = False | ||||
|
r23201 | self._delaybuf = None | ||
|
r9163 | self._divert = False | ||
|
r18231 | self.filteredrevs = frozenset() | ||
|
r17677 | |||
def tip(self): | ||||
"""filtered version of revlog.tip""" | ||||
for i in xrange(len(self) -1, -2, -1): | ||||
if i not in self.filteredrevs: | ||||
return self.node(i) | ||||
|
r24030 | def __contains__(self, rev): | ||
"""filtered version of revlog.__contains__""" | ||||
|
r24662 | return (0 <= rev < len(self) | ||
|
r24030 | and rev not in self.filteredrevs) | ||
|
r17677 | def __iter__(self): | ||
"""filtered version of revlog.__iter__""" | ||||
|
r17951 | if len(self.filteredrevs) == 0: | ||
return revlog.revlog.__iter__(self) | ||||
def filterediter(): | ||||
for i in xrange(len(self)): | ||||
if i not in self.filteredrevs: | ||||
yield i | ||||
return filterediter() | ||||
|
r17677 | |||
def revs(self, start=0, stop=None): | ||||
"""filtered version of revlog.revs""" | ||||
for i in super(changelog, self).revs(start, stop): | ||||
if i not in self.filteredrevs: | ||||
yield i | ||||
@util.propertycache | ||||
def nodemap(self): | ||||
# XXX need filtering too | ||||
self.rev(self.node(0)) | ||||
return self._nodecache | ||||
|
r26005 | def reachableroots(self, minroot, heads, roots, includepath=False): | ||
|
r26094 | return self.index.reachableroots2(minroot, heads, roots, includepath) | ||
|
r26005 | |||
|
r17677 | def headrevs(self): | ||
if self.filteredrevs: | ||||
|
r22484 | try: | ||
|
r23088 | return self.index.headrevsfiltered(self.filteredrevs) | ||
# AttributeError covers non-c-extension environments and | ||||
# old c extensions without filter handling. | ||||
except AttributeError: | ||||
|
r22484 | return self._headrevs() | ||
|
r17677 | return super(changelog, self).headrevs() | ||
def strip(self, *args, **kwargs): | ||||
# XXX make something better than assert | ||||
# We can't expect proper strip behavior if we are filtered. | ||||
assert not self.filteredrevs | ||||
super(changelog, self).strip(*args, **kwargs) | ||||
def rev(self, node): | ||||
"""filtered version of revlog.rev""" | ||||
r = super(changelog, self).rev(node) | ||||
if r in self.filteredrevs: | ||||
|
r23015 | raise error.FilteredLookupError(hex(node), self.indexfile, | ||
_('filtered node')) | ||||
|
r17677 | return r | ||
def node(self, rev): | ||||
"""filtered version of revlog.node""" | ||||
if rev in self.filteredrevs: | ||||
|
r23014 | raise error.FilteredIndexError(rev) | ||
|
r17677 | return super(changelog, self).node(rev) | ||
def linkrev(self, rev): | ||||
"""filtered version of revlog.linkrev""" | ||||
if rev in self.filteredrevs: | ||||
|
r23014 | raise error.FilteredIndexError(rev) | ||
|
r17677 | return super(changelog, self).linkrev(rev) | ||
def parentrevs(self, rev): | ||||
"""filtered version of revlog.parentrevs""" | ||||
if rev in self.filteredrevs: | ||||
|
r23014 | raise error.FilteredIndexError(rev) | ||
|
r17677 | return super(changelog, self).parentrevs(rev) | ||
def flags(self, rev): | ||||
"""filtered version of revlog.flags""" | ||||
if rev in self.filteredrevs: | ||||
|
r23014 | raise error.FilteredIndexError(rev) | ||
|
r17677 | return super(changelog, self).flags(rev) | ||
|
r1089 | |||
|
r23203 | def delayupdate(self, tr): | ||
|
r4261 | "delay visibility of index updates to other readers" | ||
|
r23201 | |||
if not self._delayed: | ||||
if len(self) == 0: | ||||
self._divert = True | ||||
if self._realopener.exists(self.indexfile + '.a'): | ||||
self._realopener.unlink(self.indexfile + '.a') | ||||
self.opener = _divertopener(self._realopener, self.indexfile) | ||||
else: | ||||
self._delaybuf = [] | ||||
self.opener = _delayopener(self._realopener, self.indexfile, | ||||
self._delaybuf) | ||||
|
r8644 | self._delayed = True | ||
|
r23203 | tr.addpending('cl-%i' % id(self), self._writepending) | ||
|
r23281 | tr.addfinalize('cl-%i' % id(self), self._finalize) | ||
|
r4261 | |||
|
r23205 | def _finalize(self, tr): | ||
|
r4261 | "finalize index updates" | ||
|
r8644 | self._delayed = False | ||
|
r9165 | self.opener = self._realopener | ||
|
r4269 | # move redirected index data back into place | ||
|
r9164 | if self._divert: | ||
|
r23201 | assert not self._delaybuf | ||
|
r19898 | tmpname = self.indexfile + ".a" | ||
nfile = self.opener.open(tmpname) | ||||
|
r14207 | nfile.close() | ||
|
r29999 | self.opener.rename(tmpname, self.indexfile, checkambig=True) | ||
|
r4269 | elif self._delaybuf: | ||
|
r29999 | fp = self.opener(self.indexfile, 'a', checkambig=True) | ||
|
r4261 | fp.write("".join(self._delaybuf)) | ||
fp.close() | ||||
|
r23201 | self._delaybuf = None | ||
self._divert = False | ||||
|
r4269 | # split when we're done | ||
|
r4261 | self.checkinlinesize(tr) | ||
|
r23280 | def _writepending(self, tr): | ||
|
r7787 | "create a file containing the unfinalized state for pretxnchangegroup" | ||
if self._delaybuf: | ||||
# make a temporary copy of the index | ||||
fp1 = self._realopener(self.indexfile) | ||||
|
r23292 | pendingfilename = self.indexfile + ".a" | ||
# register as a temp file to ensure cleanup on failure | ||||
tr.registertmp(pendingfilename) | ||||
# write existing data | ||||
fp2 = self._realopener(pendingfilename, "w") | ||||
|
r7787 | fp2.write(fp1.read()) | ||
# add pending data | ||||
fp2.write("".join(self._delaybuf)) | ||||
fp2.close() | ||||
# switch modes so finalize can simply rename | ||||
|
r23201 | self._delaybuf = None | ||
|
r9164 | self._divert = True | ||
|
r23201 | self.opener = _divertopener(self._realopener, self.indexfile) | ||
|
r7787 | |||
|
r9164 | if self._divert: | ||
|
r7787 | return True | ||
return False | ||||
|
r4261 | def checkinlinesize(self, tr, fp=None): | ||
|
r9165 | if not self._delayed: | ||
revlog.revlog.checkinlinesize(self, tr, fp) | ||||
|
r4261 | |||
|
r5744 | def read(self, node): | ||
|
r28487 | """Obtain data from a parsed changelog revision. | ||
Returns a 6-tuple of: | ||||
|
r3233 | |||
|
r28487 | - manifest node in binary | ||
- author/user as a localstr | ||||
- date as a 2-tuple of (time, timezone) | ||||
- list of files | ||||
- commit message as a localstr | ||||
- dict of extra metadata | ||||
Unless you need to access all fields, consider calling | ||||
``changelogrevision`` instead, as it is faster for partial object | ||||
access. | ||||
|
r3077 | """ | ||
|
r28487 | c = changelogrevision(self.revision(node)) | ||
return ( | ||||
c.manifest, | ||||
c.user, | ||||
c.date, | ||||
c.files, | ||||
c.description, | ||||
c.extra | ||||
) | ||||
|
r3233 | |||
|
r28487 | def changelogrevision(self, nodeorrev): | ||
"""Obtain a ``changelogrevision`` for a node or revision.""" | ||||
return changelogrevision(self.revision(nodeorrev)) | ||||
|
r1089 | |||
|
r27439 | def readfiles(self, node): | ||
""" | ||||
short version of read that only returns the files modified by the cset | ||||
""" | ||||
text = self.revision(node) | ||||
if not text: | ||||
return [] | ||||
last = text.index("\n\n") | ||||
l = text[:last].split('\n') | ||||
return l[3:] | ||||
|
r8422 | def add(self, manifest, files, desc, transaction, p1, p2, | ||
|
r9677 | user, date=None, extra=None): | ||
|
r14379 | # Convert to UTF-8 encoded bytestrings as the very first | ||
# thing: calling any method on a localstr object will turn it | ||||
# into a str object and the cached UTF-8 string is thus lost. | ||||
user, desc = encoding.fromlocal(user), encoding.fromlocal(desc) | ||||
|
r7035 | user = user.strip() | ||
|
r8424 | # An empty username or a username with a "\n" will make the | ||
# revision text contain two "\n\n" sequences -> corrupt | ||||
# repository since read cannot unpack the revision. | ||||
if not user: | ||||
raise error.RevlogError(_("empty username")) | ||||
|
r7035 | if "\n" in user: | ||
|
r7633 | raise error.RevlogError(_("username %s contains a newline") | ||
% repr(user)) | ||||
|
r8499 | |||
|
r17810 | desc = stripdesc(desc) | ||
|
r8499 | |||
|
r1195 | if date: | ||
|
r2523 | parseddate = "%d %d" % util.parsedate(date) | ||
|
r1195 | else: | ||
|
r2522 | parseddate = "%d %d" % util.makedate() | ||
|
r10417 | if extra: | ||
branch = extra.get("branch") | ||||
if branch in ("default", ""): | ||||
del extra["branch"] | ||||
elif branch in (".", "null", "tip"): | ||||
raise error.RevlogError(_('the name \'%s\' is reserved') | ||||
% branch) | ||||
|
r3233 | if extra: | ||
|
r8443 | extra = encodeextra(extra) | ||
|
r3233 | parseddate = "%s %s" % (parseddate, extra) | ||
|
r8209 | l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc] | ||
|
r1089 | text = "\n".join(l) | ||
|
r6750 | return self.addrevision(text, transaction, len(self), p1, p2) | ||
|
r18306 | |||
|
r20185 | def branchinfo(self, rev): | ||
"""return the branch name and open/close state of a revision | ||||
|
r18306 | |||
|
r18308 | This function exists because creating a changectx object | ||
just to access this is costly.""" | ||||
|
r20185 | extra = self.read(rev)[5] | ||
return encoding.tolocal(extra.get("branch")), 'close' in extra | ||||
|
r32262 | |||
def _addrevision(self, node, rawtext, transaction, *args, **kwargs): | ||||
# overlay over the standard revlog._addrevision to track the new | ||||
# revision on the transaction. | ||||
rev = len(self) | ||||
node = super(changelog, self)._addrevision(node, rawtext, transaction, | ||||
*args, **kwargs) | ||||
revs = transaction.changes.get('revs') | ||||
if revs is not None: | ||||
revs.add(rev) | ||||
return node | ||||