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