##// END OF EJS Templates
repoview: move changelog.__contains__() override to filteredchangelog...
Martin von Zweigbergk -
r43749:c470e699 default
parent child Browse files
Show More
@@ -1,720 +1,716 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 __contains__(self, rev):
409 """filtered version of revlog.__contains__"""
410 return 0 <= rev < len(self) and rev not in self.filteredrevs
411
412 408 def __iter__(self):
413 409 """filtered version of revlog.__iter__"""
414 410 if len(self.filteredrevs) == 0:
415 411 return revlog.revlog.__iter__(self)
416 412
417 413 def filterediter():
418 414 for i in pycompat.xrange(len(self)):
419 415 if i not in self.filteredrevs:
420 416 yield i
421 417
422 418 return filterediter()
423 419
424 420 def revs(self, start=0, stop=None):
425 421 """filtered version of revlog.revs"""
426 422 for i in super(changelog, self).revs(start, stop):
427 423 if i not in self.filteredrevs:
428 424 yield i
429 425
430 426 def _checknofilteredinrevs(self, revs):
431 427 """raise the appropriate error if 'revs' contains a filtered revision
432 428
433 429 This returns a version of 'revs' to be used thereafter by the caller.
434 430 In particular, if revs is an iterator, it is converted into a set.
435 431 """
436 432 safehasattr = util.safehasattr
437 433 if safehasattr(revs, '__next__'):
438 434 # Note that inspect.isgenerator() is not true for iterators,
439 435 revs = set(revs)
440 436
441 437 filteredrevs = self.filteredrevs
442 438 if safehasattr(revs, 'first'): # smartset
443 439 offenders = revs & filteredrevs
444 440 else:
445 441 offenders = filteredrevs.intersection(revs)
446 442
447 443 for rev in offenders:
448 444 raise error.FilteredIndexError(rev)
449 445 return revs
450 446
451 447 def headrevs(self, revs=None):
452 448 if revs is None and self.filteredrevs:
453 449 try:
454 450 return self.index.headrevsfiltered(self.filteredrevs)
455 451 # AttributeError covers non-c-extension environments and
456 452 # old c extensions without filter handling.
457 453 except AttributeError:
458 454 return self._headrevs()
459 455
460 456 if self.filteredrevs:
461 457 revs = self._checknofilteredinrevs(revs)
462 458 return super(changelog, self).headrevs(revs)
463 459
464 460 def strip(self, *args, **kwargs):
465 461 # XXX make something better than assert
466 462 # We can't expect proper strip behavior if we are filtered.
467 463 assert not self.filteredrevs
468 464 super(changelog, self).strip(*args, **kwargs)
469 465
470 466 def rev(self, node):
471 467 """filtered version of revlog.rev"""
472 468 r = super(changelog, self).rev(node)
473 469 if r in self.filteredrevs:
474 470 raise error.FilteredLookupError(
475 471 hex(node), self.indexfile, _(b'filtered node')
476 472 )
477 473 return r
478 474
479 475 def node(self, rev):
480 476 """filtered version of revlog.node"""
481 477 if rev in self.filteredrevs:
482 478 raise error.FilteredIndexError(rev)
483 479 return super(changelog, self).node(rev)
484 480
485 481 def linkrev(self, rev):
486 482 """filtered version of revlog.linkrev"""
487 483 if rev in self.filteredrevs:
488 484 raise error.FilteredIndexError(rev)
489 485 return super(changelog, self).linkrev(rev)
490 486
491 487 def parentrevs(self, rev):
492 488 """filtered version of revlog.parentrevs"""
493 489 if rev in self.filteredrevs:
494 490 raise error.FilteredIndexError(rev)
495 491 return super(changelog, self).parentrevs(rev)
496 492
497 493 def flags(self, rev):
498 494 """filtered version of revlog.flags"""
499 495 if rev in self.filteredrevs:
500 496 raise error.FilteredIndexError(rev)
501 497 return super(changelog, self).flags(rev)
502 498
503 499 def delayupdate(self, tr):
504 500 b"delay visibility of index updates to other readers"
505 501
506 502 if not self._delayed:
507 503 if len(self) == 0:
508 504 self._divert = True
509 505 if self._realopener.exists(self.indexfile + b'.a'):
510 506 self._realopener.unlink(self.indexfile + b'.a')
511 507 self.opener = _divertopener(self._realopener, self.indexfile)
512 508 else:
513 509 self._delaybuf = []
514 510 self.opener = _delayopener(
515 511 self._realopener, self.indexfile, self._delaybuf
516 512 )
517 513 self._delayed = True
518 514 tr.addpending(b'cl-%i' % id(self), self._writepending)
519 515 tr.addfinalize(b'cl-%i' % id(self), self._finalize)
520 516
521 517 def _finalize(self, tr):
522 518 b"finalize index updates"
523 519 self._delayed = False
524 520 self.opener = self._realopener
525 521 # move redirected index data back into place
526 522 if self._divert:
527 523 assert not self._delaybuf
528 524 tmpname = self.indexfile + b".a"
529 525 nfile = self.opener.open(tmpname)
530 526 nfile.close()
531 527 self.opener.rename(tmpname, self.indexfile, checkambig=True)
532 528 elif self._delaybuf:
533 529 fp = self.opener(self.indexfile, b'a', checkambig=True)
534 530 fp.write(b"".join(self._delaybuf))
535 531 fp.close()
536 532 self._delaybuf = None
537 533 self._divert = False
538 534 # split when we're done
539 535 self._enforceinlinesize(tr)
540 536
541 537 def _writepending(self, tr):
542 538 b"create a file containing the unfinalized state for pretxnchangegroup"
543 539 if self._delaybuf:
544 540 # make a temporary copy of the index
545 541 fp1 = self._realopener(self.indexfile)
546 542 pendingfilename = self.indexfile + b".a"
547 543 # register as a temp file to ensure cleanup on failure
548 544 tr.registertmp(pendingfilename)
549 545 # write existing data
550 546 fp2 = self._realopener(pendingfilename, b"w")
551 547 fp2.write(fp1.read())
552 548 # add pending data
553 549 fp2.write(b"".join(self._delaybuf))
554 550 fp2.close()
555 551 # switch modes so finalize can simply rename
556 552 self._delaybuf = None
557 553 self._divert = True
558 554 self.opener = _divertopener(self._realopener, self.indexfile)
559 555
560 556 if self._divert:
561 557 return True
562 558
563 559 return False
564 560
565 561 def _enforceinlinesize(self, tr, fp=None):
566 562 if not self._delayed:
567 563 revlog.revlog._enforceinlinesize(self, tr, fp)
568 564
569 565 def read(self, node):
570 566 """Obtain data from a parsed changelog revision.
571 567
572 568 Returns a 6-tuple of:
573 569
574 570 - manifest node in binary
575 571 - author/user as a localstr
576 572 - date as a 2-tuple of (time, timezone)
577 573 - list of files
578 574 - commit message as a localstr
579 575 - dict of extra metadata
580 576
581 577 Unless you need to access all fields, consider calling
582 578 ``changelogrevision`` instead, as it is faster for partial object
583 579 access.
584 580 """
585 581 d, s = self._revisiondata(node)
586 582 c = changelogrevision(
587 583 d, s, self._copiesstorage == b'changeset-sidedata'
588 584 )
589 585 return (c.manifest, c.user, c.date, c.files, c.description, c.extra)
590 586
591 587 def changelogrevision(self, nodeorrev):
592 588 """Obtain a ``changelogrevision`` for a node or revision."""
593 589 text, sidedata = self._revisiondata(nodeorrev)
594 590 return changelogrevision(
595 591 text, sidedata, self._copiesstorage == b'changeset-sidedata'
596 592 )
597 593
598 594 def readfiles(self, node):
599 595 """
600 596 short version of read that only returns the files modified by the cset
601 597 """
602 598 text = self.revision(node)
603 599 if not text:
604 600 return []
605 601 last = text.index(b"\n\n")
606 602 l = text[:last].split(b'\n')
607 603 return l[3:]
608 604
609 605 def add(
610 606 self,
611 607 manifest,
612 608 files,
613 609 desc,
614 610 transaction,
615 611 p1,
616 612 p2,
617 613 user,
618 614 date=None,
619 615 extra=None,
620 616 p1copies=None,
621 617 p2copies=None,
622 618 filesadded=None,
623 619 filesremoved=None,
624 620 ):
625 621 # Convert to UTF-8 encoded bytestrings as the very first
626 622 # thing: calling any method on a localstr object will turn it
627 623 # into a str object and the cached UTF-8 string is thus lost.
628 624 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
629 625
630 626 user = user.strip()
631 627 # An empty username or a username with a "\n" will make the
632 628 # revision text contain two "\n\n" sequences -> corrupt
633 629 # repository since read cannot unpack the revision.
634 630 if not user:
635 631 raise error.StorageError(_(b"empty username"))
636 632 if b"\n" in user:
637 633 raise error.StorageError(
638 634 _(b"username %r contains a newline") % pycompat.bytestr(user)
639 635 )
640 636
641 637 desc = stripdesc(desc)
642 638
643 639 if date:
644 640 parseddate = b"%d %d" % dateutil.parsedate(date)
645 641 else:
646 642 parseddate = b"%d %d" % dateutil.makedate()
647 643 if extra:
648 644 branch = extra.get(b"branch")
649 645 if branch in (b"default", b""):
650 646 del extra[b"branch"]
651 647 elif branch in (b".", b"null", b"tip"):
652 648 raise error.StorageError(
653 649 _(b'the name \'%s\' is reserved') % branch
654 650 )
655 651 sortedfiles = sorted(files)
656 652 sidedata = None
657 653 if extra is not None:
658 654 for name in (
659 655 b'p1copies',
660 656 b'p2copies',
661 657 b'filesadded',
662 658 b'filesremoved',
663 659 ):
664 660 extra.pop(name, None)
665 661 if p1copies is not None:
666 662 p1copies = copies.encodecopies(sortedfiles, p1copies)
667 663 if p2copies is not None:
668 664 p2copies = copies.encodecopies(sortedfiles, p2copies)
669 665 if filesadded is not None:
670 666 filesadded = copies.encodefileindices(sortedfiles, filesadded)
671 667 if filesremoved is not None:
672 668 filesremoved = copies.encodefileindices(sortedfiles, filesremoved)
673 669 if self._copiesstorage == b'extra':
674 670 extrasentries = p1copies, p2copies, filesadded, filesremoved
675 671 if extra is None and any(x is not None for x in extrasentries):
676 672 extra = {}
677 673 if p1copies is not None:
678 674 extra[b'p1copies'] = p1copies
679 675 if p2copies is not None:
680 676 extra[b'p2copies'] = p2copies
681 677 if filesadded is not None:
682 678 extra[b'filesadded'] = filesadded
683 679 if filesremoved is not None:
684 680 extra[b'filesremoved'] = filesremoved
685 681 elif self._copiesstorage == b'changeset-sidedata':
686 682 sidedata = {}
687 683 if p1copies:
688 684 sidedata[sidedatamod.SD_P1COPIES] = p1copies
689 685 if p2copies:
690 686 sidedata[sidedatamod.SD_P2COPIES] = p2copies
691 687 if filesadded:
692 688 sidedata[sidedatamod.SD_FILESADDED] = filesadded
693 689 if filesremoved:
694 690 sidedata[sidedatamod.SD_FILESREMOVED] = filesremoved
695 691 if not sidedata:
696 692 sidedata = None
697 693
698 694 if extra:
699 695 extra = encodeextra(extra)
700 696 parseddate = b"%s %s" % (parseddate, extra)
701 697 l = [hex(manifest), user, parseddate] + sortedfiles + [b"", desc]
702 698 text = b"\n".join(l)
703 699 return self.addrevision(
704 700 text, transaction, len(self), p1, p2, sidedata=sidedata
705 701 )
706 702
707 703 def branchinfo(self, rev):
708 704 """return the branch name and open/close state of a revision
709 705
710 706 This function exists because creating a changectx object
711 707 just to access this is costly."""
712 708 extra = self.read(rev)[5]
713 709 return encoding.tolocal(extra.get(b"branch")), b'close' in extra
714 710
715 711 def _nodeduplicatecallback(self, transaction, node):
716 712 # keep track of revisions that got "re-added", eg: unbunde of know rev.
717 713 #
718 714 # We track them in a list to preserve their order from the source bundle
719 715 duplicates = transaction.changes.setdefault(b'revduplicates', [])
720 716 duplicates.append(self.rev(node))
@@ -1,352 +1,356 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 226 def tiprev(self):
227 227 """filtered version of revlog.tiprev"""
228 228 for i in pycompat.xrange(len(self) - 1, -2, -1):
229 229 if i not in self.filteredrevs:
230 230 return i
231 231
232 def __contains__(self, rev):
233 """filtered version of revlog.__contains__"""
234 return 0 <= rev < len(self) and rev not in self.filteredrevs
235
232 236 cl.__class__ = filteredchangelog
233 237
234 238 return cl
235 239
236 240
237 241 class repoview(object):
238 242 """Provide a read/write view of a repo through a filtered changelog
239 243
240 244 This object is used to access a filtered version of a repository without
241 245 altering the original repository object itself. We can not alter the
242 246 original object for two main reasons:
243 247 - It prevents the use of a repo with multiple filters at the same time. In
244 248 particular when multiple threads are involved.
245 249 - It makes scope of the filtering harder to control.
246 250
247 251 This object behaves very closely to the original repository. All attribute
248 252 operations are done on the original repository:
249 253 - An access to `repoview.someattr` actually returns `repo.someattr`,
250 254 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
251 255 - A deletion of `repoview.someattr` actually drops `someattr`
252 256 from `repo.__dict__`.
253 257
254 258 The only exception is the `changelog` property. It is overridden to return
255 259 a (surface) copy of `repo.changelog` with some revisions filtered. The
256 260 `filtername` attribute of the view control the revisions that need to be
257 261 filtered. (the fact the changelog is copied is an implementation detail).
258 262
259 263 Unlike attributes, this object intercepts all method calls. This means that
260 264 all methods are run on the `repoview` object with the filtered `changelog`
261 265 property. For this purpose the simple `repoview` class must be mixed with
262 266 the actual class of the repository. This ensures that the resulting
263 267 `repoview` object have the very same methods than the repo object. This
264 268 leads to the property below.
265 269
266 270 repoview.method() --> repo.__class__.method(repoview)
267 271
268 272 The inheritance has to be done dynamically because `repo` can be of any
269 273 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
270 274 """
271 275
272 276 def __init__(self, repo, filtername, visibilityexceptions=None):
273 277 object.__setattr__(self, r'_unfilteredrepo', repo)
274 278 object.__setattr__(self, r'filtername', filtername)
275 279 object.__setattr__(self, r'_clcachekey', None)
276 280 object.__setattr__(self, r'_clcache', None)
277 281 # revs which are exceptions and must not be hidden
278 282 object.__setattr__(self, r'_visibilityexceptions', visibilityexceptions)
279 283
280 284 # not a propertycache on purpose we shall implement a proper cache later
281 285 @property
282 286 def changelog(self):
283 287 """return a filtered version of the changeset
284 288
285 289 this changelog must not be used for writing"""
286 290 # some cache may be implemented later
287 291 unfi = self._unfilteredrepo
288 292 unfichangelog = unfi.changelog
289 293 # bypass call to changelog.method
290 294 unfiindex = unfichangelog.index
291 295 unfilen = len(unfiindex)
292 296 unfinode = unfiindex[unfilen - 1][7]
293 297 with util.timedcm('repo filter for %s', self.filtername):
294 298 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions)
295 299 cl = self._clcache
296 300 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
297 301 # if cl.index is not unfiindex, unfi.changelog would be
298 302 # recreated, and our clcache refers to garbage object
299 303 if cl is not None and (
300 304 cl.index is not unfiindex or newkey != self._clcachekey
301 305 ):
302 306 cl = None
303 307 # could have been made None by the previous if
304 308 if cl is None:
305 309 cl = wrapchangelog(unfichangelog, revs)
306 310 object.__setattr__(self, r'_clcache', cl)
307 311 object.__setattr__(self, r'_clcachekey', newkey)
308 312 return cl
309 313
310 314 def unfiltered(self):
311 315 """Return an unfiltered version of a repo"""
312 316 return self._unfilteredrepo
313 317
314 318 def filtered(self, name, visibilityexceptions=None):
315 319 """Return a filtered version of a repository"""
316 320 if name == self.filtername and not visibilityexceptions:
317 321 return self
318 322 return self.unfiltered().filtered(name, visibilityexceptions)
319 323
320 324 def __repr__(self):
321 325 return r'<%s:%s %r>' % (
322 326 self.__class__.__name__,
323 327 pycompat.sysstr(self.filtername),
324 328 self.unfiltered(),
325 329 )
326 330
327 331 # everything access are forwarded to the proxied repo
328 332 def __getattr__(self, attr):
329 333 return getattr(self._unfilteredrepo, attr)
330 334
331 335 def __setattr__(self, attr, value):
332 336 return setattr(self._unfilteredrepo, attr, value)
333 337
334 338 def __delattr__(self, attr):
335 339 return delattr(self._unfilteredrepo, attr)
336 340
337 341
338 342 # Python <3.4 easily leaks types via __mro__. See
339 343 # https://bugs.python.org/issue17950. We cache dynamically created types
340 344 # so they won't be leaked on every invocation of repo.filtered().
341 345 _filteredrepotypes = weakref.WeakKeyDictionary()
342 346
343 347
344 348 def newtype(base):
345 349 """Create a new type with the repoview mixin and the given base class"""
346 350 if base not in _filteredrepotypes:
347 351
348 352 class filteredrepo(repoview, base):
349 353 pass
350 354
351 355 _filteredrepotypes[base] = filteredrepo
352 356 return _filteredrepotypes[base]
General Comments 0
You need to be logged in to leave comments. Login now