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