changelog.py
375 lines
| 12.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 | |||
|
r6211 | from node import bin, hex, nullid | ||
|
r7035 | from i18n import _ | ||
|
r7948 | import util, error, revlog, encoding | ||
|
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) | ||||
|
r5745 | >>> s == res.decode('string_escape') | ||
|
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', '') | ||||
|
r8443 | k, v = l.decode('string_escape').split(':', 1) | ||
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 | ||
|
r4261 | |||
def end(self): | ||||
return self.size + len("".join(self.data)) | ||||
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): | ||||
|
r5450 | self.data.append(str(s)) | ||
|
r4261 | self.offset += len(s) | ||
|
r23201 | def _divertopener(opener, target): | ||
"""build an opener that writes in 'target.a' instead of 'target'""" | ||||
def _divert(name, mode='r'): | ||||
|
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'""" | ||||
def _delay(name, mode='r'): | ||||
if name != target: | ||||
return opener(name, mode) | ||||
|
r19899 | return appender(opener, name, mode, buf) | ||
|
r23201 | return _delay | ||
|
r9166 | |||
|
r7634 | class changelog(revlog.revlog): | ||
|
r4258 | def __init__(self, opener): | ||
|
r9165 | revlog.revlog.__init__(self, opener, "00changelog.i") | ||
|
r14334 | if self._initempty: | ||
# changelogs don't benefit from generaldelta | ||||
self.version &= ~revlog.REVLOGGENERALDELTA | ||||
self._generaldelta = False | ||||
|
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) | ||||
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 | ||||
def hasnode(self, node): | ||||
"""filtered version of revlog.hasnode""" | ||||
try: | ||||
i = self.rev(node) | ||||
return i not in self.filteredrevs | ||||
except KeyError: | ||||
return False | ||||
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) | ||
|
r4261 | |||
def finalize(self, tr): | ||||
"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() | ||
|
r19898 | self.opener.rename(tmpname, self.indexfile) | ||
|
r4269 | elif self._delaybuf: | ||
|
r4261 | fp = self.opener(self.indexfile, 'a') | ||
fp.write("".join(self._delaybuf)) | ||||
fp.close() | ||||
|
r23201 | self._delaybuf = None | ||
self._divert = False | ||||
|
r4269 | # split when we're done | ||
|
r4261 | self.checkinlinesize(tr) | ||
|
r7787 | def readpending(self, file): | ||
r = revlog.revlog(self.opener, file) | ||||
self.index = r.index | ||||
self.nodemap = r.nodemap | ||||
|
r16619 | self._nodecache = r._nodecache | ||
|
r7787 | self._chunkcache = r._chunkcache | ||
|
r23203 | def _writepending(self): | ||
|
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) | ||||
fp2 = self._realopener(self.indexfile + ".a", "w") | ||||
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): | ||
|
r3077 | """ | ||
format used: | ||||
|
r3233 | 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) | ||||
|
r17424 | : extra is metadata, encoded and separated by '\0' | ||
|
r3233 | : 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 | ||||
|
r3077 | """ | ||
|
r5744 | text = self.revision(node) | ||
|
r1089 | if not text: | ||
|
r16267 | return (nullid, "", (0, 0), [], "", _defaultextra) | ||
|
r1089 | last = text.index("\n\n") | ||
|
r7948 | desc = encoding.tolocal(text[last + 2:]) | ||
|
r3233 | l = text[:last].split('\n') | ||
|
r1089 | manifest = bin(l[0]) | ||
|
r7948 | user = encoding.tolocal(l[1]) | ||
|
r3233 | |||
|
r16267 | tdata = l[2].split(' ', 2) | ||
if len(tdata) != 3: | ||||
time = float(tdata[0]) | ||||
|
r3233 | try: | ||
# various tools did silly things with the time zone field. | ||||
|
r16267 | timezone = int(tdata[1]) | ||
|
r14004 | except ValueError: | ||
|
r3233 | timezone = 0 | ||
|
r16267 | extra = _defaultextra | ||
|
r3233 | else: | ||
|
r16267 | time, timezone = float(tdata[0]), int(tdata[1]) | ||
extra = decodeextra(tdata[2]) | ||||
|
r1089 | files = l[3:] | ||
|
r3233 | return (manifest, user, (time, timezone), files, desc, extra) | ||
|
r1089 | |||
|
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 | ||||