##// END OF EJS Templates
typing: declare the `_phasesets` member of `phasecache` to be `Optional`...
Matt Harbison -
r52703:07086b3a default
parent child Browse files
Show More
@@ -1,1256 +1,1256 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
104 104 import heapq
105 105 import struct
106 106 import typing
107 107 import weakref
108 108
109 109 from typing import (
110 110 Any,
111 111 Callable,
112 112 Collection,
113 113 Dict,
114 114 Iterable,
115 115 List,
116 116 Optional,
117 117 Set,
118 118 Tuple,
119 119 )
120 120
121 121 from .i18n import _
122 122 from .node import (
123 123 bin,
124 124 hex,
125 125 nullrev,
126 126 short,
127 127 wdirrev,
128 128 )
129 129 from . import (
130 130 error,
131 131 requirements,
132 132 smartset,
133 133 txnutil,
134 134 util,
135 135 )
136 136
137 137 Phaseroots = Dict[int, Set[int]]
138 138 PhaseSets = Dict[int, Set[int]]
139 139
140 140 if typing.TYPE_CHECKING:
141 141 from . import (
142 142 localrepo,
143 143 ui as uimod,
144 144 )
145 145
146 146 # keeps pyflakes happy
147 147 assert [uimod]
148 148
149 149 Phasedefaults = List[
150 150 Callable[[localrepo.localrepository, Phaseroots], Phaseroots]
151 151 ]
152 152
153 153
154 154 _fphasesentry = struct.Struct(b'>i20s')
155 155
156 156 # record phase index
157 157 public: int = 0
158 158 draft: int = 1
159 159 secret: int = 2
160 160 archived = 32 # non-continuous for compatibility
161 161 internal = 96 # non-continuous for compatibility
162 162 allphases = (public, draft, secret, archived, internal)
163 163 trackedphases = (draft, secret, archived, internal)
164 164 not_public_phases = trackedphases
165 165 # record phase names
166 166 cmdphasenames = [b'public', b'draft', b'secret'] # known to `hg phase` command
167 167 phasenames = dict(enumerate(cmdphasenames))
168 168 phasenames[archived] = b'archived'
169 169 phasenames[internal] = b'internal'
170 170 # map phase name to phase number
171 171 phasenumber = {name: phase for phase, name in phasenames.items()}
172 172 # like phasenumber, but also include maps for the numeric and binary
173 173 # phase number to the phase number
174 174 phasenumber2 = phasenumber.copy()
175 175 phasenumber2.update({phase: phase for phase in phasenames})
176 176 phasenumber2.update({b'%i' % phase: phase for phase in phasenames})
177 177 # record phase property
178 178 mutablephases = (draft, secret, archived, internal)
179 179 relevant_mutable_phases = (draft, secret) # could be obsolete or unstable
180 180 remotehiddenphases = (secret, archived, internal)
181 181 localhiddenphases = (internal, archived)
182 182
183 183 all_internal_phases = tuple(p for p in allphases if p & internal)
184 184 # We do not want any internal content to exit the repository, ever.
185 185 no_bundle_phases = all_internal_phases
186 186
187 187
188 188 def supportinternal(repo: "localrepo.localrepository") -> bool:
189 189 """True if the internal phase can be used on a repository"""
190 190 return requirements.INTERNAL_PHASE_REQUIREMENT in repo.requirements
191 191
192 192
193 193 def supportarchived(repo: "localrepo.localrepository") -> bool:
194 194 """True if the archived phase can be used on a repository"""
195 195 return requirements.ARCHIVED_PHASE_REQUIREMENT in repo.requirements
196 196
197 197
198 198 def _readroots(
199 199 repo: "localrepo.localrepository",
200 200 phasedefaults: Optional["Phasedefaults"] = None,
201 201 ) -> Tuple[Phaseroots, bool]:
202 202 """Read phase roots from disk
203 203
204 204 phasedefaults is a list of fn(repo, roots) callable, which are
205 205 executed if the phase roots file does not exist. When phases are
206 206 being initialized on an existing repository, this could be used to
207 207 set selected changesets phase to something else than public.
208 208
209 209 Return (roots, dirty) where dirty is true if roots differ from
210 210 what is being stored.
211 211 """
212 212 repo = repo.unfiltered()
213 213 dirty = False
214 214 roots = {i: set() for i in allphases}
215 215 to_rev = repo.changelog.index.get_rev
216 216 unknown_msg = b'removing unknown node %s from %i-phase boundary\n'
217 217 try:
218 218 f, pending = txnutil.trypending(repo.root, repo.svfs, b'phaseroots')
219 219 try:
220 220 for line in f:
221 221 str_phase, hex_node = line.split()
222 222 phase = int(str_phase)
223 223 node = bin(hex_node)
224 224 rev = to_rev(node)
225 225 if rev is None:
226 226 repo.ui.debug(unknown_msg % (short(hex_node), phase))
227 227 dirty = True
228 228 else:
229 229 roots[phase].add(rev)
230 230 finally:
231 231 f.close()
232 232 except FileNotFoundError:
233 233 if phasedefaults:
234 234 for f in phasedefaults:
235 235 roots = f(repo, roots)
236 236 dirty = True
237 237 return roots, dirty
238 238
239 239
240 240 def binaryencode(phasemapping: Dict[int, List[bytes]]) -> bytes:
241 241 """encode a 'phase -> nodes' mapping into a binary stream
242 242
243 243 The revision lists are encoded as (phase, root) pairs.
244 244 """
245 245 binarydata = []
246 246 for phase, nodes in phasemapping.items():
247 247 for head in nodes:
248 248 binarydata.append(_fphasesentry.pack(phase, head))
249 249 return b''.join(binarydata)
250 250
251 251
252 252 def binarydecode(stream) -> Dict[int, List[bytes]]:
253 253 """decode a binary stream into a 'phase -> nodes' mapping
254 254
255 255 The (phase, root) pairs are turned back into a dictionary with
256 256 the phase as index and the aggregated roots of that phase as value."""
257 257 headsbyphase = {i: [] for i in allphases}
258 258 entrysize = _fphasesentry.size
259 259 while True:
260 260 entry = stream.read(entrysize)
261 261 if len(entry) < entrysize:
262 262 if entry:
263 263 raise error.Abort(_(b'bad phase-heads stream'))
264 264 break
265 265 phase, node = _fphasesentry.unpack(entry)
266 266 headsbyphase[phase].append(node)
267 267 return headsbyphase
268 268
269 269
270 270 def _sortedrange_insert(data, idx, rev, t):
271 271 merge_before = False
272 272 if idx:
273 273 r1, t1 = data[idx - 1]
274 274 merge_before = r1[-1] + 1 == rev and t1 == t
275 275 merge_after = False
276 276 if idx < len(data):
277 277 r2, t2 = data[idx]
278 278 merge_after = r2[0] == rev + 1 and t2 == t
279 279
280 280 if merge_before and merge_after:
281 281 data[idx - 1] = (range(r1[0], r2[-1] + 1), t)
282 282 data.pop(idx)
283 283 elif merge_before:
284 284 data[idx - 1] = (range(r1[0], rev + 1), t)
285 285 elif merge_after:
286 286 data[idx] = (range(rev, r2[-1] + 1), t)
287 287 else:
288 288 data.insert(idx, (range(rev, rev + 1), t))
289 289
290 290
291 291 def _sortedrange_split(data, idx, rev, t):
292 292 r1, t1 = data[idx]
293 293 if t == t1:
294 294 return
295 295 t = (t1[0], t[1])
296 296 if len(r1) == 1:
297 297 data.pop(idx)
298 298 _sortedrange_insert(data, idx, rev, t)
299 299 elif r1[0] == rev:
300 300 data[idx] = (range(rev + 1, r1[-1] + 1), t1)
301 301 _sortedrange_insert(data, idx, rev, t)
302 302 elif r1[-1] == rev:
303 303 data[idx] = (range(r1[0], rev), t1)
304 304 _sortedrange_insert(data, idx + 1, rev, t)
305 305 else:
306 306 data[idx : idx + 1] = [
307 307 (range(r1[0], rev), t1),
308 308 (range(rev, rev + 1), t),
309 309 (range(rev + 1, r1[-1] + 1), t1),
310 310 ]
311 311
312 312
313 313 def _trackphasechange(data, rev, old, new):
314 314 """add a phase move to the <data> list of ranges
315 315
316 316 If data is None, nothing happens.
317 317 """
318 318 if data is None:
319 319 return
320 320
321 321 # If data is empty, create a one-revision range and done
322 322 if not data:
323 323 data.insert(0, (range(rev, rev + 1), (old, new)))
324 324 return
325 325
326 326 low = 0
327 327 high = len(data)
328 328 t = (old, new)
329 329 while low < high:
330 330 mid = (low + high) // 2
331 331 revs = data[mid][0]
332 332 revs_low = revs[0]
333 333 revs_high = revs[-1]
334 334
335 335 if rev >= revs_low and rev <= revs_high:
336 336 _sortedrange_split(data, mid, rev, t)
337 337 return
338 338
339 339 if revs_low == rev + 1:
340 340 if mid and data[mid - 1][0][-1] == rev:
341 341 _sortedrange_split(data, mid - 1, rev, t)
342 342 else:
343 343 _sortedrange_insert(data, mid, rev, t)
344 344 return
345 345
346 346 if revs_high == rev - 1:
347 347 if mid + 1 < len(data) and data[mid + 1][0][0] == rev:
348 348 _sortedrange_split(data, mid + 1, rev, t)
349 349 else:
350 350 _sortedrange_insert(data, mid + 1, rev, t)
351 351 return
352 352
353 353 if revs_low > rev:
354 354 high = mid
355 355 else:
356 356 low = mid + 1
357 357
358 358 if low == len(data):
359 359 data.append((range(rev, rev + 1), t))
360 360 return
361 361
362 362 r1, t1 = data[low]
363 363 if r1[0] > rev:
364 364 data.insert(low, (range(rev, rev + 1), t))
365 365 else:
366 366 data.insert(low + 1, (range(rev, rev + 1), t))
367 367
368 368
369 369 # consider incrementaly updating the phase set the update set is not bigger
370 370 # than this size
371 371 #
372 372 # Be warned, this number is picked arbitrarily, without any benchmark. It
373 373 # should blindly pickup "small update"
374 374 INCREMENTAL_PHASE_SETS_UPDATE_MAX_UPDATE = 100
375 375
376 376
377 377 class phasecache:
378 378 def __init__(
379 379 self,
380 380 repo: "localrepo.localrepository",
381 381 phasedefaults: Optional["Phasedefaults"],
382 382 _load: bool = True,
383 383 ):
384 384 if _load:
385 385 # Cheap trick to allow shallow-copy without copy module
386 386 loaded = _readroots(repo, phasedefaults)
387 387 self._phaseroots: Phaseroots = loaded[0]
388 388 self.dirty: bool = loaded[1]
389 389 self._loadedrevslen = 0
390 self._phasesets: PhaseSets = None
390 self._phasesets: Optional[PhaseSets] = None
391 391
392 392 def hasnonpublicphases(self, repo: "localrepo.localrepository") -> bool:
393 393 """detect if there are revisions with non-public phase"""
394 394 # XXX deprecate the unused repo argument
395 395 return any(
396 396 revs for phase, revs in self._phaseroots.items() if phase != public
397 397 )
398 398
399 399 def nonpublicphaseroots(
400 400 self, repo: "localrepo.localrepository"
401 401 ) -> Set[int]:
402 402 """returns the roots of all non-public phases
403 403
404 404 The roots are not minimized, so if the secret revisions are
405 405 descendants of draft revisions, their roots will still be present.
406 406 """
407 407 repo = repo.unfiltered()
408 408 self._ensure_phase_sets(repo)
409 409 return set().union(
410 410 *[
411 411 revs
412 412 for phase, revs in self._phaseroots.items()
413 413 if phase != public
414 414 ]
415 415 )
416 416
417 417 def get_raw_set(
418 418 self,
419 419 repo: "localrepo.localrepository",
420 420 phase: int,
421 421 ) -> Set[int]:
422 422 """return the set of revision in that phase
423 423
424 424 The returned set is not filtered and might contains revision filtered
425 425 for the passed repoview.
426 426
427 427 The returned set might be the internal one and MUST NOT be mutated to
428 428 avoid side effect.
429 429 """
430 430 if phase == public:
431 431 raise error.ProgrammingError("cannot get_set for public phase")
432 432 self._ensure_phase_sets(repo.unfiltered())
433 433 revs = self._phasesets.get(phase)
434 434 if revs is None:
435 435 return set()
436 436 return revs
437 437
438 438 def getrevset(
439 439 self,
440 440 repo: "localrepo.localrepository",
441 441 phases: Iterable[int],
442 442 subset: Optional[Any] = None,
443 443 ) -> Any:
444 444 # TODO: finish typing this
445 445 """return a smartset for the given phases"""
446 446 self._ensure_phase_sets(repo.unfiltered())
447 447 phases = set(phases)
448 448 publicphase = public in phases
449 449
450 450 if publicphase:
451 451 # In this case, phases keeps all the *other* phases.
452 452 phases = set(allphases).difference(phases)
453 453 if not phases:
454 454 return smartset.fullreposet(repo)
455 455
456 456 # fast path: _phasesets contains the interesting sets,
457 457 # might only need a union and post-filtering.
458 458 revsneedscopy = False
459 459 if len(phases) == 1:
460 460 [p] = phases
461 461 revs = self._phasesets[p]
462 462 revsneedscopy = True # Don't modify _phasesets
463 463 else:
464 464 # revs has the revisions in all *other* phases.
465 465 revs = set.union(*[self._phasesets[p] for p in phases])
466 466
467 467 def _addwdir(wdirsubset, wdirrevs):
468 468 if wdirrev in wdirsubset and repo[None].phase() in phases:
469 469 if revsneedscopy:
470 470 wdirrevs = wdirrevs.copy()
471 471 # The working dir would never be in the # cache, but it was in
472 472 # the subset being filtered for its phase (or filtered out,
473 473 # depending on publicphase), so add it to the output to be
474 474 # included (or filtered out).
475 475 wdirrevs.add(wdirrev)
476 476 return wdirrevs
477 477
478 478 if not publicphase:
479 479 if repo.changelog.filteredrevs:
480 480 revs = revs - repo.changelog.filteredrevs
481 481
482 482 if subset is None:
483 483 return smartset.baseset(revs)
484 484 else:
485 485 revs = _addwdir(subset, revs)
486 486 return subset & smartset.baseset(revs)
487 487 else:
488 488 if subset is None:
489 489 subset = smartset.fullreposet(repo)
490 490
491 491 revs = _addwdir(subset, revs)
492 492
493 493 if not revs:
494 494 return subset
495 495 return subset.filter(lambda r: r not in revs)
496 496
497 497 def copy(self):
498 498 # Shallow copy meant to ensure isolation in
499 499 # advance/retractboundary(), nothing more.
500 500 ph = self.__class__(None, None, _load=False)
501 501 ph._phaseroots = self._phaseroots.copy()
502 502 ph.dirty = self.dirty
503 503 ph._loadedrevslen = self._loadedrevslen
504 504 if self._phasesets is None:
505 505 ph._phasesets = None
506 506 else:
507 507 ph._phasesets = self._phasesets.copy()
508 508 return ph
509 509
510 510 def replace(self, phcache):
511 511 """replace all values in 'self' with content of phcache"""
512 512 for a in (
513 513 '_phaseroots',
514 514 'dirty',
515 515 '_loadedrevslen',
516 516 '_phasesets',
517 517 ):
518 518 setattr(self, a, getattr(phcache, a))
519 519
520 520 def _getphaserevsnative(self, repo):
521 521 repo = repo.unfiltered()
522 522 return repo.changelog.computephases(self._phaseroots)
523 523
524 524 def _computephaserevspure(self, repo):
525 525 repo = repo.unfiltered()
526 526 cl = repo.changelog
527 527 self._phasesets = {phase: set() for phase in allphases}
528 528 lowerroots = set()
529 529 for phase in reversed(trackedphases):
530 530 roots = self._phaseroots[phase]
531 531 if roots:
532 532 ps = set(cl.descendants(roots))
533 533 for root in roots:
534 534 ps.add(root)
535 535 ps.difference_update(lowerroots)
536 536 lowerroots.update(ps)
537 537 self._phasesets[phase] = ps
538 538 self._loadedrevslen = len(cl)
539 539
540 540 def _ensure_phase_sets(self, repo: "localrepo.localrepository") -> None:
541 541 """ensure phase information is loaded in the object"""
542 542 assert repo.filtername is None
543 543 update = -1
544 544 cl = repo.changelog
545 545 cl_size = len(cl)
546 546 if self._phasesets is None:
547 547 update = 0
548 548 else:
549 549 if cl_size > self._loadedrevslen:
550 550 # check if an incremental update is worth it.
551 551 # note we need a tradeoff here because the whole logic is not
552 552 # stored and implemented in native code nd datastructure.
553 553 # Otherwise the incremental update woul always be a win.
554 554 missing = cl_size - self._loadedrevslen
555 555 if missing <= INCREMENTAL_PHASE_SETS_UPDATE_MAX_UPDATE:
556 556 update = self._loadedrevslen
557 557 else:
558 558 update = 0
559 559
560 560 if update == 0:
561 561 try:
562 562 res = self._getphaserevsnative(repo)
563 563 self._loadedrevslen, self._phasesets = res
564 564 except AttributeError:
565 565 self._computephaserevspure(repo)
566 566 assert self._loadedrevslen == len(repo.changelog)
567 567 elif update > 0:
568 568 # good candidate for native code
569 569 assert update == self._loadedrevslen
570 570 if self.hasnonpublicphases(repo):
571 571 start = self._loadedrevslen
572 572 get_phase = self.phase
573 573 rev_phases = [0] * missing
574 574 parents = cl.parentrevs
575 575 sets = {phase: set() for phase in self._phasesets}
576 576 for phase, roots in self._phaseroots.items():
577 577 # XXX should really store the max somewhere
578 578 for r in roots:
579 579 if r >= start:
580 580 rev_phases[r - start] = phase
581 581 for rev in range(start, cl_size):
582 582 phase = rev_phases[rev - start]
583 583 p1, p2 = parents(rev)
584 584 if p1 == nullrev:
585 585 p1_phase = public
586 586 elif p1 >= start:
587 587 p1_phase = rev_phases[p1 - start]
588 588 else:
589 589 p1_phase = max(phase, get_phase(repo, p1))
590 590 if p2 == nullrev:
591 591 p2_phase = public
592 592 elif p2 >= start:
593 593 p2_phase = rev_phases[p2 - start]
594 594 else:
595 595 p2_phase = max(phase, get_phase(repo, p2))
596 596 phase = max(phase, p1_phase, p2_phase)
597 597 if phase > public:
598 598 rev_phases[rev - start] = phase
599 599 sets[phase].add(rev)
600 600
601 601 # Be careful to preserve shallow-copied values: do not update
602 602 # phaseroots values, replace them.
603 603 for phase, extra in sets.items():
604 604 if extra:
605 605 self._phasesets[phase] = self._phasesets[phase] | extra
606 606 self._loadedrevslen = cl_size
607 607
608 608 def invalidate(self):
609 609 self._loadedrevslen = 0
610 610 self._phasesets = None
611 611
612 612 def phase(self, repo: "localrepo.localrepository", rev: int) -> int:
613 613 # We need a repo argument here to be able to build _phasesets
614 614 # if necessary. The repository instance is not stored in
615 615 # phasecache to avoid reference cycles. The changelog instance
616 616 # is not stored because it is a filecache() property and can
617 617 # be replaced without us being notified.
618 618 if rev == nullrev:
619 619 return public
620 620 if rev < nullrev:
621 621 raise ValueError(_(b'cannot lookup negative revision'))
622 622 # double check self._loadedrevslen to avoid an extra method call as
623 623 # python is slow for that.
624 624 if rev >= self._loadedrevslen:
625 625 self._ensure_phase_sets(repo.unfiltered())
626 626 for phase in trackedphases:
627 627 if rev in self._phasesets[phase]:
628 628 return phase
629 629 return public
630 630
631 631 def write(self, repo):
632 632 if not self.dirty:
633 633 return
634 634 f = repo.svfs(b'phaseroots', b'w', atomictemp=True, checkambig=True)
635 635 try:
636 636 self._write(repo.unfiltered(), f)
637 637 finally:
638 638 f.close()
639 639
640 640 def _write(self, repo, fp):
641 641 assert repo.filtername is None
642 642 to_node = repo.changelog.node
643 643 for phase, roots in self._phaseroots.items():
644 644 for r in sorted(roots):
645 645 h = to_node(r)
646 646 fp.write(b'%i %s\n' % (phase, hex(h)))
647 647 self.dirty = False
648 648
649 649 def _updateroots(self, repo, phase, newroots, tr, invalidate=True):
650 650 self._phaseroots[phase] = newroots
651 651 self.dirty = True
652 652 if invalidate:
653 653 self.invalidate()
654 654
655 655 assert repo.filtername is None
656 656 wrepo = weakref.ref(repo)
657 657
658 658 def tr_write(fp):
659 659 repo = wrepo()
660 660 assert repo is not None
661 661 self._write(repo, fp)
662 662
663 663 tr.addfilegenerator(b'phase', (b'phaseroots',), tr_write)
664 664 tr.hookargs[b'phases_moved'] = b'1'
665 665
666 666 def registernew(self, repo, tr, targetphase, revs):
667 667 repo = repo.unfiltered()
668 668 self._retractboundary(repo, tr, targetphase, [], revs=revs)
669 669 if tr is not None and b'phases' in tr.changes:
670 670 phasetracking = tr.changes[b'phases']
671 671 phase = self.phase
672 672 for rev in sorted(revs):
673 673 revphase = phase(repo, rev)
674 674 _trackphasechange(phasetracking, rev, None, revphase)
675 675 repo.invalidatevolatilesets()
676 676
677 677 def advanceboundary(
678 678 self, repo, tr, targetphase, nodes=None, revs=None, dryrun=None
679 679 ):
680 680 """Set all 'nodes' to phase 'targetphase'
681 681
682 682 Nodes with a phase lower than 'targetphase' are not affected.
683 683
684 684 If dryrun is True, no actions will be performed
685 685
686 686 Returns a set of revs whose phase is changed or should be changed
687 687 """
688 688 if targetphase == public and not self.hasnonpublicphases(repo):
689 689 return set()
690 690 repo = repo.unfiltered()
691 691 cl = repo.changelog
692 692 torev = cl.index.rev
693 693 # Be careful to preserve shallow-copied values: do not update
694 694 # phaseroots values, replace them.
695 695 new_revs = set()
696 696 if revs is not None:
697 697 new_revs.update(revs)
698 698 if nodes is not None:
699 699 new_revs.update(torev(node) for node in nodes)
700 700 if not new_revs: # bail out early to avoid the loadphaserevs call
701 701 return (
702 702 set()
703 703 ) # note: why do people call advanceboundary with nothing?
704 704
705 705 if tr is None:
706 706 phasetracking = None
707 707 else:
708 708 phasetracking = tr.changes.get(b'phases')
709 709
710 710 affectable_phases = sorted(
711 711 p for p in allphases if p > targetphase and self._phaseroots[p]
712 712 )
713 713 # filter revision already in the right phases
714 714 candidates = new_revs
715 715 new_revs = set()
716 716 self._ensure_phase_sets(repo)
717 717 for phase in affectable_phases:
718 718 found = candidates & self._phasesets[phase]
719 719 new_revs |= found
720 720 candidates -= found
721 721 if not candidates:
722 722 break
723 723 if not new_revs:
724 724 return set()
725 725
726 726 # search for affected high phase changesets and roots
727 727 seen = set(new_revs)
728 728 push = heapq.heappush
729 729 pop = heapq.heappop
730 730 parents = cl.parentrevs
731 731 get_phase = self.phase
732 732 changed = {} # set of revisions to be changed
733 733 # set of root deleted by this path
734 734 delroots = set()
735 735 new_roots = {p: set() for p in affectable_phases}
736 736 new_target_roots = set()
737 737 # revision to walk down
738 738 revs = [-r for r in new_revs]
739 739 heapq.heapify(revs)
740 740 while revs:
741 741 current = -pop(revs)
742 742 current_phase = get_phase(repo, current)
743 743 changed[current] = current_phase
744 744 p1, p2 = parents(current)
745 745 if p1 == nullrev:
746 746 p1_phase = public
747 747 else:
748 748 p1_phase = get_phase(repo, p1)
749 749 if p2 == nullrev:
750 750 p2_phase = public
751 751 else:
752 752 p2_phase = get_phase(repo, p2)
753 753 # do we have a root ?
754 754 if current_phase != p1_phase and current_phase != p2_phase:
755 755 # do not record phase, because we could have "duplicated"
756 756 # roots, were one root is shadowed by the very same roots of an
757 757 # higher phases
758 758 delroots.add(current)
759 759 # schedule a walk down if needed
760 760 if p1_phase > targetphase and p1 not in seen:
761 761 seen.add(p1)
762 762 push(revs, -p1)
763 763 if p2_phase > targetphase and p2 not in seen:
764 764 seen.add(p2)
765 765 push(revs, -p2)
766 766 if p1_phase < targetphase and p2_phase < targetphase:
767 767 new_target_roots.add(current)
768 768
769 769 # the last iteration was done with the smallest value
770 770 min_current = current
771 771 # do we have unwalked children that might be new roots
772 772 if (min_current + len(changed)) < len(cl):
773 773 for r in range(min_current, len(cl)):
774 774 if r in changed:
775 775 continue
776 776 phase = get_phase(repo, r)
777 777 if phase <= targetphase:
778 778 continue
779 779 p1, p2 = parents(r)
780 780 if not (p1 in changed or p2 in changed):
781 781 continue # not affected
782 782 if p1 != nullrev and p1 not in changed:
783 783 p1_phase = get_phase(repo, p1)
784 784 if p1_phase == phase:
785 785 continue # not a root
786 786 if p2 != nullrev and p2 not in changed:
787 787 p2_phase = get_phase(repo, p2)
788 788 if p2_phase == phase:
789 789 continue # not a root
790 790 new_roots[phase].add(r)
791 791
792 792 # apply the changes
793 793 if not dryrun:
794 794 for r, p in changed.items():
795 795 _trackphasechange(phasetracking, r, p, targetphase)
796 796 if targetphase > public:
797 797 self._phasesets[targetphase].update(changed)
798 798 for phase in affectable_phases:
799 799 roots = self._phaseroots[phase]
800 800 removed = roots & delroots
801 801 if removed or new_roots[phase]:
802 802 self._phasesets[phase].difference_update(changed)
803 803 # Be careful to preserve shallow-copied values: do not
804 804 # update phaseroots values, replace them.
805 805 final_roots = roots - delroots | new_roots[phase]
806 806 self._updateroots(
807 807 repo, phase, final_roots, tr, invalidate=False
808 808 )
809 809 if new_target_roots:
810 810 # Thanks for previous filtering, we can't replace existing
811 811 # roots
812 812 new_target_roots |= self._phaseroots[targetphase]
813 813 self._updateroots(
814 814 repo, targetphase, new_target_roots, tr, invalidate=False
815 815 )
816 816 repo.invalidatevolatilesets()
817 817 return changed
818 818
819 819 def retractboundary(self, repo, tr, targetphase, nodes):
820 820 if tr is None:
821 821 phasetracking = None
822 822 else:
823 823 phasetracking = tr.changes.get(b'phases')
824 824 repo = repo.unfiltered()
825 825 retracted = self._retractboundary(repo, tr, targetphase, nodes)
826 826 if retracted and phasetracking is not None:
827 827 for r, old_phase in sorted(retracted.items()):
828 828 _trackphasechange(phasetracking, r, old_phase, targetphase)
829 829 repo.invalidatevolatilesets()
830 830
831 831 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
832 832 if targetphase == public:
833 833 return {}
834 834 if (
835 835 targetphase == internal
836 836 and not supportinternal(repo)
837 837 or targetphase == archived
838 838 and not supportarchived(repo)
839 839 ):
840 840 name = phasenames[targetphase]
841 841 msg = b'this repository does not support the %s phase' % name
842 842 raise error.ProgrammingError(msg)
843 843 assert repo.filtername is None
844 844 cl = repo.changelog
845 845 torev = cl.index.rev
846 846 new_revs = set()
847 847 if revs is not None:
848 848 new_revs.update(revs)
849 849 if nodes is not None:
850 850 new_revs.update(torev(node) for node in nodes)
851 851 if not new_revs: # bail out early to avoid the loadphaserevs call
852 852 return {} # note: why do people call retractboundary with nothing ?
853 853
854 854 if nullrev in new_revs:
855 855 raise error.Abort(_(b'cannot change null revision phase'))
856 856
857 857 # Filter revision that are already in the right phase
858 858 self._ensure_phase_sets(repo)
859 859 for phase, revs in self._phasesets.items():
860 860 if phase >= targetphase:
861 861 new_revs -= revs
862 862 if not new_revs: # all revisions already in the right phases
863 863 return {}
864 864
865 865 # Compute change in phase roots by walking the graph
866 866 #
867 867 # note: If we had a cheap parent β†’ children mapping we could do
868 868 # something even cheaper/more-bounded
869 869 #
870 870 # The idea would be to walk from item in new_revs stopping at
871 871 # descendant with phases >= target_phase.
872 872 #
873 873 # 1) This detect new_revs that are not new_roots (either already >=
874 874 # target_phase or reachable though another new_revs
875 875 # 2) This detect replaced current_roots as we reach them
876 876 # 3) This can avoid walking to the tip if we retract over a small
877 877 # branch.
878 878 #
879 879 # So instead, we do a variation of this, we walk from the smaller new
880 880 # revision to the tip to avoid missing any potential children.
881 881 #
882 882 # The following code would be a good candidate for native code… if only
883 883 # we could knew the phase of a changeset efficiently in native code.
884 884 parents = cl.parentrevs
885 885 phase = self.phase
886 886 new_roots = set() # roots added by this phases
887 887 changed_revs = {} # revision affected by this call
888 888 replaced_roots = set() # older roots replaced by this call
889 889 currentroots = self._phaseroots[targetphase]
890 890 start = min(new_revs)
891 891 end = len(cl)
892 892 rev_phases = [None] * (end - start)
893 893
894 894 this_phase_set = self._phasesets[targetphase]
895 895 for r in range(start, end):
896 896 # gather information about the current_rev
897 897 r_phase = phase(repo, r)
898 898 p_phase = None # phase inherited from parents
899 899 p1, p2 = parents(r)
900 900 if p1 >= start:
901 901 p1_phase = rev_phases[p1 - start]
902 902 if p1_phase is not None:
903 903 p_phase = p1_phase
904 904 if p2 >= start:
905 905 p2_phase = rev_phases[p2 - start]
906 906 if p2_phase is not None:
907 907 if p_phase is not None:
908 908 p_phase = max(p_phase, p2_phase)
909 909 else:
910 910 p_phase = p2_phase
911 911
912 912 # assess the situation
913 913 if r in new_revs and r_phase < targetphase:
914 914 if p_phase is None or p_phase < targetphase:
915 915 new_roots.add(r)
916 916 rev_phases[r - start] = targetphase
917 917 changed_revs[r] = r_phase
918 918 this_phase_set.add(r)
919 919 elif p_phase is None:
920 920 rev_phases[r - start] = r_phase
921 921 else:
922 922 if p_phase > r_phase:
923 923 rev_phases[r - start] = p_phase
924 924 else:
925 925 rev_phases[r - start] = r_phase
926 926 if p_phase == targetphase:
927 927 if p_phase > r_phase:
928 928 changed_revs[r] = r_phase
929 929 this_phase_set.add(r)
930 930 elif r in currentroots:
931 931 replaced_roots.add(r)
932 932 sets = self._phasesets
933 933 if targetphase > draft:
934 934 for r, old in changed_revs.items():
935 935 if old > public:
936 936 sets[old].discard(r)
937 937
938 938 if new_roots:
939 939 assert changed_revs
940 940
941 941 final_roots = new_roots | currentroots - replaced_roots
942 942 self._updateroots(
943 943 repo,
944 944 targetphase,
945 945 final_roots,
946 946 tr,
947 947 invalidate=False,
948 948 )
949 949 if targetphase > 1:
950 950 retracted = set(changed_revs)
951 951 for lower_phase in range(1, targetphase):
952 952 lower_roots = self._phaseroots.get(lower_phase)
953 953 if lower_roots is None:
954 954 continue
955 955 if lower_roots & retracted:
956 956 simpler_roots = lower_roots - retracted
957 957 self._updateroots(
958 958 repo,
959 959 lower_phase,
960 960 simpler_roots,
961 961 tr,
962 962 invalidate=False,
963 963 )
964 964 return changed_revs
965 965 else:
966 966 assert not changed_revs
967 967 assert not replaced_roots
968 968 return {}
969 969
970 970 def register_strip(
971 971 self,
972 972 repo,
973 973 tr,
974 974 strip_rev: int,
975 975 ):
976 976 """announce a strip to the phase cache
977 977
978 978 Any roots higher than the stripped revision should be dropped.
979 979 """
980 980 for targetphase, roots in list(self._phaseroots.items()):
981 981 filtered = {r for r in roots if r >= strip_rev}
982 982 if filtered:
983 983 self._updateroots(repo, targetphase, roots - filtered, tr)
984 984 self.invalidate()
985 985
986 986
987 987 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
988 988 """Add nodes to a phase changing other nodes phases if necessary.
989 989
990 990 This function move boundary *forward* this means that all nodes
991 991 are set in the target phase or kept in a *lower* phase.
992 992
993 993 Simplify boundary to contains phase roots only.
994 994
995 995 If dryrun is True, no actions will be performed
996 996
997 997 Returns a set of revs whose phase is changed or should be changed
998 998 """
999 999 if revs is None:
1000 1000 revs = []
1001 1001 phcache = repo._phasecache.copy()
1002 1002 changes = phcache.advanceboundary(
1003 1003 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
1004 1004 )
1005 1005 if not dryrun:
1006 1006 repo._phasecache.replace(phcache)
1007 1007 return changes
1008 1008
1009 1009
1010 1010 def retractboundary(repo, tr, targetphase, nodes):
1011 1011 """Set nodes back to a phase changing other nodes phases if
1012 1012 necessary.
1013 1013
1014 1014 This function move boundary *backward* this means that all nodes
1015 1015 are set in the target phase or kept in a *higher* phase.
1016 1016
1017 1017 Simplify boundary to contains phase roots only."""
1018 1018 phcache = repo._phasecache.copy()
1019 1019 phcache.retractboundary(repo, tr, targetphase, nodes)
1020 1020 repo._phasecache.replace(phcache)
1021 1021
1022 1022
1023 1023 def registernew(repo, tr, targetphase, revs):
1024 1024 """register a new revision and its phase
1025 1025
1026 1026 Code adding revisions to the repository should use this function to
1027 1027 set new changeset in their target phase (or higher).
1028 1028 """
1029 1029 phcache = repo._phasecache.copy()
1030 1030 phcache.registernew(repo, tr, targetphase, revs)
1031 1031 repo._phasecache.replace(phcache)
1032 1032
1033 1033
1034 1034 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
1035 1035 """List phases root for serialization over pushkey"""
1036 1036 # Use ordered dictionary so behavior is deterministic.
1037 1037 keys = util.sortdict()
1038 1038 value = b'%i' % draft
1039 1039 cl = repo.unfiltered().changelog
1040 1040 to_node = cl.node
1041 1041 for root in repo._phasecache._phaseroots[draft]:
1042 1042 if repo._phasecache.phase(repo, root) <= draft:
1043 1043 keys[hex(to_node(root))] = value
1044 1044
1045 1045 if repo.publishing():
1046 1046 # Add an extra data to let remote know we are a publishing
1047 1047 # repo. Publishing repo can't just pretend they are old repo.
1048 1048 # When pushing to a publishing repo, the client still need to
1049 1049 # push phase boundary
1050 1050 #
1051 1051 # Push do not only push changeset. It also push phase data.
1052 1052 # New phase data may apply to common changeset which won't be
1053 1053 # push (as they are common). Here is a very simple example:
1054 1054 #
1055 1055 # 1) repo A push changeset X as draft to repo B
1056 1056 # 2) repo B make changeset X public
1057 1057 # 3) repo B push to repo A. X is not pushed but the data that
1058 1058 # X as now public should
1059 1059 #
1060 1060 # The server can't handle it on it's own as it has no idea of
1061 1061 # client phase data.
1062 1062 keys[b'publishing'] = b'True'
1063 1063 return keys
1064 1064
1065 1065
1066 1066 def pushphase(
1067 1067 repo: "localrepo.localrepository",
1068 1068 nhex: bytes,
1069 1069 oldphasestr: bytes,
1070 1070 newphasestr: bytes,
1071 1071 ) -> bool:
1072 1072 """List phases root for serialization over pushkey"""
1073 1073 repo = repo.unfiltered()
1074 1074 with repo.lock():
1075 1075 currentphase = repo[nhex].phase()
1076 1076 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
1077 1077 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
1078 1078 if currentphase == oldphase and newphase < oldphase:
1079 1079 with repo.transaction(b'pushkey-phase') as tr:
1080 1080 advanceboundary(repo, tr, newphase, [bin(nhex)])
1081 1081 return True
1082 1082 elif currentphase == newphase:
1083 1083 # raced, but got correct result
1084 1084 return True
1085 1085 else:
1086 1086 return False
1087 1087
1088 1088
1089 1089 def subsetphaseheads(repo, subset):
1090 1090 """Finds the phase heads for a subset of a history
1091 1091
1092 1092 Returns a list indexed by phase number where each item is a list of phase
1093 1093 head nodes.
1094 1094 """
1095 1095 cl = repo.changelog
1096 1096
1097 1097 headsbyphase = {i: [] for i in allphases}
1098 1098 for phase in allphases:
1099 1099 revset = b"heads(%%ln & _phase(%d))" % phase
1100 1100 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
1101 1101 return headsbyphase
1102 1102
1103 1103
1104 1104 def updatephases(repo, trgetter, headsbyphase):
1105 1105 """Updates the repo with the given phase heads"""
1106 1106 # Now advance phase boundaries of all phases
1107 1107 #
1108 1108 # run the update (and fetch transaction) only if there are actually things
1109 1109 # to update. This avoid creating empty transaction during no-op operation.
1110 1110
1111 1111 for phase in allphases:
1112 1112 revset = b'%ln - _phase(%s)'
1113 1113 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
1114 1114 if heads:
1115 1115 advanceboundary(repo, trgetter(), phase, heads)
1116 1116
1117 1117
1118 1118 def analyze_remote_phases(
1119 1119 repo,
1120 1120 subset: Collection[int],
1121 1121 roots: Dict[bytes, bytes],
1122 1122 ) -> Tuple[Collection[int], Collection[int]]:
1123 1123 """Compute phases heads and root in a subset of node from root dict
1124 1124
1125 1125 * subset is heads of the subset
1126 1126 * roots is {<nodeid> => phase} mapping. key and value are string.
1127 1127
1128 1128 Accept unknown element input
1129 1129 """
1130 1130 repo = repo.unfiltered()
1131 1131 # build list from dictionary
1132 1132 draft_roots = []
1133 1133 to_rev = repo.changelog.index.get_rev
1134 1134 for nhex, phase in roots.items():
1135 1135 if nhex == b'publishing': # ignore data related to publish option
1136 1136 continue
1137 1137 node = bin(nhex)
1138 1138 phase = int(phase)
1139 1139 if phase == public:
1140 1140 if node != repo.nullid:
1141 1141 msg = _(b'ignoring inconsistent public root from remote: %s\n')
1142 1142 repo.ui.warn(msg % nhex)
1143 1143 elif phase == draft:
1144 1144 rev = to_rev(node)
1145 1145 if rev is not None: # to filter unknown nodes
1146 1146 draft_roots.append(rev)
1147 1147 else:
1148 1148 msg = _(b'ignoring unexpected root from remote: %i %s\n')
1149 1149 repo.ui.warn(msg % (phase, nhex))
1150 1150 # compute heads
1151 1151 public_heads = new_heads(repo, subset, draft_roots)
1152 1152 return public_heads, draft_roots
1153 1153
1154 1154
1155 1155 class RemotePhasesSummary:
1156 1156 """summarize phase information on the remote side
1157 1157
1158 1158 :publishing: True is the remote is publishing
1159 1159 :public_heads: list of remote public phase heads (revs)
1160 1160 :draft_heads: list of remote draft phase heads (revs)
1161 1161 :draft_roots: list of remote draft phase root (revs)
1162 1162 """
1163 1163
1164 1164 def __init__(
1165 1165 self,
1166 1166 repo,
1167 1167 remote_subset: Collection[int],
1168 1168 remote_roots: Dict[bytes, bytes],
1169 1169 ):
1170 1170 unfi = repo.unfiltered()
1171 1171 self._allremoteroots: Dict[bytes, bytes] = remote_roots
1172 1172
1173 1173 self.publishing: bool = bool(remote_roots.get(b'publishing', False))
1174 1174
1175 1175 heads, roots = analyze_remote_phases(repo, remote_subset, remote_roots)
1176 1176 self.public_heads: Collection[int] = heads
1177 1177 self.draft_roots: Collection[int] = roots
1178 1178 # Get the list of all "heads" revs draft on remote
1179 1179 dheads = unfi.revs(b'heads(%ld::%ld)', roots, remote_subset)
1180 1180 self.draft_heads: Collection[int] = dheads
1181 1181
1182 1182
1183 1183 def new_heads(
1184 1184 repo,
1185 1185 heads: Collection[int],
1186 1186 roots: Collection[int],
1187 1187 ) -> Collection[int]:
1188 1188 """compute new head of a subset minus another
1189 1189
1190 1190 * `heads`: define the first subset
1191 1191 * `roots`: define the second we subtract from the first"""
1192 1192 # prevent an import cycle
1193 1193 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
1194 1194 from . import dagop
1195 1195
1196 1196 if not roots:
1197 1197 return heads
1198 1198 if not heads or heads == [nullrev]:
1199 1199 return []
1200 1200 # The logic operated on revisions, convert arguments early for convenience
1201 1201 # PERF-XXX: maybe heads could directly comes as a set without impacting
1202 1202 # other user of that value
1203 1203 new_heads = set(heads)
1204 1204 new_heads.discard(nullrev)
1205 1205 # compute the area we need to remove
1206 1206 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
1207 1207 # heads in the area are no longer heads
1208 1208 new_heads.difference_update(affected_zone)
1209 1209 # revisions in the area have children outside of it,
1210 1210 # They might be new heads
1211 1211 candidates = repo.revs(
1212 1212 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
1213 1213 )
1214 1214 candidates -= affected_zone
1215 1215 if new_heads or candidates:
1216 1216 # remove candidate that are ancestors of other heads
1217 1217 new_heads.update(candidates)
1218 1218 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
1219 1219 pruned = dagop.reachableroots(repo, candidates, prunestart)
1220 1220 new_heads.difference_update(pruned)
1221 1221
1222 1222 # PERF-XXX: do we actually need a sorted list here? Could we simply return
1223 1223 # a set?
1224 1224 return sorted(new_heads)
1225 1225
1226 1226
1227 1227 def newcommitphase(ui: "uimod.ui") -> int:
1228 1228 """helper to get the target phase of new commit
1229 1229
1230 1230 Handle all possible values for the phases.new-commit options.
1231 1231
1232 1232 """
1233 1233 v = ui.config(b'phases', b'new-commit')
1234 1234 try:
1235 1235 return phasenumber2[v]
1236 1236 except KeyError:
1237 1237 raise error.ConfigError(
1238 1238 _(b"phases.new-commit: not a valid phase name ('%s')") % v
1239 1239 )
1240 1240
1241 1241
1242 1242 def hassecret(repo: "localrepo.localrepository") -> bool:
1243 1243 """utility function that check if a repo have any secret changeset."""
1244 1244 return bool(repo._phasecache._phaseroots[secret])
1245 1245
1246 1246
1247 1247 def preparehookargs(
1248 1248 node: bytes,
1249 1249 old: Optional[int],
1250 1250 new: Optional[int],
1251 1251 ) -> Dict[bytes, bytes]:
1252 1252 if old is None:
1253 1253 old = b''
1254 1254 else:
1255 1255 old = phasenames[old]
1256 1256 return {b'node': node, b'oldphase': old, b'phase': phasenames[new]}
General Comments 0
You need to be logged in to leave comments. Login now