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