##// END OF EJS Templates
repoview: move changelog.tiprev() override to filteredchangelog...
Martin von Zweigbergk -
r43748:7bc8e49a default
parent child Browse files
Show More
@@ -1,726 +1,720 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 attr
17 17
18 18 from . import (
19 19 copies,
20 20 encoding,
21 21 error,
22 22 pycompat,
23 23 revlog,
24 24 util,
25 25 )
26 26 from .utils import (
27 27 dateutil,
28 28 stringutil,
29 29 )
30 30
31 31 from .revlogutils import sidedata as sidedatamod
32 32
33 33 _defaultextra = {b'branch': b'default'}
34 34
35 35
36 36 def _string_escape(text):
37 37 """
38 38 >>> from .pycompat import bytechr as chr
39 39 >>> d = {b'nl': chr(10), b'bs': chr(92), b'cr': chr(13), b'nul': chr(0)}
40 40 >>> s = b"ab%(nl)scd%(bs)s%(bs)sn%(nul)s12ab%(cr)scd%(bs)s%(nl)s" % d
41 41 >>> s
42 42 'ab\\ncd\\\\\\\\n\\x0012ab\\rcd\\\\\\n'
43 43 >>> res = _string_escape(s)
44 44 >>> s == _string_unescape(res)
45 45 True
46 46 """
47 47 # subset of the string_escape codec
48 48 text = (
49 49 text.replace(b'\\', b'\\\\')
50 50 .replace(b'\n', b'\\n')
51 51 .replace(b'\r', b'\\r')
52 52 )
53 53 return text.replace(b'\0', b'\\0')
54 54
55 55
56 56 def _string_unescape(text):
57 57 if b'\\0' in text:
58 58 # fix up \0 without getting into trouble with \\0
59 59 text = text.replace(b'\\\\', b'\\\\\n')
60 60 text = text.replace(b'\\0', b'\0')
61 61 text = text.replace(b'\n', b'')
62 62 return stringutil.unescapestr(text)
63 63
64 64
65 65 def decodeextra(text):
66 66 """
67 67 >>> from .pycompat import bytechr as chr
68 68 >>> sorted(decodeextra(encodeextra({b'foo': b'bar', b'baz': chr(0) + b'2'})
69 69 ... ).items())
70 70 [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
71 71 >>> sorted(decodeextra(encodeextra({b'foo': b'bar',
72 72 ... b'baz': chr(92) + chr(0) + b'2'})
73 73 ... ).items())
74 74 [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
75 75 """
76 76 extra = _defaultextra.copy()
77 77 for l in text.split(b'\0'):
78 78 if l:
79 79 k, v = _string_unescape(l).split(b':', 1)
80 80 extra[k] = v
81 81 return extra
82 82
83 83
84 84 def encodeextra(d):
85 85 # keys must be sorted to produce a deterministic changelog entry
86 86 items = [
87 87 _string_escape(b'%s:%s' % (k, pycompat.bytestr(d[k])))
88 88 for k in sorted(d)
89 89 ]
90 90 return b"\0".join(items)
91 91
92 92
93 93 def stripdesc(desc):
94 94 """strip trailing whitespace and leading and trailing empty lines"""
95 95 return b'\n'.join([l.rstrip() for l in desc.splitlines()]).strip(b'\n')
96 96
97 97
98 98 class appender(object):
99 99 '''the changelog index must be updated last on disk, so we use this class
100 100 to delay writes to it'''
101 101
102 102 def __init__(self, vfs, name, mode, buf):
103 103 self.data = buf
104 104 fp = vfs(name, mode)
105 105 self.fp = fp
106 106 self.offset = fp.tell()
107 107 self.size = vfs.fstat(fp).st_size
108 108 self._end = self.size
109 109
110 110 def end(self):
111 111 return self._end
112 112
113 113 def tell(self):
114 114 return self.offset
115 115
116 116 def flush(self):
117 117 pass
118 118
119 119 @property
120 120 def closed(self):
121 121 return self.fp.closed
122 122
123 123 def close(self):
124 124 self.fp.close()
125 125
126 126 def seek(self, offset, whence=0):
127 127 '''virtual file offset spans real file and data'''
128 128 if whence == 0:
129 129 self.offset = offset
130 130 elif whence == 1:
131 131 self.offset += offset
132 132 elif whence == 2:
133 133 self.offset = self.end() + offset
134 134 if self.offset < self.size:
135 135 self.fp.seek(self.offset)
136 136
137 137 def read(self, count=-1):
138 138 '''only trick here is reads that span real file and data'''
139 139 ret = b""
140 140 if self.offset < self.size:
141 141 s = self.fp.read(count)
142 142 ret = s
143 143 self.offset += len(s)
144 144 if count > 0:
145 145 count -= len(s)
146 146 if count != 0:
147 147 doff = self.offset - self.size
148 148 self.data.insert(0, b"".join(self.data))
149 149 del self.data[1:]
150 150 s = self.data[0][doff : doff + count]
151 151 self.offset += len(s)
152 152 ret += s
153 153 return ret
154 154
155 155 def write(self, s):
156 156 self.data.append(bytes(s))
157 157 self.offset += len(s)
158 158 self._end += len(s)
159 159
160 160 def __enter__(self):
161 161 self.fp.__enter__()
162 162 return self
163 163
164 164 def __exit__(self, *args):
165 165 return self.fp.__exit__(*args)
166 166
167 167
168 168 def _divertopener(opener, target):
169 169 """build an opener that writes in 'target.a' instead of 'target'"""
170 170
171 171 def _divert(name, mode=b'r', checkambig=False):
172 172 if name != target:
173 173 return opener(name, mode)
174 174 return opener(name + b".a", mode)
175 175
176 176 return _divert
177 177
178 178
179 179 def _delayopener(opener, target, buf):
180 180 """build an opener that stores chunks in 'buf' instead of 'target'"""
181 181
182 182 def _delay(name, mode=b'r', checkambig=False):
183 183 if name != target:
184 184 return opener(name, mode)
185 185 return appender(opener, name, mode, buf)
186 186
187 187 return _delay
188 188
189 189
190 190 @attr.s
191 191 class _changelogrevision(object):
192 192 # Extensions might modify _defaultextra, so let the constructor below pass
193 193 # it in
194 194 extra = attr.ib()
195 195 manifest = attr.ib(default=nullid)
196 196 user = attr.ib(default=b'')
197 197 date = attr.ib(default=(0, 0))
198 198 files = attr.ib(default=attr.Factory(list))
199 199 filesadded = attr.ib(default=None)
200 200 filesremoved = attr.ib(default=None)
201 201 p1copies = attr.ib(default=None)
202 202 p2copies = attr.ib(default=None)
203 203 description = attr.ib(default=b'')
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 r'_offsets',
216 216 r'_text',
217 217 r'_sidedata',
218 218 r'_cpsd',
219 219 )
220 220
221 221 def __new__(cls, text, sidedata, cpsd):
222 222 if not text:
223 223 return _changelogrevision(extra=_defaultextra)
224 224
225 225 self = super(changelogrevision, cls).__new__(cls)
226 226 # We could return here and implement the following as an __init__.
227 227 # But doing it here is equivalent and saves an extra function call.
228 228
229 229 # format used:
230 230 # nodeid\n : manifest node in ascii
231 231 # user\n : user, no \n or \r allowed
232 232 # time tz extra\n : date (time is int or float, timezone is int)
233 233 # : extra is metadata, encoded and separated by '\0'
234 234 # : older versions ignore it
235 235 # files\n\n : files modified by the cset, no \n or \r allowed
236 236 # (.*) : comment (free text, ideally utf-8)
237 237 #
238 238 # changelog v0 doesn't use extra
239 239
240 240 nl1 = text.index(b'\n')
241 241 nl2 = text.index(b'\n', nl1 + 1)
242 242 nl3 = text.index(b'\n', nl2 + 1)
243 243
244 244 # The list of files may be empty. Which means nl3 is the first of the
245 245 # double newline that precedes the description.
246 246 if text[nl3 + 1 : nl3 + 2] == b'\n':
247 247 doublenl = nl3
248 248 else:
249 249 doublenl = text.index(b'\n\n', nl3 + 1)
250 250
251 251 self._offsets = (nl1, nl2, nl3, doublenl)
252 252 self._text = text
253 253 self._sidedata = sidedata
254 254 self._cpsd = cpsd
255 255
256 256 return self
257 257
258 258 @property
259 259 def manifest(self):
260 260 return bin(self._text[0 : self._offsets[0]])
261 261
262 262 @property
263 263 def user(self):
264 264 off = self._offsets
265 265 return encoding.tolocal(self._text[off[0] + 1 : off[1]])
266 266
267 267 @property
268 268 def _rawdate(self):
269 269 off = self._offsets
270 270 dateextra = self._text[off[1] + 1 : off[2]]
271 271 return dateextra.split(b' ', 2)[0:2]
272 272
273 273 @property
274 274 def _rawextra(self):
275 275 off = self._offsets
276 276 dateextra = self._text[off[1] + 1 : off[2]]
277 277 fields = dateextra.split(b' ', 2)
278 278 if len(fields) != 3:
279 279 return None
280 280
281 281 return fields[2]
282 282
283 283 @property
284 284 def date(self):
285 285 raw = self._rawdate
286 286 time = float(raw[0])
287 287 # Various tools did silly things with the timezone.
288 288 try:
289 289 timezone = int(raw[1])
290 290 except ValueError:
291 291 timezone = 0
292 292
293 293 return time, timezone
294 294
295 295 @property
296 296 def extra(self):
297 297 raw = self._rawextra
298 298 if raw is None:
299 299 return _defaultextra
300 300
301 301 return decodeextra(raw)
302 302
303 303 @property
304 304 def files(self):
305 305 off = self._offsets
306 306 if off[2] == off[3]:
307 307 return []
308 308
309 309 return self._text[off[2] + 1 : off[3]].split(b'\n')
310 310
311 311 @property
312 312 def filesadded(self):
313 313 if self._cpsd:
314 314 rawindices = self._sidedata.get(sidedatamod.SD_FILESADDED)
315 315 if not rawindices:
316 316 return []
317 317 else:
318 318 rawindices = self.extra.get(b'filesadded')
319 319 if rawindices is None:
320 320 return None
321 321 return copies.decodefileindices(self.files, rawindices)
322 322
323 323 @property
324 324 def filesremoved(self):
325 325 if self._cpsd:
326 326 rawindices = self._sidedata.get(sidedatamod.SD_FILESREMOVED)
327 327 if not rawindices:
328 328 return []
329 329 else:
330 330 rawindices = self.extra.get(b'filesremoved')
331 331 if rawindices is None:
332 332 return None
333 333 return copies.decodefileindices(self.files, rawindices)
334 334
335 335 @property
336 336 def p1copies(self):
337 337 if self._cpsd:
338 338 rawcopies = self._sidedata.get(sidedatamod.SD_P1COPIES)
339 339 if not rawcopies:
340 340 return {}
341 341 else:
342 342 rawcopies = self.extra.get(b'p1copies')
343 343 if rawcopies is None:
344 344 return None
345 345 return copies.decodecopies(self.files, rawcopies)
346 346
347 347 @property
348 348 def p2copies(self):
349 349 if self._cpsd:
350 350 rawcopies = self._sidedata.get(sidedatamod.SD_P2COPIES)
351 351 if not rawcopies:
352 352 return {}
353 353 else:
354 354 rawcopies = self.extra.get(b'p2copies')
355 355 if rawcopies is None:
356 356 return None
357 357 return copies.decodecopies(self.files, rawcopies)
358 358
359 359 @property
360 360 def description(self):
361 361 return encoding.tolocal(self._text[self._offsets[3] + 2 :])
362 362
363 363
364 364 class changelog(revlog.revlog):
365 365 def __init__(self, opener, trypending=False):
366 366 """Load a changelog revlog using an opener.
367 367
368 368 If ``trypending`` is true, we attempt to load the index from a
369 369 ``00changelog.i.a`` file instead of the default ``00changelog.i``.
370 370 The ``00changelog.i.a`` file contains index (and possibly inline
371 371 revision) data for a transaction that hasn't been finalized yet.
372 372 It exists in a separate file to facilitate readers (such as
373 373 hooks processes) accessing data before a transaction is finalized.
374 374 """
375 375 if trypending and opener.exists(b'00changelog.i.a'):
376 376 indexfile = b'00changelog.i.a'
377 377 else:
378 378 indexfile = b'00changelog.i'
379 379
380 380 datafile = b'00changelog.d'
381 381 revlog.revlog.__init__(
382 382 self,
383 383 opener,
384 384 indexfile,
385 385 datafile=datafile,
386 386 checkambig=True,
387 387 mmaplargeindex=True,
388 388 )
389 389
390 390 if self._initempty and (self.version & 0xFFFF == revlog.REVLOGV1):
391 391 # changelogs don't benefit from generaldelta.
392 392
393 393 self.version &= ~revlog.FLAG_GENERALDELTA
394 394 self._generaldelta = False
395 395
396 396 # Delta chains for changelogs tend to be very small because entries
397 397 # tend to be small and don't delta well with each. So disable delta
398 398 # chains.
399 399 self._storedeltachains = False
400 400
401 401 self._realopener = opener
402 402 self._delayed = False
403 403 self._delaybuf = None
404 404 self._divert = False
405 405 self.filteredrevs = frozenset()
406 406 self._copiesstorage = opener.options.get(b'copies-storage')
407 407
408 def tiprev(self):
409 """filtered version of revlog.tiprev"""
410 for i in pycompat.xrange(len(self) - 1, -2, -1):
411 if i not in self.filteredrevs:
412 return i
413
414 408 def __contains__(self, rev):
415 409 """filtered version of revlog.__contains__"""
416 410 return 0 <= rev < len(self) and rev not in self.filteredrevs
417 411
418 412 def __iter__(self):
419 413 """filtered version of revlog.__iter__"""
420 414 if len(self.filteredrevs) == 0:
421 415 return revlog.revlog.__iter__(self)
422 416
423 417 def filterediter():
424 418 for i in pycompat.xrange(len(self)):
425 419 if i not in self.filteredrevs:
426 420 yield i
427 421
428 422 return filterediter()
429 423
430 424 def revs(self, start=0, stop=None):
431 425 """filtered version of revlog.revs"""
432 426 for i in super(changelog, self).revs(start, stop):
433 427 if i not in self.filteredrevs:
434 428 yield i
435 429
436 430 def _checknofilteredinrevs(self, revs):
437 431 """raise the appropriate error if 'revs' contains a filtered revision
438 432
439 433 This returns a version of 'revs' to be used thereafter by the caller.
440 434 In particular, if revs is an iterator, it is converted into a set.
441 435 """
442 436 safehasattr = util.safehasattr
443 437 if safehasattr(revs, '__next__'):
444 438 # Note that inspect.isgenerator() is not true for iterators,
445 439 revs = set(revs)
446 440
447 441 filteredrevs = self.filteredrevs
448 442 if safehasattr(revs, 'first'): # smartset
449 443 offenders = revs & filteredrevs
450 444 else:
451 445 offenders = filteredrevs.intersection(revs)
452 446
453 447 for rev in offenders:
454 448 raise error.FilteredIndexError(rev)
455 449 return revs
456 450
457 451 def headrevs(self, revs=None):
458 452 if revs is None and self.filteredrevs:
459 453 try:
460 454 return self.index.headrevsfiltered(self.filteredrevs)
461 455 # AttributeError covers non-c-extension environments and
462 456 # old c extensions without filter handling.
463 457 except AttributeError:
464 458 return self._headrevs()
465 459
466 460 if self.filteredrevs:
467 461 revs = self._checknofilteredinrevs(revs)
468 462 return super(changelog, self).headrevs(revs)
469 463
470 464 def strip(self, *args, **kwargs):
471 465 # XXX make something better than assert
472 466 # We can't expect proper strip behavior if we are filtered.
473 467 assert not self.filteredrevs
474 468 super(changelog, self).strip(*args, **kwargs)
475 469
476 470 def rev(self, node):
477 471 """filtered version of revlog.rev"""
478 472 r = super(changelog, self).rev(node)
479 473 if r in self.filteredrevs:
480 474 raise error.FilteredLookupError(
481 475 hex(node), self.indexfile, _(b'filtered node')
482 476 )
483 477 return r
484 478
485 479 def node(self, rev):
486 480 """filtered version of revlog.node"""
487 481 if rev in self.filteredrevs:
488 482 raise error.FilteredIndexError(rev)
489 483 return super(changelog, self).node(rev)
490 484
491 485 def linkrev(self, rev):
492 486 """filtered version of revlog.linkrev"""
493 487 if rev in self.filteredrevs:
494 488 raise error.FilteredIndexError(rev)
495 489 return super(changelog, self).linkrev(rev)
496 490
497 491 def parentrevs(self, rev):
498 492 """filtered version of revlog.parentrevs"""
499 493 if rev in self.filteredrevs:
500 494 raise error.FilteredIndexError(rev)
501 495 return super(changelog, self).parentrevs(rev)
502 496
503 497 def flags(self, rev):
504 498 """filtered version of revlog.flags"""
505 499 if rev in self.filteredrevs:
506 500 raise error.FilteredIndexError(rev)
507 501 return super(changelog, self).flags(rev)
508 502
509 503 def delayupdate(self, tr):
510 504 b"delay visibility of index updates to other readers"
511 505
512 506 if not self._delayed:
513 507 if len(self) == 0:
514 508 self._divert = True
515 509 if self._realopener.exists(self.indexfile + b'.a'):
516 510 self._realopener.unlink(self.indexfile + b'.a')
517 511 self.opener = _divertopener(self._realopener, self.indexfile)
518 512 else:
519 513 self._delaybuf = []
520 514 self.opener = _delayopener(
521 515 self._realopener, self.indexfile, self._delaybuf
522 516 )
523 517 self._delayed = True
524 518 tr.addpending(b'cl-%i' % id(self), self._writepending)
525 519 tr.addfinalize(b'cl-%i' % id(self), self._finalize)
526 520
527 521 def _finalize(self, tr):
528 522 b"finalize index updates"
529 523 self._delayed = False
530 524 self.opener = self._realopener
531 525 # move redirected index data back into place
532 526 if self._divert:
533 527 assert not self._delaybuf
534 528 tmpname = self.indexfile + b".a"
535 529 nfile = self.opener.open(tmpname)
536 530 nfile.close()
537 531 self.opener.rename(tmpname, self.indexfile, checkambig=True)
538 532 elif self._delaybuf:
539 533 fp = self.opener(self.indexfile, b'a', checkambig=True)
540 534 fp.write(b"".join(self._delaybuf))
541 535 fp.close()
542 536 self._delaybuf = None
543 537 self._divert = False
544 538 # split when we're done
545 539 self._enforceinlinesize(tr)
546 540
547 541 def _writepending(self, tr):
548 542 b"create a file containing the unfinalized state for pretxnchangegroup"
549 543 if self._delaybuf:
550 544 # make a temporary copy of the index
551 545 fp1 = self._realopener(self.indexfile)
552 546 pendingfilename = self.indexfile + b".a"
553 547 # register as a temp file to ensure cleanup on failure
554 548 tr.registertmp(pendingfilename)
555 549 # write existing data
556 550 fp2 = self._realopener(pendingfilename, b"w")
557 551 fp2.write(fp1.read())
558 552 # add pending data
559 553 fp2.write(b"".join(self._delaybuf))
560 554 fp2.close()
561 555 # switch modes so finalize can simply rename
562 556 self._delaybuf = None
563 557 self._divert = True
564 558 self.opener = _divertopener(self._realopener, self.indexfile)
565 559
566 560 if self._divert:
567 561 return True
568 562
569 563 return False
570 564
571 565 def _enforceinlinesize(self, tr, fp=None):
572 566 if not self._delayed:
573 567 revlog.revlog._enforceinlinesize(self, tr, fp)
574 568
575 569 def read(self, node):
576 570 """Obtain data from a parsed changelog revision.
577 571
578 572 Returns a 6-tuple of:
579 573
580 574 - manifest node in binary
581 575 - author/user as a localstr
582 576 - date as a 2-tuple of (time, timezone)
583 577 - list of files
584 578 - commit message as a localstr
585 579 - dict of extra metadata
586 580
587 581 Unless you need to access all fields, consider calling
588 582 ``changelogrevision`` instead, as it is faster for partial object
589 583 access.
590 584 """
591 585 d, s = self._revisiondata(node)
592 586 c = changelogrevision(
593 587 d, s, self._copiesstorage == b'changeset-sidedata'
594 588 )
595 589 return (c.manifest, c.user, c.date, c.files, c.description, c.extra)
596 590
597 591 def changelogrevision(self, nodeorrev):
598 592 """Obtain a ``changelogrevision`` for a node or revision."""
599 593 text, sidedata = self._revisiondata(nodeorrev)
600 594 return changelogrevision(
601 595 text, sidedata, self._copiesstorage == b'changeset-sidedata'
602 596 )
603 597
604 598 def readfiles(self, node):
605 599 """
606 600 short version of read that only returns the files modified by the cset
607 601 """
608 602 text = self.revision(node)
609 603 if not text:
610 604 return []
611 605 last = text.index(b"\n\n")
612 606 l = text[:last].split(b'\n')
613 607 return l[3:]
614 608
615 609 def add(
616 610 self,
617 611 manifest,
618 612 files,
619 613 desc,
620 614 transaction,
621 615 p1,
622 616 p2,
623 617 user,
624 618 date=None,
625 619 extra=None,
626 620 p1copies=None,
627 621 p2copies=None,
628 622 filesadded=None,
629 623 filesremoved=None,
630 624 ):
631 625 # Convert to UTF-8 encoded bytestrings as the very first
632 626 # thing: calling any method on a localstr object will turn it
633 627 # into a str object and the cached UTF-8 string is thus lost.
634 628 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
635 629
636 630 user = user.strip()
637 631 # An empty username or a username with a "\n" will make the
638 632 # revision text contain two "\n\n" sequences -> corrupt
639 633 # repository since read cannot unpack the revision.
640 634 if not user:
641 635 raise error.StorageError(_(b"empty username"))
642 636 if b"\n" in user:
643 637 raise error.StorageError(
644 638 _(b"username %r contains a newline") % pycompat.bytestr(user)
645 639 )
646 640
647 641 desc = stripdesc(desc)
648 642
649 643 if date:
650 644 parseddate = b"%d %d" % dateutil.parsedate(date)
651 645 else:
652 646 parseddate = b"%d %d" % dateutil.makedate()
653 647 if extra:
654 648 branch = extra.get(b"branch")
655 649 if branch in (b"default", b""):
656 650 del extra[b"branch"]
657 651 elif branch in (b".", b"null", b"tip"):
658 652 raise error.StorageError(
659 653 _(b'the name \'%s\' is reserved') % branch
660 654 )
661 655 sortedfiles = sorted(files)
662 656 sidedata = None
663 657 if extra is not None:
664 658 for name in (
665 659 b'p1copies',
666 660 b'p2copies',
667 661 b'filesadded',
668 662 b'filesremoved',
669 663 ):
670 664 extra.pop(name, None)
671 665 if p1copies is not None:
672 666 p1copies = copies.encodecopies(sortedfiles, p1copies)
673 667 if p2copies is not None:
674 668 p2copies = copies.encodecopies(sortedfiles, p2copies)
675 669 if filesadded is not None:
676 670 filesadded = copies.encodefileindices(sortedfiles, filesadded)
677 671 if filesremoved is not None:
678 672 filesremoved = copies.encodefileindices(sortedfiles, filesremoved)
679 673 if self._copiesstorage == b'extra':
680 674 extrasentries = p1copies, p2copies, filesadded, filesremoved
681 675 if extra is None and any(x is not None for x in extrasentries):
682 676 extra = {}
683 677 if p1copies is not None:
684 678 extra[b'p1copies'] = p1copies
685 679 if p2copies is not None:
686 680 extra[b'p2copies'] = p2copies
687 681 if filesadded is not None:
688 682 extra[b'filesadded'] = filesadded
689 683 if filesremoved is not None:
690 684 extra[b'filesremoved'] = filesremoved
691 685 elif self._copiesstorage == b'changeset-sidedata':
692 686 sidedata = {}
693 687 if p1copies:
694 688 sidedata[sidedatamod.SD_P1COPIES] = p1copies
695 689 if p2copies:
696 690 sidedata[sidedatamod.SD_P2COPIES] = p2copies
697 691 if filesadded:
698 692 sidedata[sidedatamod.SD_FILESADDED] = filesadded
699 693 if filesremoved:
700 694 sidedata[sidedatamod.SD_FILESREMOVED] = filesremoved
701 695 if not sidedata:
702 696 sidedata = None
703 697
704 698 if extra:
705 699 extra = encodeextra(extra)
706 700 parseddate = b"%s %s" % (parseddate, extra)
707 701 l = [hex(manifest), user, parseddate] + sortedfiles + [b"", desc]
708 702 text = b"\n".join(l)
709 703 return self.addrevision(
710 704 text, transaction, len(self), p1, p2, sidedata=sidedata
711 705 )
712 706
713 707 def branchinfo(self, rev):
714 708 """return the branch name and open/close state of a revision
715 709
716 710 This function exists because creating a changectx object
717 711 just to access this is costly."""
718 712 extra = self.read(rev)[5]
719 713 return encoding.tolocal(extra.get(b"branch")), b'close' in extra
720 714
721 715 def _nodeduplicatecallback(self, transaction, node):
722 716 # keep track of revisions that got "re-added", eg: unbunde of know rev.
723 717 #
724 718 # We track them in a list to preserve their order from the source bundle
725 719 duplicates = transaction.changes.setdefault(b'revduplicates', [])
726 720 duplicates.append(self.rev(node))
@@ -1,348 +1,352 b''
1 1 # repoview.py - Filtered view of a localrepo object
2 2 #
3 3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 4 # Logilab SA <contact@logilab.fr>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import copy
12 12 import weakref
13 13
14 14 from .node import nullrev
15 15 from .pycompat import (
16 16 delattr,
17 17 getattr,
18 18 setattr,
19 19 )
20 20 from . import (
21 21 obsolete,
22 22 phases,
23 23 pycompat,
24 24 tags as tagsmod,
25 25 util,
26 26 )
27 27 from .utils import repoviewutil
28 28
29 29
30 30 def hideablerevs(repo):
31 31 """Revision candidates to be hidden
32 32
33 33 This is a standalone function to allow extensions to wrap it.
34 34
35 35 Because we use the set of immutable changesets as a fallback subset in
36 36 branchmap (see mercurial.utils.repoviewutils.subsettable), you cannot set
37 37 "public" changesets as "hideable". Doing so would break multiple code
38 38 assertions and lead to crashes."""
39 39 obsoletes = obsolete.getrevs(repo, b'obsolete')
40 40 internals = repo._phasecache.getrevset(repo, phases.localhiddenphases)
41 41 internals = frozenset(internals)
42 42 return obsoletes | internals
43 43
44 44
45 45 def pinnedrevs(repo):
46 46 """revisions blocking hidden changesets from being filtered
47 47 """
48 48
49 49 cl = repo.changelog
50 50 pinned = set()
51 51 pinned.update([par.rev() for par in repo[None].parents()])
52 52 pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()])
53 53
54 54 tags = {}
55 55 tagsmod.readlocaltags(repo.ui, repo, tags, {})
56 56 if tags:
57 57 rev, nodemap = cl.rev, cl.nodemap
58 58 pinned.update(rev(t[0]) for t in tags.values() if t[0] in nodemap)
59 59 return pinned
60 60
61 61
62 62 def _revealancestors(pfunc, hidden, revs):
63 63 """reveals contiguous chains of hidden ancestors of 'revs' by removing them
64 64 from 'hidden'
65 65
66 66 - pfunc(r): a funtion returning parent of 'r',
67 67 - hidden: the (preliminary) hidden revisions, to be updated
68 68 - revs: iterable of revnum,
69 69
70 70 (Ancestors are revealed exclusively, i.e. the elements in 'revs' are
71 71 *not* revealed)
72 72 """
73 73 stack = list(revs)
74 74 while stack:
75 75 for p in pfunc(stack.pop()):
76 76 if p != nullrev and p in hidden:
77 77 hidden.remove(p)
78 78 stack.append(p)
79 79
80 80
81 81 def computehidden(repo, visibilityexceptions=None):
82 82 """compute the set of hidden revision to filter
83 83
84 84 During most operation hidden should be filtered."""
85 85 assert not repo.changelog.filteredrevs
86 86
87 87 hidden = hideablerevs(repo)
88 88 if hidden:
89 89 hidden = set(hidden - pinnedrevs(repo))
90 90 if visibilityexceptions:
91 91 hidden -= visibilityexceptions
92 92 pfunc = repo.changelog.parentrevs
93 93 mutable = repo._phasecache.getrevset(repo, phases.mutablephases)
94 94
95 95 visible = mutable - hidden
96 96 _revealancestors(pfunc, hidden, visible)
97 97 return frozenset(hidden)
98 98
99 99
100 100 def computesecret(repo, visibilityexceptions=None):
101 101 """compute the set of revision that can never be exposed through hgweb
102 102
103 103 Changeset in the secret phase (or above) should stay unaccessible."""
104 104 assert not repo.changelog.filteredrevs
105 105 secrets = repo._phasecache.getrevset(repo, phases.remotehiddenphases)
106 106 return frozenset(secrets)
107 107
108 108
109 109 def computeunserved(repo, visibilityexceptions=None):
110 110 """compute the set of revision that should be filtered when used a server
111 111
112 112 Secret and hidden changeset should not pretend to be here."""
113 113 assert not repo.changelog.filteredrevs
114 114 # fast path in simple case to avoid impact of non optimised code
115 115 hiddens = filterrevs(repo, b'visible')
116 116 secrets = filterrevs(repo, b'served.hidden')
117 117 if secrets:
118 118 return frozenset(hiddens | secrets)
119 119 else:
120 120 return hiddens
121 121
122 122
123 123 def computemutable(repo, visibilityexceptions=None):
124 124 assert not repo.changelog.filteredrevs
125 125 # fast check to avoid revset call on huge repo
126 126 if any(repo._phasecache.phaseroots[1:]):
127 127 getphase = repo._phasecache.phase
128 128 maymutable = filterrevs(repo, b'base')
129 129 return frozenset(r for r in maymutable if getphase(repo, r))
130 130 return frozenset()
131 131
132 132
133 133 def computeimpactable(repo, visibilityexceptions=None):
134 134 """Everything impactable by mutable revision
135 135
136 136 The immutable filter still have some chance to get invalidated. This will
137 137 happen when:
138 138
139 139 - you garbage collect hidden changeset,
140 140 - public phase is moved backward,
141 141 - something is changed in the filtering (this could be fixed)
142 142
143 143 This filter out any mutable changeset and any public changeset that may be
144 144 impacted by something happening to a mutable revision.
145 145
146 146 This is achieved by filtered everything with a revision number egal or
147 147 higher than the first mutable changeset is filtered."""
148 148 assert not repo.changelog.filteredrevs
149 149 cl = repo.changelog
150 150 firstmutable = len(cl)
151 151 for roots in repo._phasecache.phaseroots[1:]:
152 152 if roots:
153 153 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
154 154 # protect from nullrev root
155 155 firstmutable = max(0, firstmutable)
156 156 return frozenset(pycompat.xrange(firstmutable, len(cl)))
157 157
158 158
159 159 # function to compute filtered set
160 160 #
161 161 # When adding a new filter you MUST update the table at:
162 162 # mercurial.utils.repoviewutil.subsettable
163 163 # Otherwise your filter will have to recompute all its branches cache
164 164 # from scratch (very slow).
165 165 filtertable = {
166 166 b'visible': computehidden,
167 167 b'visible-hidden': computehidden,
168 168 b'served.hidden': computesecret,
169 169 b'served': computeunserved,
170 170 b'immutable': computemutable,
171 171 b'base': computeimpactable,
172 172 }
173 173
174 174 _basefiltername = list(filtertable)
175 175
176 176
177 177 def extrafilter(ui):
178 178 """initialize extra filter and return its id
179 179
180 180 If extra filtering is configured, we make sure the associated filtered view
181 181 are declared and return the associated id.
182 182 """
183 183 frevs = ui.config(b'experimental', b'extra-filter-revs')
184 184 if frevs is None:
185 185 return None
186 186
187 187 fid = pycompat.sysbytes(util.DIGESTS[b'sha1'](frevs).hexdigest())[:12]
188 188
189 189 combine = lambda fname: fname + b'%' + fid
190 190
191 191 subsettable = repoviewutil.subsettable
192 192
193 193 if combine(b'base') not in filtertable:
194 194 for name in _basefiltername:
195 195
196 196 def extrafilteredrevs(repo, *args, **kwargs):
197 197 baserevs = filtertable[name](repo, *args, **kwargs)
198 198 extrarevs = frozenset(repo.revs(frevs))
199 199 return baserevs | extrarevs
200 200
201 201 filtertable[combine(name)] = extrafilteredrevs
202 202 if name in subsettable:
203 203 subsettable[combine(name)] = combine(subsettable[name])
204 204 return fid
205 205
206 206
207 207 def filterrevs(repo, filtername, visibilityexceptions=None):
208 208 """returns set of filtered revision for this filter name
209 209
210 210 visibilityexceptions is a set of revs which must are exceptions for
211 211 hidden-state and must be visible. They are dynamic and hence we should not
212 212 cache it's result"""
213 213 if filtername not in repo.filteredrevcache:
214 214 func = filtertable[filtername]
215 215 if visibilityexceptions:
216 216 return func(repo.unfiltered, visibilityexceptions)
217 217 repo.filteredrevcache[filtername] = func(repo.unfiltered())
218 218 return repo.filteredrevcache[filtername]
219 219
220 220
221 221 def wrapchangelog(unfichangelog, filteredrevs):
222 222 cl = copy.copy(unfichangelog)
223 223 cl.filteredrevs = filteredrevs
224 224
225 225 class filteredchangelog(cl.__class__):
226 pass
226 def tiprev(self):
227 """filtered version of revlog.tiprev"""
228 for i in pycompat.xrange(len(self) - 1, -2, -1):
229 if i not in self.filteredrevs:
230 return i
227 231
228 232 cl.__class__ = filteredchangelog
229 233
230 234 return cl
231 235
232 236
233 237 class repoview(object):
234 238 """Provide a read/write view of a repo through a filtered changelog
235 239
236 240 This object is used to access a filtered version of a repository without
237 241 altering the original repository object itself. We can not alter the
238 242 original object for two main reasons:
239 243 - It prevents the use of a repo with multiple filters at the same time. In
240 244 particular when multiple threads are involved.
241 245 - It makes scope of the filtering harder to control.
242 246
243 247 This object behaves very closely to the original repository. All attribute
244 248 operations are done on the original repository:
245 249 - An access to `repoview.someattr` actually returns `repo.someattr`,
246 250 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
247 251 - A deletion of `repoview.someattr` actually drops `someattr`
248 252 from `repo.__dict__`.
249 253
250 254 The only exception is the `changelog` property. It is overridden to return
251 255 a (surface) copy of `repo.changelog` with some revisions filtered. The
252 256 `filtername` attribute of the view control the revisions that need to be
253 257 filtered. (the fact the changelog is copied is an implementation detail).
254 258
255 259 Unlike attributes, this object intercepts all method calls. This means that
256 260 all methods are run on the `repoview` object with the filtered `changelog`
257 261 property. For this purpose the simple `repoview` class must be mixed with
258 262 the actual class of the repository. This ensures that the resulting
259 263 `repoview` object have the very same methods than the repo object. This
260 264 leads to the property below.
261 265
262 266 repoview.method() --> repo.__class__.method(repoview)
263 267
264 268 The inheritance has to be done dynamically because `repo` can be of any
265 269 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
266 270 """
267 271
268 272 def __init__(self, repo, filtername, visibilityexceptions=None):
269 273 object.__setattr__(self, r'_unfilteredrepo', repo)
270 274 object.__setattr__(self, r'filtername', filtername)
271 275 object.__setattr__(self, r'_clcachekey', None)
272 276 object.__setattr__(self, r'_clcache', None)
273 277 # revs which are exceptions and must not be hidden
274 278 object.__setattr__(self, r'_visibilityexceptions', visibilityexceptions)
275 279
276 280 # not a propertycache on purpose we shall implement a proper cache later
277 281 @property
278 282 def changelog(self):
279 283 """return a filtered version of the changeset
280 284
281 285 this changelog must not be used for writing"""
282 286 # some cache may be implemented later
283 287 unfi = self._unfilteredrepo
284 288 unfichangelog = unfi.changelog
285 289 # bypass call to changelog.method
286 290 unfiindex = unfichangelog.index
287 291 unfilen = len(unfiindex)
288 292 unfinode = unfiindex[unfilen - 1][7]
289 293 with util.timedcm('repo filter for %s', self.filtername):
290 294 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions)
291 295 cl = self._clcache
292 296 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
293 297 # if cl.index is not unfiindex, unfi.changelog would be
294 298 # recreated, and our clcache refers to garbage object
295 299 if cl is not None and (
296 300 cl.index is not unfiindex or newkey != self._clcachekey
297 301 ):
298 302 cl = None
299 303 # could have been made None by the previous if
300 304 if cl is None:
301 305 cl = wrapchangelog(unfichangelog, revs)
302 306 object.__setattr__(self, r'_clcache', cl)
303 307 object.__setattr__(self, r'_clcachekey', newkey)
304 308 return cl
305 309
306 310 def unfiltered(self):
307 311 """Return an unfiltered version of a repo"""
308 312 return self._unfilteredrepo
309 313
310 314 def filtered(self, name, visibilityexceptions=None):
311 315 """Return a filtered version of a repository"""
312 316 if name == self.filtername and not visibilityexceptions:
313 317 return self
314 318 return self.unfiltered().filtered(name, visibilityexceptions)
315 319
316 320 def __repr__(self):
317 321 return r'<%s:%s %r>' % (
318 322 self.__class__.__name__,
319 323 pycompat.sysstr(self.filtername),
320 324 self.unfiltered(),
321 325 )
322 326
323 327 # everything access are forwarded to the proxied repo
324 328 def __getattr__(self, attr):
325 329 return getattr(self._unfilteredrepo, attr)
326 330
327 331 def __setattr__(self, attr, value):
328 332 return setattr(self._unfilteredrepo, attr, value)
329 333
330 334 def __delattr__(self, attr):
331 335 return delattr(self._unfilteredrepo, attr)
332 336
333 337
334 338 # Python <3.4 easily leaks types via __mro__. See
335 339 # https://bugs.python.org/issue17950. We cache dynamically created types
336 340 # so they won't be leaked on every invocation of repo.filtered().
337 341 _filteredrepotypes = weakref.WeakKeyDictionary()
338 342
339 343
340 344 def newtype(base):
341 345 """Create a new type with the repoview mixin and the given base class"""
342 346 if base not in _filteredrepotypes:
343 347
344 348 class filteredrepo(repoview, base):
345 349 pass
346 350
347 351 _filteredrepotypes[base] = filteredrepo
348 352 return _filteredrepotypes[base]
General Comments 0
You need to be logged in to leave comments. Login now