##// END OF EJS Templates
phases: provide a test and accessor for non-public phase roots...
Joerg Sonnenberger -
r45674:e2d17974 default
parent child Browse files
Show More
@@ -1,897 +1,919 b''
1 1 """ Mercurial phases support code
2 2
3 3 ---
4 4
5 5 Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
6 6 Logilab SA <contact@logilab.fr>
7 7 Augie Fackler <durin42@gmail.com>
8 8
9 9 This software may be used and distributed according to the terms
10 10 of the GNU General Public License version 2 or any later version.
11 11
12 12 ---
13 13
14 14 This module implements most phase logic in mercurial.
15 15
16 16
17 17 Basic Concept
18 18 =============
19 19
20 20 A 'changeset phase' is an indicator that tells us how a changeset is
21 21 manipulated and communicated. The details of each phase is described
22 22 below, here we describe the properties they have in common.
23 23
24 24 Like bookmarks, phases are not stored in history and thus are not
25 25 permanent and leave no audit trail.
26 26
27 27 First, no changeset can be in two phases at once. Phases are ordered,
28 28 so they can be considered from lowest to highest. The default, lowest
29 29 phase is 'public' - this is the normal phase of existing changesets. A
30 30 child changeset can not be in a lower phase than its parents.
31 31
32 32 These phases share a hierarchy of traits:
33 33
34 34 immutable shared
35 35 public: X X
36 36 draft: X
37 37 secret:
38 38
39 39 Local commits are draft by default.
40 40
41 41 Phase Movement and Exchange
42 42 ===========================
43 43
44 44 Phase data is exchanged by pushkey on pull and push. Some servers have
45 45 a publish option set, we call such a server a "publishing server".
46 46 Pushing a draft changeset to a publishing server changes the phase to
47 47 public.
48 48
49 49 A small list of fact/rules define the exchange of phase:
50 50
51 51 * old client never changes server states
52 52 * pull never changes server states
53 53 * publish and old server changesets are seen as public by client
54 54 * any secret changeset seen in another repository is lowered to at
55 55 least draft
56 56
57 57 Here is the final table summing up the 49 possible use cases of phase
58 58 exchange:
59 59
60 60 server
61 61 old publish non-publish
62 62 N X N D P N D P
63 63 old client
64 64 pull
65 65 N - X/X - X/D X/P - X/D X/P
66 66 X - X/X - X/D X/P - X/D X/P
67 67 push
68 68 X X/X X/X X/P X/P X/P X/D X/D X/P
69 69 new client
70 70 pull
71 71 N - P/X - P/D P/P - D/D P/P
72 72 D - P/X - P/D P/P - D/D P/P
73 73 P - P/X - P/D P/P - P/D P/P
74 74 push
75 75 D P/X P/X P/P P/P P/P D/D D/D P/P
76 76 P P/X P/X P/P P/P P/P P/P P/P P/P
77 77
78 78 Legend:
79 79
80 80 A/B = final state on client / state on server
81 81
82 82 * N = new/not present,
83 83 * P = public,
84 84 * D = draft,
85 85 * X = not tracked (i.e., the old client or server has no internal
86 86 way of recording the phase.)
87 87
88 88 passive = only pushes
89 89
90 90
91 91 A cell here can be read like this:
92 92
93 93 "When a new client pushes a draft changeset (D) to a publishing
94 94 server where it's not present (N), it's marked public on both
95 95 sides (P/P)."
96 96
97 97 Note: old client behave as a publishing server with draft only content
98 98 - other people see it as public
99 99 - content is pushed as draft
100 100
101 101 """
102 102
103 103 from __future__ import absolute_import
104 104
105 105 import errno
106 106 import struct
107 107
108 108 from .i18n import _
109 109 from .node import (
110 110 bin,
111 111 hex,
112 112 nullid,
113 113 nullrev,
114 114 short,
115 115 wdirrev,
116 116 )
117 117 from .pycompat import (
118 118 getattr,
119 119 setattr,
120 120 )
121 121 from . import (
122 122 error,
123 123 pycompat,
124 124 smartset,
125 125 txnutil,
126 126 util,
127 127 )
128 128
129 129 _fphasesentry = struct.Struct(b'>i20s')
130 130
131 131 INTERNAL_FLAG = 64 # Phases for mercurial internal usage only
132 132 HIDEABLE_FLAG = 32 # Phases that are hideable
133 133
134 134 # record phase index
135 135 public, draft, secret = range(3)
136 136 internal = INTERNAL_FLAG | HIDEABLE_FLAG
137 137 archived = HIDEABLE_FLAG
138 138 allphases = list(range(internal + 1))
139 139 trackedphases = allphases[1:]
140 140 # record phase names
141 141 cmdphasenames = [b'public', b'draft', b'secret'] # known to `hg phase` command
142 142 phasenames = [None] * len(allphases)
143 143 phasenames[: len(cmdphasenames)] = cmdphasenames
144 144 phasenames[archived] = b'archived'
145 145 phasenames[internal] = b'internal'
146 146 # record phase property
147 147 mutablephases = tuple(allphases[1:])
148 148 remotehiddenphases = tuple(allphases[2:])
149 149 localhiddenphases = tuple(p for p in allphases if p & HIDEABLE_FLAG)
150 150
151 151
152 152 def supportinternal(repo):
153 153 """True if the internal phase can be used on a repository"""
154 154 return b'internal-phase' in repo.requirements
155 155
156 156
157 157 def _readroots(repo, phasedefaults=None):
158 158 """Read phase roots from disk
159 159
160 160 phasedefaults is a list of fn(repo, roots) callable, which are
161 161 executed if the phase roots file does not exist. When phases are
162 162 being initialized on an existing repository, this could be used to
163 163 set selected changesets phase to something else than public.
164 164
165 165 Return (roots, dirty) where dirty is true if roots differ from
166 166 what is being stored.
167 167 """
168 168 repo = repo.unfiltered()
169 169 dirty = False
170 170 roots = [set() for i in allphases]
171 171 try:
172 172 f, pending = txnutil.trypending(repo.root, repo.svfs, b'phaseroots')
173 173 try:
174 174 for line in f:
175 175 phase, nh = line.split()
176 176 roots[int(phase)].add(bin(nh))
177 177 finally:
178 178 f.close()
179 179 except IOError as inst:
180 180 if inst.errno != errno.ENOENT:
181 181 raise
182 182 if phasedefaults:
183 183 for f in phasedefaults:
184 184 roots = f(repo, roots)
185 185 dirty = True
186 186 return roots, dirty
187 187
188 188
189 189 def binaryencode(phasemapping):
190 190 """encode a 'phase -> nodes' mapping into a binary stream
191 191
192 192 Since phases are integer the mapping is actually a python list:
193 193 [[PUBLIC_HEADS], [DRAFTS_HEADS], [SECRET_HEADS]]
194 194 """
195 195 binarydata = []
196 196 for phase, nodes in enumerate(phasemapping):
197 197 for head in nodes:
198 198 binarydata.append(_fphasesentry.pack(phase, head))
199 199 return b''.join(binarydata)
200 200
201 201
202 202 def binarydecode(stream):
203 203 """decode a binary stream into a 'phase -> nodes' mapping
204 204
205 205 Since phases are integer the mapping is actually a python list."""
206 206 headsbyphase = [[] for i in allphases]
207 207 entrysize = _fphasesentry.size
208 208 while True:
209 209 entry = stream.read(entrysize)
210 210 if len(entry) < entrysize:
211 211 if entry:
212 212 raise error.Abort(_(b'bad phase-heads stream'))
213 213 break
214 214 phase, node = _fphasesentry.unpack(entry)
215 215 headsbyphase[phase].append(node)
216 216 return headsbyphase
217 217
218 218
219 219 def _sortedrange_insert(data, idx, rev, t):
220 220 merge_before = False
221 221 if idx:
222 222 r1, t1 = data[idx - 1]
223 223 merge_before = r1[-1] + 1 == rev and t1 == t
224 224 merge_after = False
225 225 if idx < len(data):
226 226 r2, t2 = data[idx]
227 227 merge_after = r2[0] == rev + 1 and t2 == t
228 228
229 229 if merge_before and merge_after:
230 230 data[idx - 1] = (pycompat.xrange(r1[0], r2[-1] + 1), t)
231 231 data.pop(idx)
232 232 elif merge_before:
233 233 data[idx - 1] = (pycompat.xrange(r1[0], rev + 1), t)
234 234 elif merge_after:
235 235 data[idx] = (pycompat.xrange(rev, r2[-1] + 1), t)
236 236 else:
237 237 data.insert(idx, (pycompat.xrange(rev, rev + 1), t))
238 238
239 239
240 240 def _sortedrange_split(data, idx, rev, t):
241 241 r1, t1 = data[idx]
242 242 if t == t1:
243 243 return
244 244 t = (t1[0], t[1])
245 245 if len(r1) == 1:
246 246 data.pop(idx)
247 247 _sortedrange_insert(data, idx, rev, t)
248 248 elif r1[0] == rev:
249 249 data[idx] = (pycompat.xrange(rev + 1, r1[-1] + 1), t1)
250 250 _sortedrange_insert(data, idx, rev, t)
251 251 elif r1[-1] == rev:
252 252 data[idx] = (pycompat.xrange(r1[0], rev), t1)
253 253 _sortedrange_insert(data, idx + 1, rev, t)
254 254 else:
255 255 data[idx : idx + 1] = [
256 256 (pycompat.xrange(r1[0], rev), t1),
257 257 (pycompat.xrange(rev, rev + 1), t),
258 258 (pycompat.xrange(rev + 1, r1[-1] + 1), t1),
259 259 ]
260 260
261 261
262 262 def _trackphasechange(data, rev, old, new):
263 263 """add a phase move to the <data> list of ranges
264 264
265 265 If data is None, nothing happens.
266 266 """
267 267 if data is None:
268 268 return
269 269
270 270 # If data is empty, create a one-revision range and done
271 271 if not data:
272 272 data.insert(0, (pycompat.xrange(rev, rev + 1), (old, new)))
273 273 return
274 274
275 275 low = 0
276 276 high = len(data)
277 277 t = (old, new)
278 278 while low < high:
279 279 mid = (low + high) // 2
280 280 revs = data[mid][0]
281 281
282 282 if rev in revs:
283 283 _sortedrange_split(data, mid, rev, t)
284 284 return
285 285
286 286 if revs[0] == rev + 1:
287 287 if mid and data[mid - 1][0][-1] == rev:
288 288 _sortedrange_split(data, mid - 1, rev, t)
289 289 else:
290 290 _sortedrange_insert(data, mid, rev, t)
291 291 return
292 292
293 293 if revs[-1] == rev - 1:
294 294 if mid + 1 < len(data) and data[mid + 1][0][0] == rev:
295 295 _sortedrange_split(data, mid + 1, rev, t)
296 296 else:
297 297 _sortedrange_insert(data, mid + 1, rev, t)
298 298 return
299 299
300 300 if revs[0] > rev:
301 301 high = mid
302 302 else:
303 303 low = mid + 1
304 304
305 305 if low == len(data):
306 306 data.append((pycompat.xrange(rev, rev + 1), t))
307 307 return
308 308
309 309 r1, t1 = data[low]
310 310 if r1[0] > rev:
311 311 data.insert(low, (pycompat.xrange(rev, rev + 1), t))
312 312 else:
313 313 data.insert(low + 1, (pycompat.xrange(rev, rev + 1), t))
314 314
315 315
316 316 class phasecache(object):
317 317 def __init__(self, repo, phasedefaults, _load=True):
318 318 if _load:
319 319 # Cheap trick to allow shallow-copy without copy module
320 320 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
321 321 self._loadedrevslen = 0
322 322 self._phasesets = None
323 323 self.filterunknown(repo)
324 324 self.opener = repo.svfs
325 325
326 def hasnonpublicphases(self, repo):
327 """detect if there are revisions with non-public phase"""
328 repo = repo.unfiltered()
329 cl = repo.changelog
330 if len(cl) >= self._loadedrevslen:
331 self.invalidate()
332 self.loadphaserevs(repo)
333 return any(self.phaseroots[1:])
334
335 def nonpublicphaseroots(self, repo):
336 """returns the roots of all non-public phases
337
338 The roots are not minimized, so if the secret revisions are
339 descendants of draft revisions, their roots will still be present.
340 """
341 repo = repo.unfiltered()
342 cl = repo.changelog
343 if len(cl) >= self._loadedrevslen:
344 self.invalidate()
345 self.loadphaserevs(repo)
346 return set().union(*[roots for roots in self.phaseroots[1:] if roots])
347
326 348 def getrevset(self, repo, phases, subset=None):
327 349 """return a smartset for the given phases"""
328 350 self.loadphaserevs(repo) # ensure phase's sets are loaded
329 351 phases = set(phases)
330 352 publicphase = public in phases
331 353
332 354 if publicphase:
333 355 # In this case, phases keeps all the *other* phases.
334 356 phases = set(allphases).difference(phases)
335 357 if not phases:
336 358 return smartset.fullreposet(repo)
337 359
338 360 # fast path: _phasesets contains the interesting sets,
339 361 # might only need a union and post-filtering.
340 362 revsneedscopy = False
341 363 if len(phases) == 1:
342 364 [p] = phases
343 365 revs = self._phasesets[p]
344 366 revsneedscopy = True # Don't modify _phasesets
345 367 else:
346 368 # revs has the revisions in all *other* phases.
347 369 revs = set.union(*[self._phasesets[p] for p in phases])
348 370
349 371 def _addwdir(wdirsubset, wdirrevs):
350 372 if wdirrev in wdirsubset and repo[None].phase() in phases:
351 373 if revsneedscopy:
352 374 wdirrevs = wdirrevs.copy()
353 375 # The working dir would never be in the # cache, but it was in
354 376 # the subset being filtered for its phase (or filtered out,
355 377 # depending on publicphase), so add it to the output to be
356 378 # included (or filtered out).
357 379 wdirrevs.add(wdirrev)
358 380 return wdirrevs
359 381
360 382 if not publicphase:
361 383 if repo.changelog.filteredrevs:
362 384 revs = revs - repo.changelog.filteredrevs
363 385
364 386 if subset is None:
365 387 return smartset.baseset(revs)
366 388 else:
367 389 revs = _addwdir(subset, revs)
368 390 return subset & smartset.baseset(revs)
369 391 else:
370 392 if subset is None:
371 393 subset = smartset.fullreposet(repo)
372 394
373 395 revs = _addwdir(subset, revs)
374 396
375 397 if not revs:
376 398 return subset
377 399 return subset.filter(lambda r: r not in revs)
378 400
379 401 def copy(self):
380 402 # Shallow copy meant to ensure isolation in
381 403 # advance/retractboundary(), nothing more.
382 404 ph = self.__class__(None, None, _load=False)
383 405 ph.phaseroots = self.phaseroots[:]
384 406 ph.dirty = self.dirty
385 407 ph.opener = self.opener
386 408 ph._loadedrevslen = self._loadedrevslen
387 409 ph._phasesets = self._phasesets
388 410 return ph
389 411
390 412 def replace(self, phcache):
391 413 """replace all values in 'self' with content of phcache"""
392 414 for a in (
393 415 b'phaseroots',
394 416 b'dirty',
395 417 b'opener',
396 418 b'_loadedrevslen',
397 419 b'_phasesets',
398 420 ):
399 421 setattr(self, a, getattr(phcache, a))
400 422
401 423 def _getphaserevsnative(self, repo):
402 424 repo = repo.unfiltered()
403 425 nativeroots = []
404 426 for phase in trackedphases:
405 427 nativeroots.append(
406 428 pycompat.maplist(repo.changelog.rev, self.phaseroots[phase])
407 429 )
408 430 return repo.changelog.computephases(nativeroots)
409 431
410 432 def _computephaserevspure(self, repo):
411 433 repo = repo.unfiltered()
412 434 cl = repo.changelog
413 435 self._phasesets = [set() for phase in allphases]
414 436 lowerroots = set()
415 437 for phase in reversed(trackedphases):
416 438 roots = pycompat.maplist(cl.rev, self.phaseroots[phase])
417 439 if roots:
418 440 ps = set(cl.descendants(roots))
419 441 for root in roots:
420 442 ps.add(root)
421 443 ps.difference_update(lowerroots)
422 444 lowerroots.update(ps)
423 445 self._phasesets[phase] = ps
424 446 self._loadedrevslen = len(cl)
425 447
426 448 def loadphaserevs(self, repo):
427 449 """ensure phase information is loaded in the object"""
428 450 if self._phasesets is None:
429 451 try:
430 452 res = self._getphaserevsnative(repo)
431 453 self._loadedrevslen, self._phasesets = res
432 454 except AttributeError:
433 455 self._computephaserevspure(repo)
434 456
435 457 def invalidate(self):
436 458 self._loadedrevslen = 0
437 459 self._phasesets = None
438 460
439 461 def phase(self, repo, rev):
440 462 # We need a repo argument here to be able to build _phasesets
441 463 # if necessary. The repository instance is not stored in
442 464 # phasecache to avoid reference cycles. The changelog instance
443 465 # is not stored because it is a filecache() property and can
444 466 # be replaced without us being notified.
445 467 if rev == nullrev:
446 468 return public
447 469 if rev < nullrev:
448 470 raise ValueError(_(b'cannot lookup negative revision'))
449 471 if rev >= self._loadedrevslen:
450 472 self.invalidate()
451 473 self.loadphaserevs(repo)
452 474 for phase in trackedphases:
453 475 if rev in self._phasesets[phase]:
454 476 return phase
455 477 return public
456 478
457 479 def write(self):
458 480 if not self.dirty:
459 481 return
460 482 f = self.opener(b'phaseroots', b'w', atomictemp=True, checkambig=True)
461 483 try:
462 484 self._write(f)
463 485 finally:
464 486 f.close()
465 487
466 488 def _write(self, fp):
467 489 for phase, roots in enumerate(self.phaseroots):
468 490 for h in sorted(roots):
469 491 fp.write(b'%i %s\n' % (phase, hex(h)))
470 492 self.dirty = False
471 493
472 494 def _updateroots(self, phase, newroots, tr):
473 495 self.phaseroots[phase] = newroots
474 496 self.invalidate()
475 497 self.dirty = True
476 498
477 499 tr.addfilegenerator(b'phase', (b'phaseroots',), self._write)
478 500 tr.hookargs[b'phases_moved'] = b'1'
479 501
480 502 def registernew(self, repo, tr, targetphase, nodes):
481 503 repo = repo.unfiltered()
482 504 self._retractboundary(repo, tr, targetphase, nodes)
483 505 if tr is not None and b'phases' in tr.changes:
484 506 phasetracking = tr.changes[b'phases']
485 507 torev = repo.changelog.rev
486 508 phase = self.phase
487 509 revs = [torev(node) for node in nodes]
488 510 revs.sort()
489 511 for rev in revs:
490 512 revphase = phase(repo, rev)
491 513 _trackphasechange(phasetracking, rev, None, revphase)
492 514 repo.invalidatevolatilesets()
493 515
494 516 def advanceboundary(self, repo, tr, targetphase, nodes, dryrun=None):
495 517 """Set all 'nodes' to phase 'targetphase'
496 518
497 519 Nodes with a phase lower than 'targetphase' are not affected.
498 520
499 521 If dryrun is True, no actions will be performed
500 522
501 523 Returns a set of revs whose phase is changed or should be changed
502 524 """
503 525 # Be careful to preserve shallow-copied values: do not update
504 526 # phaseroots values, replace them.
505 527 if tr is None:
506 528 phasetracking = None
507 529 else:
508 530 phasetracking = tr.changes.get(b'phases')
509 531
510 532 repo = repo.unfiltered()
511 533
512 534 changes = set() # set of revisions to be changed
513 535 delroots = [] # set of root deleted by this path
514 536 for phase in pycompat.xrange(targetphase + 1, len(allphases)):
515 537 # filter nodes that are not in a compatible phase already
516 538 nodes = [
517 539 n for n in nodes if self.phase(repo, repo[n].rev()) >= phase
518 540 ]
519 541 if not nodes:
520 542 break # no roots to move anymore
521 543
522 544 olds = self.phaseroots[phase]
523 545
524 546 affected = repo.revs(b'%ln::%ln', olds, nodes)
525 547 changes.update(affected)
526 548 if dryrun:
527 549 continue
528 550 for r in affected:
529 551 _trackphasechange(
530 552 phasetracking, r, self.phase(repo, r), targetphase
531 553 )
532 554
533 555 roots = {
534 556 ctx.node()
535 557 for ctx in repo.set(b'roots((%ln::) - %ld)', olds, affected)
536 558 }
537 559 if olds != roots:
538 560 self._updateroots(phase, roots, tr)
539 561 # some roots may need to be declared for lower phases
540 562 delroots.extend(olds - roots)
541 563 if not dryrun:
542 564 # declare deleted root in the target phase
543 565 if targetphase != 0:
544 566 self._retractboundary(repo, tr, targetphase, delroots)
545 567 repo.invalidatevolatilesets()
546 568 return changes
547 569
548 570 def retractboundary(self, repo, tr, targetphase, nodes):
549 571 oldroots = self.phaseroots[: targetphase + 1]
550 572 if tr is None:
551 573 phasetracking = None
552 574 else:
553 575 phasetracking = tr.changes.get(b'phases')
554 576 repo = repo.unfiltered()
555 577 if (
556 578 self._retractboundary(repo, tr, targetphase, nodes)
557 579 and phasetracking is not None
558 580 ):
559 581
560 582 # find the affected revisions
561 583 new = self.phaseroots[targetphase]
562 584 old = oldroots[targetphase]
563 585 affected = set(repo.revs(b'(%ln::) - (%ln::)', new, old))
564 586
565 587 # find the phase of the affected revision
566 588 for phase in pycompat.xrange(targetphase, -1, -1):
567 589 if phase:
568 590 roots = oldroots[phase]
569 591 revs = set(repo.revs(b'%ln::%ld', roots, affected))
570 592 affected -= revs
571 593 else: # public phase
572 594 revs = affected
573 595 for r in sorted(revs):
574 596 _trackphasechange(phasetracking, r, phase, targetphase)
575 597 repo.invalidatevolatilesets()
576 598
577 599 def _retractboundary(self, repo, tr, targetphase, nodes):
578 600 # Be careful to preserve shallow-copied values: do not update
579 601 # phaseroots values, replace them.
580 602 if targetphase in (archived, internal) and not supportinternal(repo):
581 603 name = phasenames[targetphase]
582 604 msg = b'this repository does not support the %s phase' % name
583 605 raise error.ProgrammingError(msg)
584 606
585 607 repo = repo.unfiltered()
586 608 torev = repo.changelog.rev
587 609 tonode = repo.changelog.node
588 610 currentroots = {torev(node) for node in self.phaseroots[targetphase]}
589 611 finalroots = oldroots = set(currentroots)
590 612 newroots = [torev(node) for node in nodes]
591 613 newroots = [
592 614 rev for rev in newroots if self.phase(repo, rev) < targetphase
593 615 ]
594 616
595 617 if newroots:
596 618 if nullrev in newroots:
597 619 raise error.Abort(_(b'cannot change null revision phase'))
598 620 currentroots.update(newroots)
599 621
600 622 # Only compute new roots for revs above the roots that are being
601 623 # retracted.
602 624 minnewroot = min(newroots)
603 625 aboveroots = [rev for rev in currentroots if rev >= minnewroot]
604 626 updatedroots = repo.revs(b'roots(%ld::)', aboveroots)
605 627
606 628 finalroots = {rev for rev in currentroots if rev < minnewroot}
607 629 finalroots.update(updatedroots)
608 630 if finalroots != oldroots:
609 631 self._updateroots(
610 632 targetphase, {tonode(rev) for rev in finalroots}, tr
611 633 )
612 634 return True
613 635 return False
614 636
615 637 def filterunknown(self, repo):
616 638 """remove unknown nodes from the phase boundary
617 639
618 640 Nothing is lost as unknown nodes only hold data for their descendants.
619 641 """
620 642 filtered = False
621 643 has_node = repo.changelog.index.has_node # to filter unknown nodes
622 644 for phase, nodes in enumerate(self.phaseroots):
623 645 missing = sorted(node for node in nodes if not has_node(node))
624 646 if missing:
625 647 for mnode in missing:
626 648 repo.ui.debug(
627 649 b'removing unknown node %s from %i-phase boundary\n'
628 650 % (short(mnode), phase)
629 651 )
630 652 nodes.symmetric_difference_update(missing)
631 653 filtered = True
632 654 if filtered:
633 655 self.dirty = True
634 656 # filterunknown is called by repo.destroyed, we may have no changes in
635 657 # root but _phasesets contents is certainly invalid (or at least we
636 658 # have not proper way to check that). related to issue 3858.
637 659 #
638 660 # The other caller is __init__ that have no _phasesets initialized
639 661 # anyway. If this change we should consider adding a dedicated
640 662 # "destroyed" function to phasecache or a proper cache key mechanism
641 663 # (see branchmap one)
642 664 self.invalidate()
643 665
644 666
645 667 def advanceboundary(repo, tr, targetphase, nodes, dryrun=None):
646 668 """Add nodes to a phase changing other nodes phases if necessary.
647 669
648 670 This function move boundary *forward* this means that all nodes
649 671 are set in the target phase or kept in a *lower* phase.
650 672
651 673 Simplify boundary to contains phase roots only.
652 674
653 675 If dryrun is True, no actions will be performed
654 676
655 677 Returns a set of revs whose phase is changed or should be changed
656 678 """
657 679 phcache = repo._phasecache.copy()
658 680 changes = phcache.advanceboundary(
659 681 repo, tr, targetphase, nodes, dryrun=dryrun
660 682 )
661 683 if not dryrun:
662 684 repo._phasecache.replace(phcache)
663 685 return changes
664 686
665 687
666 688 def retractboundary(repo, tr, targetphase, nodes):
667 689 """Set nodes back to a phase changing other nodes phases if
668 690 necessary.
669 691
670 692 This function move boundary *backward* this means that all nodes
671 693 are set in the target phase or kept in a *higher* phase.
672 694
673 695 Simplify boundary to contains phase roots only."""
674 696 phcache = repo._phasecache.copy()
675 697 phcache.retractboundary(repo, tr, targetphase, nodes)
676 698 repo._phasecache.replace(phcache)
677 699
678 700
679 701 def registernew(repo, tr, targetphase, nodes):
680 702 """register a new revision and its phase
681 703
682 704 Code adding revisions to the repository should use this function to
683 705 set new changeset in their target phase (or higher).
684 706 """
685 707 phcache = repo._phasecache.copy()
686 708 phcache.registernew(repo, tr, targetphase, nodes)
687 709 repo._phasecache.replace(phcache)
688 710
689 711
690 712 def listphases(repo):
691 713 """List phases root for serialization over pushkey"""
692 714 # Use ordered dictionary so behavior is deterministic.
693 715 keys = util.sortdict()
694 716 value = b'%i' % draft
695 717 cl = repo.unfiltered().changelog
696 718 for root in repo._phasecache.phaseroots[draft]:
697 719 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
698 720 keys[hex(root)] = value
699 721
700 722 if repo.publishing():
701 723 # Add an extra data to let remote know we are a publishing
702 724 # repo. Publishing repo can't just pretend they are old repo.
703 725 # When pushing to a publishing repo, the client still need to
704 726 # push phase boundary
705 727 #
706 728 # Push do not only push changeset. It also push phase data.
707 729 # New phase data may apply to common changeset which won't be
708 730 # push (as they are common). Here is a very simple example:
709 731 #
710 732 # 1) repo A push changeset X as draft to repo B
711 733 # 2) repo B make changeset X public
712 734 # 3) repo B push to repo A. X is not pushed but the data that
713 735 # X as now public should
714 736 #
715 737 # The server can't handle it on it's own as it has no idea of
716 738 # client phase data.
717 739 keys[b'publishing'] = b'True'
718 740 return keys
719 741
720 742
721 743 def pushphase(repo, nhex, oldphasestr, newphasestr):
722 744 """List phases root for serialization over pushkey"""
723 745 repo = repo.unfiltered()
724 746 with repo.lock():
725 747 currentphase = repo[nhex].phase()
726 748 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
727 749 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
728 750 if currentphase == oldphase and newphase < oldphase:
729 751 with repo.transaction(b'pushkey-phase') as tr:
730 752 advanceboundary(repo, tr, newphase, [bin(nhex)])
731 753 return True
732 754 elif currentphase == newphase:
733 755 # raced, but got correct result
734 756 return True
735 757 else:
736 758 return False
737 759
738 760
739 761 def subsetphaseheads(repo, subset):
740 762 """Finds the phase heads for a subset of a history
741 763
742 764 Returns a list indexed by phase number where each item is a list of phase
743 765 head nodes.
744 766 """
745 767 cl = repo.changelog
746 768
747 769 headsbyphase = [[] for i in allphases]
748 770 # No need to keep track of secret phase; any heads in the subset that
749 771 # are not mentioned are implicitly secret.
750 772 for phase in allphases[:secret]:
751 773 revset = b"heads(%%ln & %s())" % phasenames[phase]
752 774 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
753 775 return headsbyphase
754 776
755 777
756 778 def updatephases(repo, trgetter, headsbyphase):
757 779 """Updates the repo with the given phase heads"""
758 780 # Now advance phase boundaries of all but secret phase
759 781 #
760 782 # run the update (and fetch transaction) only if there are actually things
761 783 # to update. This avoid creating empty transaction during no-op operation.
762 784
763 785 for phase in allphases[:-1]:
764 786 revset = b'%ln - _phase(%s)'
765 787 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
766 788 if heads:
767 789 advanceboundary(repo, trgetter(), phase, heads)
768 790
769 791
770 792 def analyzeremotephases(repo, subset, roots):
771 793 """Compute phases heads and root in a subset of node from root dict
772 794
773 795 * subset is heads of the subset
774 796 * roots is {<nodeid> => phase} mapping. key and value are string.
775 797
776 798 Accept unknown element input
777 799 """
778 800 repo = repo.unfiltered()
779 801 # build list from dictionary
780 802 draftroots = []
781 803 has_node = repo.changelog.index.has_node # to filter unknown nodes
782 804 for nhex, phase in pycompat.iteritems(roots):
783 805 if nhex == b'publishing': # ignore data related to publish option
784 806 continue
785 807 node = bin(nhex)
786 808 phase = int(phase)
787 809 if phase == public:
788 810 if node != nullid:
789 811 repo.ui.warn(
790 812 _(
791 813 b'ignoring inconsistent public root'
792 814 b' from remote: %s\n'
793 815 )
794 816 % nhex
795 817 )
796 818 elif phase == draft:
797 819 if has_node(node):
798 820 draftroots.append(node)
799 821 else:
800 822 repo.ui.warn(
801 823 _(b'ignoring unexpected root from remote: %i %s\n')
802 824 % (phase, nhex)
803 825 )
804 826 # compute heads
805 827 publicheads = newheads(repo, subset, draftroots)
806 828 return publicheads, draftroots
807 829
808 830
809 831 class remotephasessummary(object):
810 832 """summarize phase information on the remote side
811 833
812 834 :publishing: True is the remote is publishing
813 835 :publicheads: list of remote public phase heads (nodes)
814 836 :draftheads: list of remote draft phase heads (nodes)
815 837 :draftroots: list of remote draft phase root (nodes)
816 838 """
817 839
818 840 def __init__(self, repo, remotesubset, remoteroots):
819 841 unfi = repo.unfiltered()
820 842 self._allremoteroots = remoteroots
821 843
822 844 self.publishing = remoteroots.get(b'publishing', False)
823 845
824 846 ana = analyzeremotephases(repo, remotesubset, remoteroots)
825 847 self.publicheads, self.draftroots = ana
826 848 # Get the list of all "heads" revs draft on remote
827 849 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
828 850 self.draftheads = [c.node() for c in dheads]
829 851
830 852
831 853 def newheads(repo, heads, roots):
832 854 """compute new head of a subset minus another
833 855
834 856 * `heads`: define the first subset
835 857 * `roots`: define the second we subtract from the first"""
836 858 # prevent an import cycle
837 859 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
838 860 from . import dagop
839 861
840 862 repo = repo.unfiltered()
841 863 cl = repo.changelog
842 864 rev = cl.index.get_rev
843 865 if not roots:
844 866 return heads
845 867 if not heads or heads == [nullid]:
846 868 return []
847 869 # The logic operated on revisions, convert arguments early for convenience
848 870 new_heads = {rev(n) for n in heads if n != nullid}
849 871 roots = [rev(n) for n in roots]
850 872 # compute the area we need to remove
851 873 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
852 874 # heads in the area are no longer heads
853 875 new_heads.difference_update(affected_zone)
854 876 # revisions in the area have children outside of it,
855 877 # They might be new heads
856 878 candidates = repo.revs(
857 879 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
858 880 )
859 881 candidates -= affected_zone
860 882 if new_heads or candidates:
861 883 # remove candidate that are ancestors of other heads
862 884 new_heads.update(candidates)
863 885 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
864 886 pruned = dagop.reachableroots(repo, candidates, prunestart)
865 887 new_heads.difference_update(pruned)
866 888
867 889 return pycompat.maplist(cl.node, sorted(new_heads))
868 890
869 891
870 892 def newcommitphase(ui):
871 893 """helper to get the target phase of new commit
872 894
873 895 Handle all possible values for the phases.new-commit options.
874 896
875 897 """
876 898 v = ui.config(b'phases', b'new-commit')
877 899 try:
878 900 return phasenames.index(v)
879 901 except ValueError:
880 902 try:
881 903 return int(v)
882 904 except ValueError:
883 905 msg = _(b"phases.new-commit: not a valid phase name ('%s')")
884 906 raise error.ConfigError(msg % v)
885 907
886 908
887 909 def hassecret(repo):
888 910 """utility function that check if a repo have any secret changeset."""
889 911 return bool(repo._phasecache.phaseroots[secret])
890 912
891 913
892 914 def preparehookargs(node, old, new):
893 915 if old is None:
894 916 old = b''
895 917 else:
896 918 old = phasenames[old]
897 919 return {b'node': node, b'oldphase': old, b'phase': phasenames[new]}
@@ -1,471 +1,471 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 .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 tags as tagsmod,
30 30 util,
31 31 )
32 32 from .utils import repoviewutil
33 33
34 34
35 35 def hideablerevs(repo):
36 36 """Revision candidates to be hidden
37 37
38 38 This is a standalone function to allow extensions to wrap it.
39 39
40 40 Because we use the set of immutable changesets as a fallback subset in
41 41 branchmap (see mercurial.utils.repoviewutils.subsettable), you cannot set
42 42 "public" changesets as "hideable". Doing so would break multiple code
43 43 assertions and lead to crashes."""
44 44 obsoletes = obsolete.getrevs(repo, b'obsolete')
45 45 internals = repo._phasecache.getrevset(repo, phases.localhiddenphases)
46 46 internals = frozenset(internals)
47 47 return obsoletes | internals
48 48
49 49
50 50 def pinnedrevs(repo):
51 51 """revisions blocking hidden changesets from being filtered
52 52 """
53 53
54 54 cl = repo.changelog
55 55 pinned = set()
56 56 pinned.update([par.rev() for par in repo[None].parents()])
57 57 pinned.update([cl.rev(bm) for bm in repo._bookmarks.values()])
58 58
59 59 tags = {}
60 60 tagsmod.readlocaltags(repo.ui, repo, tags, {})
61 61 if tags:
62 62 rev = cl.index.get_rev
63 63 pinned.update(rev(t[0]) for t in tags.values())
64 64 pinned.discard(None)
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 if any(repo._phasecache.phaseroots[1:]):
132 if repo._phasecache.hasnonpublicphases(repo):
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 for roots in repo._phasecache.phaseroots[1:]:
157 roots = repo._phasecache.nonpublicphaseroots(repo)
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 # set of filter level that will include the working copy parent no matter what.
181 181 filter_has_wc = {b'visible', b'visible-hidden'}
182 182
183 183 _basefiltername = list(filtertable)
184 184
185 185
186 186 def extrafilter(ui):
187 187 """initialize extra filter and return its id
188 188
189 189 If extra filtering is configured, we make sure the associated filtered view
190 190 are declared and return the associated id.
191 191 """
192 192 frevs = ui.config(b'experimental', b'extra-filter-revs')
193 193 if frevs is None:
194 194 return None
195 195
196 196 fid = pycompat.sysbytes(util.DIGESTS[b'sha1'](frevs).hexdigest())[:12]
197 197
198 198 combine = lambda fname: fname + b'%' + fid
199 199
200 200 subsettable = repoviewutil.subsettable
201 201
202 202 if combine(b'base') not in filtertable:
203 203 for name in _basefiltername:
204 204
205 205 def extrafilteredrevs(repo, *args, **kwargs):
206 206 baserevs = filtertable[name](repo, *args, **kwargs)
207 207 extrarevs = frozenset(repo.revs(frevs))
208 208 return baserevs | extrarevs
209 209
210 210 filtertable[combine(name)] = extrafilteredrevs
211 211 if name in subsettable:
212 212 subsettable[combine(name)] = combine(subsettable[name])
213 213 return fid
214 214
215 215
216 216 def filterrevs(repo, filtername, visibilityexceptions=None):
217 217 """returns set of filtered revision for this filter name
218 218
219 219 visibilityexceptions is a set of revs which must are exceptions for
220 220 hidden-state and must be visible. They are dynamic and hence we should not
221 221 cache it's result"""
222 222 if filtername not in repo.filteredrevcache:
223 223 if repo.ui.configbool(b'devel', b'debug.repo-filters'):
224 224 msg = b'computing revision filter for "%s"'
225 225 msg %= filtername
226 226 if repo.ui.tracebackflag and repo.ui.debugflag:
227 227 # XXX use ui.write_err
228 228 util.debugstacktrace(
229 229 msg,
230 230 f=repo.ui._fout,
231 231 otherf=repo.ui._ferr,
232 232 prefix=b'debug.filters: ',
233 233 )
234 234 else:
235 235 repo.ui.debug(b'debug.filters: %s\n' % msg)
236 236 func = filtertable[filtername]
237 237 if visibilityexceptions:
238 238 return func(repo.unfiltered, visibilityexceptions)
239 239 repo.filteredrevcache[filtername] = func(repo.unfiltered())
240 240 return repo.filteredrevcache[filtername]
241 241
242 242
243 243 def wrapchangelog(unfichangelog, filteredrevs):
244 244 cl = copy.copy(unfichangelog)
245 245 cl.filteredrevs = filteredrevs
246 246
247 247 class filteredchangelog(filteredchangelogmixin, cl.__class__):
248 248 pass
249 249
250 250 cl.__class__ = filteredchangelog
251 251
252 252 return cl
253 253
254 254
255 255 class filteredchangelogmixin(object):
256 256 def tiprev(self):
257 257 """filtered version of revlog.tiprev"""
258 258 for i in pycompat.xrange(len(self) - 1, -2, -1):
259 259 if i not in self.filteredrevs:
260 260 return i
261 261
262 262 def __contains__(self, rev):
263 263 """filtered version of revlog.__contains__"""
264 264 return 0 <= rev < len(self) and rev not in self.filteredrevs
265 265
266 266 def __iter__(self):
267 267 """filtered version of revlog.__iter__"""
268 268
269 269 def filterediter():
270 270 for i in pycompat.xrange(len(self)):
271 271 if i not in self.filteredrevs:
272 272 yield i
273 273
274 274 return filterediter()
275 275
276 276 def revs(self, start=0, stop=None):
277 277 """filtered version of revlog.revs"""
278 278 for i in super(filteredchangelogmixin, self).revs(start, stop):
279 279 if i not in self.filteredrevs:
280 280 yield i
281 281
282 282 def _checknofilteredinrevs(self, revs):
283 283 """raise the appropriate error if 'revs' contains a filtered revision
284 284
285 285 This returns a version of 'revs' to be used thereafter by the caller.
286 286 In particular, if revs is an iterator, it is converted into a set.
287 287 """
288 288 safehasattr = util.safehasattr
289 289 if safehasattr(revs, '__next__'):
290 290 # Note that inspect.isgenerator() is not true for iterators,
291 291 revs = set(revs)
292 292
293 293 filteredrevs = self.filteredrevs
294 294 if safehasattr(revs, 'first'): # smartset
295 295 offenders = revs & filteredrevs
296 296 else:
297 297 offenders = filteredrevs.intersection(revs)
298 298
299 299 for rev in offenders:
300 300 raise error.FilteredIndexError(rev)
301 301 return revs
302 302
303 303 def headrevs(self, revs=None):
304 304 if revs is None:
305 305 try:
306 306 return self.index.headrevsfiltered(self.filteredrevs)
307 307 # AttributeError covers non-c-extension environments and
308 308 # old c extensions without filter handling.
309 309 except AttributeError:
310 310 return self._headrevs()
311 311
312 312 revs = self._checknofilteredinrevs(revs)
313 313 return super(filteredchangelogmixin, self).headrevs(revs)
314 314
315 315 def strip(self, *args, **kwargs):
316 316 # XXX make something better than assert
317 317 # We can't expect proper strip behavior if we are filtered.
318 318 assert not self.filteredrevs
319 319 super(filteredchangelogmixin, self).strip(*args, **kwargs)
320 320
321 321 def rev(self, node):
322 322 """filtered version of revlog.rev"""
323 323 r = super(filteredchangelogmixin, self).rev(node)
324 324 if r in self.filteredrevs:
325 325 raise error.FilteredLookupError(
326 326 hex(node), self.indexfile, _(b'filtered node')
327 327 )
328 328 return r
329 329
330 330 def node(self, rev):
331 331 """filtered version of revlog.node"""
332 332 if rev in self.filteredrevs:
333 333 raise error.FilteredIndexError(rev)
334 334 return super(filteredchangelogmixin, self).node(rev)
335 335
336 336 def linkrev(self, rev):
337 337 """filtered version of revlog.linkrev"""
338 338 if rev in self.filteredrevs:
339 339 raise error.FilteredIndexError(rev)
340 340 return super(filteredchangelogmixin, self).linkrev(rev)
341 341
342 342 def parentrevs(self, rev):
343 343 """filtered version of revlog.parentrevs"""
344 344 if rev in self.filteredrevs:
345 345 raise error.FilteredIndexError(rev)
346 346 return super(filteredchangelogmixin, self).parentrevs(rev)
347 347
348 348 def flags(self, rev):
349 349 """filtered version of revlog.flags"""
350 350 if rev in self.filteredrevs:
351 351 raise error.FilteredIndexError(rev)
352 352 return super(filteredchangelogmixin, self).flags(rev)
353 353
354 354
355 355 class repoview(object):
356 356 """Provide a read/write view of a repo through a filtered changelog
357 357
358 358 This object is used to access a filtered version of a repository without
359 359 altering the original repository object itself. We can not alter the
360 360 original object for two main reasons:
361 361 - It prevents the use of a repo with multiple filters at the same time. In
362 362 particular when multiple threads are involved.
363 363 - It makes scope of the filtering harder to control.
364 364
365 365 This object behaves very closely to the original repository. All attribute
366 366 operations are done on the original repository:
367 367 - An access to `repoview.someattr` actually returns `repo.someattr`,
368 368 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
369 369 - A deletion of `repoview.someattr` actually drops `someattr`
370 370 from `repo.__dict__`.
371 371
372 372 The only exception is the `changelog` property. It is overridden to return
373 373 a (surface) copy of `repo.changelog` with some revisions filtered. The
374 374 `filtername` attribute of the view control the revisions that need to be
375 375 filtered. (the fact the changelog is copied is an implementation detail).
376 376
377 377 Unlike attributes, this object intercepts all method calls. This means that
378 378 all methods are run on the `repoview` object with the filtered `changelog`
379 379 property. For this purpose the simple `repoview` class must be mixed with
380 380 the actual class of the repository. This ensures that the resulting
381 381 `repoview` object have the very same methods than the repo object. This
382 382 leads to the property below.
383 383
384 384 repoview.method() --> repo.__class__.method(repoview)
385 385
386 386 The inheritance has to be done dynamically because `repo` can be of any
387 387 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
388 388 """
389 389
390 390 def __init__(self, repo, filtername, visibilityexceptions=None):
391 391 object.__setattr__(self, '_unfilteredrepo', repo)
392 392 object.__setattr__(self, 'filtername', filtername)
393 393 object.__setattr__(self, '_clcachekey', None)
394 394 object.__setattr__(self, '_clcache', None)
395 395 # revs which are exceptions and must not be hidden
396 396 object.__setattr__(self, '_visibilityexceptions', visibilityexceptions)
397 397
398 398 # not a propertycache on purpose we shall implement a proper cache later
399 399 @property
400 400 def changelog(self):
401 401 """return a filtered version of the changeset
402 402
403 403 this changelog must not be used for writing"""
404 404 # some cache may be implemented later
405 405 unfi = self._unfilteredrepo
406 406 unfichangelog = unfi.changelog
407 407 # bypass call to changelog.method
408 408 unfiindex = unfichangelog.index
409 409 unfilen = len(unfiindex)
410 410 unfinode = unfiindex[unfilen - 1][7]
411 411 with util.timedcm('repo filter for %s', self.filtername):
412 412 revs = filterrevs(unfi, self.filtername, self._visibilityexceptions)
413 413 cl = self._clcache
414 414 newkey = (unfilen, unfinode, hash(revs), unfichangelog._delayed)
415 415 # if cl.index is not unfiindex, unfi.changelog would be
416 416 # recreated, and our clcache refers to garbage object
417 417 if cl is not None and (
418 418 cl.index is not unfiindex or newkey != self._clcachekey
419 419 ):
420 420 cl = None
421 421 # could have been made None by the previous if
422 422 if cl is None:
423 423 # Only filter if there's something to filter
424 424 cl = wrapchangelog(unfichangelog, revs) if revs else unfichangelog
425 425 object.__setattr__(self, '_clcache', cl)
426 426 object.__setattr__(self, '_clcachekey', newkey)
427 427 return cl
428 428
429 429 def unfiltered(self):
430 430 """Return an unfiltered version of a repo"""
431 431 return self._unfilteredrepo
432 432
433 433 def filtered(self, name, visibilityexceptions=None):
434 434 """Return a filtered version of a repository"""
435 435 if name == self.filtername and not visibilityexceptions:
436 436 return self
437 437 return self.unfiltered().filtered(name, visibilityexceptions)
438 438
439 439 def __repr__(self):
440 440 return '<%s:%s %r>' % (
441 441 self.__class__.__name__,
442 442 pycompat.sysstr(self.filtername),
443 443 self.unfiltered(),
444 444 )
445 445
446 446 # everything access are forwarded to the proxied repo
447 447 def __getattr__(self, attr):
448 448 return getattr(self._unfilteredrepo, attr)
449 449
450 450 def __setattr__(self, attr, value):
451 451 return setattr(self._unfilteredrepo, attr, value)
452 452
453 453 def __delattr__(self, attr):
454 454 return delattr(self._unfilteredrepo, attr)
455 455
456 456
457 457 # Python <3.4 easily leaks types via __mro__. See
458 458 # https://bugs.python.org/issue17950. We cache dynamically created types
459 459 # so they won't be leaked on every invocation of repo.filtered().
460 460 _filteredrepotypes = weakref.WeakKeyDictionary()
461 461
462 462
463 463 def newtype(base):
464 464 """Create a new type with the repoview mixin and the given base class"""
465 465 if base not in _filteredrepotypes:
466 466
467 467 class filteredrepo(repoview, base):
468 468 pass
469 469
470 470 _filteredrepotypes[base] = filteredrepo
471 471 return _filteredrepotypes[base]
General Comments 0
You need to be logged in to leave comments. Login now