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