##// END OF EJS Templates
doctest: replace chr() with pycompat.bytechr()
Yuya Nishihara -
r34135:708b5530 default
parent child Browse files
Show More
@@ -1,543 +1,545
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 __future__ import absolute_import
9 9
10 10 import collections
11 11
12 12 from .i18n import _
13 13 from .node import (
14 14 bin,
15 15 hex,
16 16 nullid,
17 17 )
18 18
19 19 from . import (
20 20 encoding,
21 21 error,
22 22 revlog,
23 23 util,
24 24 )
25 25
26 26 _defaultextra = {'branch': 'default'}
27 27
28 28 def _string_escape(text):
29 29 """
30 >>> from .pycompat import bytechr as chr
30 31 >>> d = {b'nl': chr(10), b'bs': chr(92), b'cr': chr(13), b'nul': chr(0)}
31 32 >>> s = b"ab%(nl)scd%(bs)s%(bs)sn%(nul)sab%(cr)scd%(bs)s%(nl)s" % d
32 33 >>> s
33 34 'ab\\ncd\\\\\\\\n\\x00ab\\rcd\\\\\\n'
34 35 >>> res = _string_escape(s)
35 36 >>> s == util.unescapestr(res)
36 37 True
37 38 """
38 39 # subset of the string_escape codec
39 40 text = text.replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r')
40 41 return text.replace('\0', '\\0')
41 42
42 43 def decodeextra(text):
43 44 """
45 >>> from .pycompat import bytechr as chr
44 46 >>> sorted(decodeextra(encodeextra({b'foo': b'bar', b'baz': chr(0) + b'2'})
45 47 ... ).items())
46 48 [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
47 49 >>> sorted(decodeextra(encodeextra({b'foo': b'bar',
48 50 ... b'baz': chr(92) + chr(0) + b'2'})
49 51 ... ).items())
50 52 [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
51 53 """
52 54 extra = _defaultextra.copy()
53 55 for l in text.split('\0'):
54 56 if l:
55 57 if '\\0' in l:
56 58 # fix up \0 without getting into trouble with \\0
57 59 l = l.replace('\\\\', '\\\\\n')
58 60 l = l.replace('\\0', '\0')
59 61 l = l.replace('\n', '')
60 62 k, v = util.unescapestr(l).split(':', 1)
61 63 extra[k] = v
62 64 return extra
63 65
64 66 def encodeextra(d):
65 67 # keys must be sorted to produce a deterministic changelog entry
66 68 items = [_string_escape('%s:%s' % (k, d[k])) for k in sorted(d)]
67 69 return "\0".join(items)
68 70
69 71 def stripdesc(desc):
70 72 """strip trailing whitespace and leading and trailing empty lines"""
71 73 return '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n')
72 74
73 75 class appender(object):
74 76 '''the changelog index must be updated last on disk, so we use this class
75 77 to delay writes to it'''
76 78 def __init__(self, vfs, name, mode, buf):
77 79 self.data = buf
78 80 fp = vfs(name, mode)
79 81 self.fp = fp
80 82 self.offset = fp.tell()
81 83 self.size = vfs.fstat(fp).st_size
82 84 self._end = self.size
83 85
84 86 def end(self):
85 87 return self._end
86 88 def tell(self):
87 89 return self.offset
88 90 def flush(self):
89 91 pass
90 92 def close(self):
91 93 self.fp.close()
92 94
93 95 def seek(self, offset, whence=0):
94 96 '''virtual file offset spans real file and data'''
95 97 if whence == 0:
96 98 self.offset = offset
97 99 elif whence == 1:
98 100 self.offset += offset
99 101 elif whence == 2:
100 102 self.offset = self.end() + offset
101 103 if self.offset < self.size:
102 104 self.fp.seek(self.offset)
103 105
104 106 def read(self, count=-1):
105 107 '''only trick here is reads that span real file and data'''
106 108 ret = ""
107 109 if self.offset < self.size:
108 110 s = self.fp.read(count)
109 111 ret = s
110 112 self.offset += len(s)
111 113 if count > 0:
112 114 count -= len(s)
113 115 if count != 0:
114 116 doff = self.offset - self.size
115 117 self.data.insert(0, "".join(self.data))
116 118 del self.data[1:]
117 119 s = self.data[0][doff:doff + count]
118 120 self.offset += len(s)
119 121 ret += s
120 122 return ret
121 123
122 124 def write(self, s):
123 125 self.data.append(bytes(s))
124 126 self.offset += len(s)
125 127 self._end += len(s)
126 128
127 129 def _divertopener(opener, target):
128 130 """build an opener that writes in 'target.a' instead of 'target'"""
129 131 def _divert(name, mode='r', checkambig=False):
130 132 if name != target:
131 133 return opener(name, mode)
132 134 return opener(name + ".a", mode)
133 135 return _divert
134 136
135 137 def _delayopener(opener, target, buf):
136 138 """build an opener that stores chunks in 'buf' instead of 'target'"""
137 139 def _delay(name, mode='r', checkambig=False):
138 140 if name != target:
139 141 return opener(name, mode)
140 142 return appender(opener, name, mode, buf)
141 143 return _delay
142 144
143 145 _changelogrevision = collections.namedtuple(u'changelogrevision',
144 146 (u'manifest', u'user', u'date',
145 147 u'files', u'description',
146 148 u'extra'))
147 149
148 150 class changelogrevision(object):
149 151 """Holds results of a parsed changelog revision.
150 152
151 153 Changelog revisions consist of multiple pieces of data, including
152 154 the manifest node, user, and date. This object exposes a view into
153 155 the parsed object.
154 156 """
155 157
156 158 __slots__ = (
157 159 u'_offsets',
158 160 u'_text',
159 161 )
160 162
161 163 def __new__(cls, text):
162 164 if not text:
163 165 return _changelogrevision(
164 166 manifest=nullid,
165 167 user='',
166 168 date=(0, 0),
167 169 files=[],
168 170 description='',
169 171 extra=_defaultextra,
170 172 )
171 173
172 174 self = super(changelogrevision, cls).__new__(cls)
173 175 # We could return here and implement the following as an __init__.
174 176 # But doing it here is equivalent and saves an extra function call.
175 177
176 178 # format used:
177 179 # nodeid\n : manifest node in ascii
178 180 # user\n : user, no \n or \r allowed
179 181 # time tz extra\n : date (time is int or float, timezone is int)
180 182 # : extra is metadata, encoded and separated by '\0'
181 183 # : older versions ignore it
182 184 # files\n\n : files modified by the cset, no \n or \r allowed
183 185 # (.*) : comment (free text, ideally utf-8)
184 186 #
185 187 # changelog v0 doesn't use extra
186 188
187 189 nl1 = text.index('\n')
188 190 nl2 = text.index('\n', nl1 + 1)
189 191 nl3 = text.index('\n', nl2 + 1)
190 192
191 193 # The list of files may be empty. Which means nl3 is the first of the
192 194 # double newline that precedes the description.
193 195 if text[nl3 + 1:nl3 + 2] == '\n':
194 196 doublenl = nl3
195 197 else:
196 198 doublenl = text.index('\n\n', nl3 + 1)
197 199
198 200 self._offsets = (nl1, nl2, nl3, doublenl)
199 201 self._text = text
200 202
201 203 return self
202 204
203 205 @property
204 206 def manifest(self):
205 207 return bin(self._text[0:self._offsets[0]])
206 208
207 209 @property
208 210 def user(self):
209 211 off = self._offsets
210 212 return encoding.tolocal(self._text[off[0] + 1:off[1]])
211 213
212 214 @property
213 215 def _rawdate(self):
214 216 off = self._offsets
215 217 dateextra = self._text[off[1] + 1:off[2]]
216 218 return dateextra.split(' ', 2)[0:2]
217 219
218 220 @property
219 221 def _rawextra(self):
220 222 off = self._offsets
221 223 dateextra = self._text[off[1] + 1:off[2]]
222 224 fields = dateextra.split(' ', 2)
223 225 if len(fields) != 3:
224 226 return None
225 227
226 228 return fields[2]
227 229
228 230 @property
229 231 def date(self):
230 232 raw = self._rawdate
231 233 time = float(raw[0])
232 234 # Various tools did silly things with the timezone.
233 235 try:
234 236 timezone = int(raw[1])
235 237 except ValueError:
236 238 timezone = 0
237 239
238 240 return time, timezone
239 241
240 242 @property
241 243 def extra(self):
242 244 raw = self._rawextra
243 245 if raw is None:
244 246 return _defaultextra
245 247
246 248 return decodeextra(raw)
247 249
248 250 @property
249 251 def files(self):
250 252 off = self._offsets
251 253 if off[2] == off[3]:
252 254 return []
253 255
254 256 return self._text[off[2] + 1:off[3]].split('\n')
255 257
256 258 @property
257 259 def description(self):
258 260 return encoding.tolocal(self._text[self._offsets[3] + 2:])
259 261
260 262 class changelog(revlog.revlog):
261 263 def __init__(self, opener, trypending=False):
262 264 """Load a changelog revlog using an opener.
263 265
264 266 If ``trypending`` is true, we attempt to load the index from a
265 267 ``00changelog.i.a`` file instead of the default ``00changelog.i``.
266 268 The ``00changelog.i.a`` file contains index (and possibly inline
267 269 revision) data for a transaction that hasn't been finalized yet.
268 270 It exists in a separate file to facilitate readers (such as
269 271 hooks processes) accessing data before a transaction is finalized.
270 272 """
271 273 if trypending and opener.exists('00changelog.i.a'):
272 274 indexfile = '00changelog.i.a'
273 275 else:
274 276 indexfile = '00changelog.i'
275 277
276 278 datafile = '00changelog.d'
277 279 revlog.revlog.__init__(self, opener, indexfile, datafile=datafile,
278 280 checkambig=True)
279 281
280 282 if self._initempty:
281 283 # changelogs don't benefit from generaldelta
282 284 self.version &= ~revlog.FLAG_GENERALDELTA
283 285 self._generaldelta = False
284 286
285 287 # Delta chains for changelogs tend to be very small because entries
286 288 # tend to be small and don't delta well with each. So disable delta
287 289 # chains.
288 290 self.storedeltachains = False
289 291
290 292 self._realopener = opener
291 293 self._delayed = False
292 294 self._delaybuf = None
293 295 self._divert = False
294 296 self.filteredrevs = frozenset()
295 297
296 298 def tip(self):
297 299 """filtered version of revlog.tip"""
298 300 for i in xrange(len(self) -1, -2, -1):
299 301 if i not in self.filteredrevs:
300 302 return self.node(i)
301 303
302 304 def __contains__(self, rev):
303 305 """filtered version of revlog.__contains__"""
304 306 return (0 <= rev < len(self)
305 307 and rev not in self.filteredrevs)
306 308
307 309 def __iter__(self):
308 310 """filtered version of revlog.__iter__"""
309 311 if len(self.filteredrevs) == 0:
310 312 return revlog.revlog.__iter__(self)
311 313
312 314 def filterediter():
313 315 for i in xrange(len(self)):
314 316 if i not in self.filteredrevs:
315 317 yield i
316 318
317 319 return filterediter()
318 320
319 321 def revs(self, start=0, stop=None):
320 322 """filtered version of revlog.revs"""
321 323 for i in super(changelog, self).revs(start, stop):
322 324 if i not in self.filteredrevs:
323 325 yield i
324 326
325 327 @util.propertycache
326 328 def nodemap(self):
327 329 # XXX need filtering too
328 330 self.rev(self.node(0))
329 331 return self._nodecache
330 332
331 333 def reachableroots(self, minroot, heads, roots, includepath=False):
332 334 return self.index.reachableroots2(minroot, heads, roots, includepath)
333 335
334 336 def headrevs(self):
335 337 if self.filteredrevs:
336 338 try:
337 339 return self.index.headrevsfiltered(self.filteredrevs)
338 340 # AttributeError covers non-c-extension environments and
339 341 # old c extensions without filter handling.
340 342 except AttributeError:
341 343 return self._headrevs()
342 344
343 345 return super(changelog, self).headrevs()
344 346
345 347 def strip(self, *args, **kwargs):
346 348 # XXX make something better than assert
347 349 # We can't expect proper strip behavior if we are filtered.
348 350 assert not self.filteredrevs
349 351 super(changelog, self).strip(*args, **kwargs)
350 352
351 353 def rev(self, node):
352 354 """filtered version of revlog.rev"""
353 355 r = super(changelog, self).rev(node)
354 356 if r in self.filteredrevs:
355 357 raise error.FilteredLookupError(hex(node), self.indexfile,
356 358 _('filtered node'))
357 359 return r
358 360
359 361 def node(self, rev):
360 362 """filtered version of revlog.node"""
361 363 if rev in self.filteredrevs:
362 364 raise error.FilteredIndexError(rev)
363 365 return super(changelog, self).node(rev)
364 366
365 367 def linkrev(self, rev):
366 368 """filtered version of revlog.linkrev"""
367 369 if rev in self.filteredrevs:
368 370 raise error.FilteredIndexError(rev)
369 371 return super(changelog, self).linkrev(rev)
370 372
371 373 def parentrevs(self, rev):
372 374 """filtered version of revlog.parentrevs"""
373 375 if rev in self.filteredrevs:
374 376 raise error.FilteredIndexError(rev)
375 377 return super(changelog, self).parentrevs(rev)
376 378
377 379 def flags(self, rev):
378 380 """filtered version of revlog.flags"""
379 381 if rev in self.filteredrevs:
380 382 raise error.FilteredIndexError(rev)
381 383 return super(changelog, self).flags(rev)
382 384
383 385 def delayupdate(self, tr):
384 386 "delay visibility of index updates to other readers"
385 387
386 388 if not self._delayed:
387 389 if len(self) == 0:
388 390 self._divert = True
389 391 if self._realopener.exists(self.indexfile + '.a'):
390 392 self._realopener.unlink(self.indexfile + '.a')
391 393 self.opener = _divertopener(self._realopener, self.indexfile)
392 394 else:
393 395 self._delaybuf = []
394 396 self.opener = _delayopener(self._realopener, self.indexfile,
395 397 self._delaybuf)
396 398 self._delayed = True
397 399 tr.addpending('cl-%i' % id(self), self._writepending)
398 400 tr.addfinalize('cl-%i' % id(self), self._finalize)
399 401
400 402 def _finalize(self, tr):
401 403 "finalize index updates"
402 404 self._delayed = False
403 405 self.opener = self._realopener
404 406 # move redirected index data back into place
405 407 if self._divert:
406 408 assert not self._delaybuf
407 409 tmpname = self.indexfile + ".a"
408 410 nfile = self.opener.open(tmpname)
409 411 nfile.close()
410 412 self.opener.rename(tmpname, self.indexfile, checkambig=True)
411 413 elif self._delaybuf:
412 414 fp = self.opener(self.indexfile, 'a', checkambig=True)
413 415 fp.write("".join(self._delaybuf))
414 416 fp.close()
415 417 self._delaybuf = None
416 418 self._divert = False
417 419 # split when we're done
418 420 self.checkinlinesize(tr)
419 421
420 422 def _writepending(self, tr):
421 423 "create a file containing the unfinalized state for pretxnchangegroup"
422 424 if self._delaybuf:
423 425 # make a temporary copy of the index
424 426 fp1 = self._realopener(self.indexfile)
425 427 pendingfilename = self.indexfile + ".a"
426 428 # register as a temp file to ensure cleanup on failure
427 429 tr.registertmp(pendingfilename)
428 430 # write existing data
429 431 fp2 = self._realopener(pendingfilename, "w")
430 432 fp2.write(fp1.read())
431 433 # add pending data
432 434 fp2.write("".join(self._delaybuf))
433 435 fp2.close()
434 436 # switch modes so finalize can simply rename
435 437 self._delaybuf = None
436 438 self._divert = True
437 439 self.opener = _divertopener(self._realopener, self.indexfile)
438 440
439 441 if self._divert:
440 442 return True
441 443
442 444 return False
443 445
444 446 def checkinlinesize(self, tr, fp=None):
445 447 if not self._delayed:
446 448 revlog.revlog.checkinlinesize(self, tr, fp)
447 449
448 450 def read(self, node):
449 451 """Obtain data from a parsed changelog revision.
450 452
451 453 Returns a 6-tuple of:
452 454
453 455 - manifest node in binary
454 456 - author/user as a localstr
455 457 - date as a 2-tuple of (time, timezone)
456 458 - list of files
457 459 - commit message as a localstr
458 460 - dict of extra metadata
459 461
460 462 Unless you need to access all fields, consider calling
461 463 ``changelogrevision`` instead, as it is faster for partial object
462 464 access.
463 465 """
464 466 c = changelogrevision(self.revision(node))
465 467 return (
466 468 c.manifest,
467 469 c.user,
468 470 c.date,
469 471 c.files,
470 472 c.description,
471 473 c.extra
472 474 )
473 475
474 476 def changelogrevision(self, nodeorrev):
475 477 """Obtain a ``changelogrevision`` for a node or revision."""
476 478 return changelogrevision(self.revision(nodeorrev))
477 479
478 480 def readfiles(self, node):
479 481 """
480 482 short version of read that only returns the files modified by the cset
481 483 """
482 484 text = self.revision(node)
483 485 if not text:
484 486 return []
485 487 last = text.index("\n\n")
486 488 l = text[:last].split('\n')
487 489 return l[3:]
488 490
489 491 def add(self, manifest, files, desc, transaction, p1, p2,
490 492 user, date=None, extra=None):
491 493 # Convert to UTF-8 encoded bytestrings as the very first
492 494 # thing: calling any method on a localstr object will turn it
493 495 # into a str object and the cached UTF-8 string is thus lost.
494 496 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
495 497
496 498 user = user.strip()
497 499 # An empty username or a username with a "\n" will make the
498 500 # revision text contain two "\n\n" sequences -> corrupt
499 501 # repository since read cannot unpack the revision.
500 502 if not user:
501 503 raise error.RevlogError(_("empty username"))
502 504 if "\n" in user:
503 505 raise error.RevlogError(_("username %s contains a newline")
504 506 % repr(user))
505 507
506 508 desc = stripdesc(desc)
507 509
508 510 if date:
509 511 parseddate = "%d %d" % util.parsedate(date)
510 512 else:
511 513 parseddate = "%d %d" % util.makedate()
512 514 if extra:
513 515 branch = extra.get("branch")
514 516 if branch in ("default", ""):
515 517 del extra["branch"]
516 518 elif branch in (".", "null", "tip"):
517 519 raise error.RevlogError(_('the name \'%s\' is reserved')
518 520 % branch)
519 521 if extra:
520 522 extra = encodeextra(extra)
521 523 parseddate = "%s %s" % (parseddate, extra)
522 524 l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc]
523 525 text = "\n".join(l)
524 526 return self.addrevision(text, transaction, len(self), p1, p2)
525 527
526 528 def branchinfo(self, rev):
527 529 """return the branch name and open/close state of a revision
528 530
529 531 This function exists because creating a changectx object
530 532 just to access this is costly."""
531 533 extra = self.read(rev)[5]
532 534 return encoding.tolocal(extra.get("branch")), 'close' in extra
533 535
534 536 def _addrevision(self, node, rawtext, transaction, *args, **kwargs):
535 537 # overlay over the standard revlog._addrevision to track the new
536 538 # revision on the transaction.
537 539 rev = len(self)
538 540 node = super(changelog, self)._addrevision(node, rawtext, transaction,
539 541 *args, **kwargs)
540 542 revs = transaction.changes.get('revs')
541 543 if revs is not None:
542 544 revs.add(rev)
543 545 return node
General Comments 0
You need to be logged in to leave comments. Login now