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