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