##// END OF EJS Templates
tests: fix doctest stability over Python versions...
Mads Kiilerich -
r18379:e0c4f4ba default
parent child Browse files
Show More
@@ -1,348 +1,349
1 # changelog.py - changelog class for mercurial
1 # changelog.py - changelog class for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from node import bin, hex, nullid
8 from node import bin, hex, nullid
9 from i18n import _
9 from i18n import _
10 import util, error, revlog, encoding
10 import util, error, revlog, encoding
11
11
12 _defaultextra = {'branch': 'default'}
12 _defaultextra = {'branch': 'default'}
13
13
14 def _string_escape(text):
14 def _string_escape(text):
15 """
15 """
16 >>> d = {'nl': chr(10), 'bs': chr(92), 'cr': chr(13), 'nul': chr(0)}
16 >>> d = {'nl': chr(10), 'bs': chr(92), 'cr': chr(13), 'nul': chr(0)}
17 >>> s = "ab%(nl)scd%(bs)s%(bs)sn%(nul)sab%(cr)scd%(bs)s%(nl)s" % d
17 >>> s = "ab%(nl)scd%(bs)s%(bs)sn%(nul)sab%(cr)scd%(bs)s%(nl)s" % d
18 >>> s
18 >>> s
19 'ab\\ncd\\\\\\\\n\\x00ab\\rcd\\\\\\n'
19 'ab\\ncd\\\\\\\\n\\x00ab\\rcd\\\\\\n'
20 >>> res = _string_escape(s)
20 >>> res = _string_escape(s)
21 >>> s == res.decode('string_escape')
21 >>> s == res.decode('string_escape')
22 True
22 True
23 """
23 """
24 # subset of the string_escape codec
24 # subset of the string_escape codec
25 text = text.replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r')
25 text = text.replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r')
26 return text.replace('\0', '\\0')
26 return text.replace('\0', '\\0')
27
27
28 def decodeextra(text):
28 def decodeextra(text):
29 """
29 """
30 >>> from pprint import pprint as pp
30 >>> sorted(decodeextra(encodeextra({'foo': 'bar', 'baz': chr(0) + '2'})
31 >>> pp(decodeextra(encodeextra({'foo': 'bar', 'baz': chr(0) + '2'})))
31 ... ).iteritems())
32 {'baz': '\\x002', 'branch': 'default', 'foo': 'bar'}
32 [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
33 >>> pp(decodeextra(encodeextra({'foo': 'bar',
33 >>> sorted(decodeextra(encodeextra({'foo': 'bar',
34 ... 'baz': chr(92) + chr(0) + '2'})))
34 ... 'baz': chr(92) + chr(0) + '2'})
35 {'baz': '\\\\\\x002', 'branch': 'default', 'foo': 'bar'}
35 ... ).iteritems())
36 [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
36 """
37 """
37 extra = _defaultextra.copy()
38 extra = _defaultextra.copy()
38 for l in text.split('\0'):
39 for l in text.split('\0'):
39 if l:
40 if l:
40 if '\\0' in l:
41 if '\\0' in l:
41 # fix up \0 without getting into trouble with \\0
42 # fix up \0 without getting into trouble with \\0
42 l = l.replace('\\\\', '\\\\\n')
43 l = l.replace('\\\\', '\\\\\n')
43 l = l.replace('\\0', '\0')
44 l = l.replace('\\0', '\0')
44 l = l.replace('\n', '')
45 l = l.replace('\n', '')
45 k, v = l.decode('string_escape').split(':', 1)
46 k, v = l.decode('string_escape').split(':', 1)
46 extra[k] = v
47 extra[k] = v
47 return extra
48 return extra
48
49
49 def encodeextra(d):
50 def encodeextra(d):
50 # keys must be sorted to produce a deterministic changelog entry
51 # keys must be sorted to produce a deterministic changelog entry
51 items = [_string_escape('%s:%s' % (k, d[k])) for k in sorted(d)]
52 items = [_string_escape('%s:%s' % (k, d[k])) for k in sorted(d)]
52 return "\0".join(items)
53 return "\0".join(items)
53
54
54 def stripdesc(desc):
55 def stripdesc(desc):
55 """strip trailing whitespace and leading and trailing empty lines"""
56 """strip trailing whitespace and leading and trailing empty lines"""
56 return '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n')
57 return '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n')
57
58
58 class appender(object):
59 class appender(object):
59 '''the changelog index must be updated last on disk, so we use this class
60 '''the changelog index must be updated last on disk, so we use this class
60 to delay writes to it'''
61 to delay writes to it'''
61 def __init__(self, fp, buf):
62 def __init__(self, fp, buf):
62 self.data = buf
63 self.data = buf
63 self.fp = fp
64 self.fp = fp
64 self.offset = fp.tell()
65 self.offset = fp.tell()
65 self.size = util.fstat(fp).st_size
66 self.size = util.fstat(fp).st_size
66
67
67 def end(self):
68 def end(self):
68 return self.size + len("".join(self.data))
69 return self.size + len("".join(self.data))
69 def tell(self):
70 def tell(self):
70 return self.offset
71 return self.offset
71 def flush(self):
72 def flush(self):
72 pass
73 pass
73 def close(self):
74 def close(self):
74 self.fp.close()
75 self.fp.close()
75
76
76 def seek(self, offset, whence=0):
77 def seek(self, offset, whence=0):
77 '''virtual file offset spans real file and data'''
78 '''virtual file offset spans real file and data'''
78 if whence == 0:
79 if whence == 0:
79 self.offset = offset
80 self.offset = offset
80 elif whence == 1:
81 elif whence == 1:
81 self.offset += offset
82 self.offset += offset
82 elif whence == 2:
83 elif whence == 2:
83 self.offset = self.end() + offset
84 self.offset = self.end() + offset
84 if self.offset < self.size:
85 if self.offset < self.size:
85 self.fp.seek(self.offset)
86 self.fp.seek(self.offset)
86
87
87 def read(self, count=-1):
88 def read(self, count=-1):
88 '''only trick here is reads that span real file and data'''
89 '''only trick here is reads that span real file and data'''
89 ret = ""
90 ret = ""
90 if self.offset < self.size:
91 if self.offset < self.size:
91 s = self.fp.read(count)
92 s = self.fp.read(count)
92 ret = s
93 ret = s
93 self.offset += len(s)
94 self.offset += len(s)
94 if count > 0:
95 if count > 0:
95 count -= len(s)
96 count -= len(s)
96 if count != 0:
97 if count != 0:
97 doff = self.offset - self.size
98 doff = self.offset - self.size
98 self.data.insert(0, "".join(self.data))
99 self.data.insert(0, "".join(self.data))
99 del self.data[1:]
100 del self.data[1:]
100 s = self.data[0][doff:doff + count]
101 s = self.data[0][doff:doff + count]
101 self.offset += len(s)
102 self.offset += len(s)
102 ret += s
103 ret += s
103 return ret
104 return ret
104
105
105 def write(self, s):
106 def write(self, s):
106 self.data.append(str(s))
107 self.data.append(str(s))
107 self.offset += len(s)
108 self.offset += len(s)
108
109
109 def delayopener(opener, target, divert, buf):
110 def delayopener(opener, target, divert, buf):
110 def o(name, mode='r'):
111 def o(name, mode='r'):
111 if name != target:
112 if name != target:
112 return opener(name, mode)
113 return opener(name, mode)
113 if divert:
114 if divert:
114 return opener(name + ".a", mode.replace('a', 'w'))
115 return opener(name + ".a", mode.replace('a', 'w'))
115 # otherwise, divert to memory
116 # otherwise, divert to memory
116 return appender(opener(name, mode), buf)
117 return appender(opener(name, mode), buf)
117 return o
118 return o
118
119
119 class changelog(revlog.revlog):
120 class changelog(revlog.revlog):
120 def __init__(self, opener):
121 def __init__(self, opener):
121 revlog.revlog.__init__(self, opener, "00changelog.i")
122 revlog.revlog.__init__(self, opener, "00changelog.i")
122 if self._initempty:
123 if self._initempty:
123 # changelogs don't benefit from generaldelta
124 # changelogs don't benefit from generaldelta
124 self.version &= ~revlog.REVLOGGENERALDELTA
125 self.version &= ~revlog.REVLOGGENERALDELTA
125 self._generaldelta = False
126 self._generaldelta = False
126 self._realopener = opener
127 self._realopener = opener
127 self._delayed = False
128 self._delayed = False
128 self._divert = False
129 self._divert = False
129 self.filteredrevs = frozenset()
130 self.filteredrevs = frozenset()
130
131
131 def tip(self):
132 def tip(self):
132 """filtered version of revlog.tip"""
133 """filtered version of revlog.tip"""
133 for i in xrange(len(self) -1, -2, -1):
134 for i in xrange(len(self) -1, -2, -1):
134 if i not in self.filteredrevs:
135 if i not in self.filteredrevs:
135 return self.node(i)
136 return self.node(i)
136
137
137 def __iter__(self):
138 def __iter__(self):
138 """filtered version of revlog.__iter__"""
139 """filtered version of revlog.__iter__"""
139 if len(self.filteredrevs) == 0:
140 if len(self.filteredrevs) == 0:
140 return revlog.revlog.__iter__(self)
141 return revlog.revlog.__iter__(self)
141
142
142 def filterediter():
143 def filterediter():
143 for i in xrange(len(self)):
144 for i in xrange(len(self)):
144 if i not in self.filteredrevs:
145 if i not in self.filteredrevs:
145 yield i
146 yield i
146
147
147 return filterediter()
148 return filterediter()
148
149
149 def revs(self, start=0, stop=None):
150 def revs(self, start=0, stop=None):
150 """filtered version of revlog.revs"""
151 """filtered version of revlog.revs"""
151 for i in super(changelog, self).revs(start, stop):
152 for i in super(changelog, self).revs(start, stop):
152 if i not in self.filteredrevs:
153 if i not in self.filteredrevs:
153 yield i
154 yield i
154
155
155 @util.propertycache
156 @util.propertycache
156 def nodemap(self):
157 def nodemap(self):
157 # XXX need filtering too
158 # XXX need filtering too
158 self.rev(self.node(0))
159 self.rev(self.node(0))
159 return self._nodecache
160 return self._nodecache
160
161
161 def hasnode(self, node):
162 def hasnode(self, node):
162 """filtered version of revlog.hasnode"""
163 """filtered version of revlog.hasnode"""
163 try:
164 try:
164 i = self.rev(node)
165 i = self.rev(node)
165 return i not in self.filteredrevs
166 return i not in self.filteredrevs
166 except KeyError:
167 except KeyError:
167 return False
168 return False
168
169
169 def headrevs(self):
170 def headrevs(self):
170 if self.filteredrevs:
171 if self.filteredrevs:
171 # XXX we should fix and use the C version
172 # XXX we should fix and use the C version
172 return self._headrevs()
173 return self._headrevs()
173 return super(changelog, self).headrevs()
174 return super(changelog, self).headrevs()
174
175
175 def strip(self, *args, **kwargs):
176 def strip(self, *args, **kwargs):
176 # XXX make something better than assert
177 # XXX make something better than assert
177 # We can't expect proper strip behavior if we are filtered.
178 # We can't expect proper strip behavior if we are filtered.
178 assert not self.filteredrevs
179 assert not self.filteredrevs
179 super(changelog, self).strip(*args, **kwargs)
180 super(changelog, self).strip(*args, **kwargs)
180
181
181 def rev(self, node):
182 def rev(self, node):
182 """filtered version of revlog.rev"""
183 """filtered version of revlog.rev"""
183 r = super(changelog, self).rev(node)
184 r = super(changelog, self).rev(node)
184 if r in self.filteredrevs:
185 if r in self.filteredrevs:
185 raise error.LookupError(node, self.indexfile, _('no node'))
186 raise error.LookupError(node, self.indexfile, _('no node'))
186 return r
187 return r
187
188
188 def node(self, rev):
189 def node(self, rev):
189 """filtered version of revlog.node"""
190 """filtered version of revlog.node"""
190 if rev in self.filteredrevs:
191 if rev in self.filteredrevs:
191 raise IndexError(rev)
192 raise IndexError(rev)
192 return super(changelog, self).node(rev)
193 return super(changelog, self).node(rev)
193
194
194 def linkrev(self, rev):
195 def linkrev(self, rev):
195 """filtered version of revlog.linkrev"""
196 """filtered version of revlog.linkrev"""
196 if rev in self.filteredrevs:
197 if rev in self.filteredrevs:
197 raise IndexError(rev)
198 raise IndexError(rev)
198 return super(changelog, self).linkrev(rev)
199 return super(changelog, self).linkrev(rev)
199
200
200 def parentrevs(self, rev):
201 def parentrevs(self, rev):
201 """filtered version of revlog.parentrevs"""
202 """filtered version of revlog.parentrevs"""
202 if rev in self.filteredrevs:
203 if rev in self.filteredrevs:
203 raise IndexError(rev)
204 raise IndexError(rev)
204 return super(changelog, self).parentrevs(rev)
205 return super(changelog, self).parentrevs(rev)
205
206
206 def flags(self, rev):
207 def flags(self, rev):
207 """filtered version of revlog.flags"""
208 """filtered version of revlog.flags"""
208 if rev in self.filteredrevs:
209 if rev in self.filteredrevs:
209 raise IndexError(rev)
210 raise IndexError(rev)
210 return super(changelog, self).flags(rev)
211 return super(changelog, self).flags(rev)
211
212
212 def delayupdate(self):
213 def delayupdate(self):
213 "delay visibility of index updates to other readers"
214 "delay visibility of index updates to other readers"
214 self._delayed = True
215 self._delayed = True
215 self._divert = (len(self) == 0)
216 self._divert = (len(self) == 0)
216 self._delaybuf = []
217 self._delaybuf = []
217 self.opener = delayopener(self._realopener, self.indexfile,
218 self.opener = delayopener(self._realopener, self.indexfile,
218 self._divert, self._delaybuf)
219 self._divert, self._delaybuf)
219
220
220 def finalize(self, tr):
221 def finalize(self, tr):
221 "finalize index updates"
222 "finalize index updates"
222 self._delayed = False
223 self._delayed = False
223 self.opener = self._realopener
224 self.opener = self._realopener
224 # move redirected index data back into place
225 # move redirected index data back into place
225 if self._divert:
226 if self._divert:
226 nfile = self.opener(self.indexfile + ".a")
227 nfile = self.opener(self.indexfile + ".a")
227 n = nfile.name
228 n = nfile.name
228 nfile.close()
229 nfile.close()
229 util.rename(n, n[:-2])
230 util.rename(n, n[:-2])
230 elif self._delaybuf:
231 elif self._delaybuf:
231 fp = self.opener(self.indexfile, 'a')
232 fp = self.opener(self.indexfile, 'a')
232 fp.write("".join(self._delaybuf))
233 fp.write("".join(self._delaybuf))
233 fp.close()
234 fp.close()
234 self._delaybuf = []
235 self._delaybuf = []
235 # split when we're done
236 # split when we're done
236 self.checkinlinesize(tr)
237 self.checkinlinesize(tr)
237
238
238 def readpending(self, file):
239 def readpending(self, file):
239 r = revlog.revlog(self.opener, file)
240 r = revlog.revlog(self.opener, file)
240 self.index = r.index
241 self.index = r.index
241 self.nodemap = r.nodemap
242 self.nodemap = r.nodemap
242 self._nodecache = r._nodecache
243 self._nodecache = r._nodecache
243 self._chunkcache = r._chunkcache
244 self._chunkcache = r._chunkcache
244
245
245 def writepending(self):
246 def writepending(self):
246 "create a file containing the unfinalized state for pretxnchangegroup"
247 "create a file containing the unfinalized state for pretxnchangegroup"
247 if self._delaybuf:
248 if self._delaybuf:
248 # make a temporary copy of the index
249 # make a temporary copy of the index
249 fp1 = self._realopener(self.indexfile)
250 fp1 = self._realopener(self.indexfile)
250 fp2 = self._realopener(self.indexfile + ".a", "w")
251 fp2 = self._realopener(self.indexfile + ".a", "w")
251 fp2.write(fp1.read())
252 fp2.write(fp1.read())
252 # add pending data
253 # add pending data
253 fp2.write("".join(self._delaybuf))
254 fp2.write("".join(self._delaybuf))
254 fp2.close()
255 fp2.close()
255 # switch modes so finalize can simply rename
256 # switch modes so finalize can simply rename
256 self._delaybuf = []
257 self._delaybuf = []
257 self._divert = True
258 self._divert = True
258
259
259 if self._divert:
260 if self._divert:
260 return True
261 return True
261
262
262 return False
263 return False
263
264
264 def checkinlinesize(self, tr, fp=None):
265 def checkinlinesize(self, tr, fp=None):
265 if not self._delayed:
266 if not self._delayed:
266 revlog.revlog.checkinlinesize(self, tr, fp)
267 revlog.revlog.checkinlinesize(self, tr, fp)
267
268
268 def read(self, node):
269 def read(self, node):
269 """
270 """
270 format used:
271 format used:
271 nodeid\n : manifest node in ascii
272 nodeid\n : manifest node in ascii
272 user\n : user, no \n or \r allowed
273 user\n : user, no \n or \r allowed
273 time tz extra\n : date (time is int or float, timezone is int)
274 time tz extra\n : date (time is int or float, timezone is int)
274 : extra is metadata, encoded and separated by '\0'
275 : extra is metadata, encoded and separated by '\0'
275 : older versions ignore it
276 : older versions ignore it
276 files\n\n : files modified by the cset, no \n or \r allowed
277 files\n\n : files modified by the cset, no \n or \r allowed
277 (.*) : comment (free text, ideally utf-8)
278 (.*) : comment (free text, ideally utf-8)
278
279
279 changelog v0 doesn't use extra
280 changelog v0 doesn't use extra
280 """
281 """
281 text = self.revision(node)
282 text = self.revision(node)
282 if not text:
283 if not text:
283 return (nullid, "", (0, 0), [], "", _defaultextra)
284 return (nullid, "", (0, 0), [], "", _defaultextra)
284 last = text.index("\n\n")
285 last = text.index("\n\n")
285 desc = encoding.tolocal(text[last + 2:])
286 desc = encoding.tolocal(text[last + 2:])
286 l = text[:last].split('\n')
287 l = text[:last].split('\n')
287 manifest = bin(l[0])
288 manifest = bin(l[0])
288 user = encoding.tolocal(l[1])
289 user = encoding.tolocal(l[1])
289
290
290 tdata = l[2].split(' ', 2)
291 tdata = l[2].split(' ', 2)
291 if len(tdata) != 3:
292 if len(tdata) != 3:
292 time = float(tdata[0])
293 time = float(tdata[0])
293 try:
294 try:
294 # various tools did silly things with the time zone field.
295 # various tools did silly things with the time zone field.
295 timezone = int(tdata[1])
296 timezone = int(tdata[1])
296 except ValueError:
297 except ValueError:
297 timezone = 0
298 timezone = 0
298 extra = _defaultextra
299 extra = _defaultextra
299 else:
300 else:
300 time, timezone = float(tdata[0]), int(tdata[1])
301 time, timezone = float(tdata[0]), int(tdata[1])
301 extra = decodeextra(tdata[2])
302 extra = decodeextra(tdata[2])
302
303
303 files = l[3:]
304 files = l[3:]
304 return (manifest, user, (time, timezone), files, desc, extra)
305 return (manifest, user, (time, timezone), files, desc, extra)
305
306
306 def add(self, manifest, files, desc, transaction, p1, p2,
307 def add(self, manifest, files, desc, transaction, p1, p2,
307 user, date=None, extra=None):
308 user, date=None, extra=None):
308 # Convert to UTF-8 encoded bytestrings as the very first
309 # Convert to UTF-8 encoded bytestrings as the very first
309 # thing: calling any method on a localstr object will turn it
310 # thing: calling any method on a localstr object will turn it
310 # into a str object and the cached UTF-8 string is thus lost.
311 # into a str object and the cached UTF-8 string is thus lost.
311 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
312 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
312
313
313 user = user.strip()
314 user = user.strip()
314 # An empty username or a username with a "\n" will make the
315 # An empty username or a username with a "\n" will make the
315 # revision text contain two "\n\n" sequences -> corrupt
316 # revision text contain two "\n\n" sequences -> corrupt
316 # repository since read cannot unpack the revision.
317 # repository since read cannot unpack the revision.
317 if not user:
318 if not user:
318 raise error.RevlogError(_("empty username"))
319 raise error.RevlogError(_("empty username"))
319 if "\n" in user:
320 if "\n" in user:
320 raise error.RevlogError(_("username %s contains a newline")
321 raise error.RevlogError(_("username %s contains a newline")
321 % repr(user))
322 % repr(user))
322
323
323 desc = stripdesc(desc)
324 desc = stripdesc(desc)
324
325
325 if date:
326 if date:
326 parseddate = "%d %d" % util.parsedate(date)
327 parseddate = "%d %d" % util.parsedate(date)
327 else:
328 else:
328 parseddate = "%d %d" % util.makedate()
329 parseddate = "%d %d" % util.makedate()
329 if extra:
330 if extra:
330 branch = extra.get("branch")
331 branch = extra.get("branch")
331 if branch in ("default", ""):
332 if branch in ("default", ""):
332 del extra["branch"]
333 del extra["branch"]
333 elif branch in (".", "null", "tip"):
334 elif branch in (".", "null", "tip"):
334 raise error.RevlogError(_('the name \'%s\' is reserved')
335 raise error.RevlogError(_('the name \'%s\' is reserved')
335 % branch)
336 % branch)
336 if extra:
337 if extra:
337 extra = encodeextra(extra)
338 extra = encodeextra(extra)
338 parseddate = "%s %s" % (parseddate, extra)
339 parseddate = "%s %s" % (parseddate, extra)
339 l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc]
340 l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc]
340 text = "\n".join(l)
341 text = "\n".join(l)
341 return self.addrevision(text, transaction, len(self), p1, p2)
342 return self.addrevision(text, transaction, len(self), p1, p2)
342
343
343 def branch(self, rev):
344 def branch(self, rev):
344 """return the branch of a revision
345 """return the branch of a revision
345
346
346 This function exists because creating a changectx object
347 This function exists because creating a changectx object
347 just to access this is costly."""
348 just to access this is costly."""
348 return encoding.tolocal(self.read(rev)[5].get("branch"))
349 return encoding.tolocal(self.read(rev)[5].get("branch"))
General Comments 0
You need to be logged in to leave comments. Login now