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