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