##// END OF EJS Templates
phases: avoid a potentially costly dictionary interation in some case...
marmoute -
r52409:e0f92bd9 stable
parent child Browse files
Show More
@@ -1,1223 +1,1224 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 Dict,
113 113 Iterable,
114 114 List,
115 115 Optional,
116 116 Set,
117 117 Tuple,
118 118 )
119 119
120 120 from .i18n import _
121 121 from .node import (
122 122 bin,
123 123 hex,
124 124 nullrev,
125 125 short,
126 126 wdirrev,
127 127 )
128 128 from . import (
129 129 error,
130 130 pycompat,
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 390 self._phasesets: 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 getrevset(
418 418 self,
419 419 repo: "localrepo.localrepository",
420 420 phases: Iterable[int],
421 421 subset: Optional[Any] = None,
422 422 ) -> Any:
423 423 # TODO: finish typing this
424 424 """return a smartset for the given phases"""
425 425 self._ensure_phase_sets(repo.unfiltered())
426 426 phases = set(phases)
427 427 publicphase = public in phases
428 428
429 429 if publicphase:
430 430 # In this case, phases keeps all the *other* phases.
431 431 phases = set(allphases).difference(phases)
432 432 if not phases:
433 433 return smartset.fullreposet(repo)
434 434
435 435 # fast path: _phasesets contains the interesting sets,
436 436 # might only need a union and post-filtering.
437 437 revsneedscopy = False
438 438 if len(phases) == 1:
439 439 [p] = phases
440 440 revs = self._phasesets[p]
441 441 revsneedscopy = True # Don't modify _phasesets
442 442 else:
443 443 # revs has the revisions in all *other* phases.
444 444 revs = set.union(*[self._phasesets[p] for p in phases])
445 445
446 446 def _addwdir(wdirsubset, wdirrevs):
447 447 if wdirrev in wdirsubset and repo[None].phase() in phases:
448 448 if revsneedscopy:
449 449 wdirrevs = wdirrevs.copy()
450 450 # The working dir would never be in the # cache, but it was in
451 451 # the subset being filtered for its phase (or filtered out,
452 452 # depending on publicphase), so add it to the output to be
453 453 # included (or filtered out).
454 454 wdirrevs.add(wdirrev)
455 455 return wdirrevs
456 456
457 457 if not publicphase:
458 458 if repo.changelog.filteredrevs:
459 459 revs = revs - repo.changelog.filteredrevs
460 460
461 461 if subset is None:
462 462 return smartset.baseset(revs)
463 463 else:
464 464 revs = _addwdir(subset, revs)
465 465 return subset & smartset.baseset(revs)
466 466 else:
467 467 if subset is None:
468 468 subset = smartset.fullreposet(repo)
469 469
470 470 revs = _addwdir(subset, revs)
471 471
472 472 if not revs:
473 473 return subset
474 474 return subset.filter(lambda r: r not in revs)
475 475
476 476 def copy(self):
477 477 # Shallow copy meant to ensure isolation in
478 478 # advance/retractboundary(), nothing more.
479 479 ph = self.__class__(None, None, _load=False)
480 480 ph._phaseroots = self._phaseroots.copy()
481 481 ph.dirty = self.dirty
482 482 ph._loadedrevslen = self._loadedrevslen
483 483 if self._phasesets is None:
484 484 ph._phasesets = None
485 485 else:
486 486 ph._phasesets = self._phasesets.copy()
487 487 return ph
488 488
489 489 def replace(self, phcache):
490 490 """replace all values in 'self' with content of phcache"""
491 491 for a in (
492 492 '_phaseroots',
493 493 'dirty',
494 494 '_loadedrevslen',
495 495 '_phasesets',
496 496 ):
497 497 setattr(self, a, getattr(phcache, a))
498 498
499 499 def _getphaserevsnative(self, repo):
500 500 repo = repo.unfiltered()
501 501 return repo.changelog.computephases(self._phaseroots)
502 502
503 503 def _computephaserevspure(self, repo):
504 504 repo = repo.unfiltered()
505 505 cl = repo.changelog
506 506 self._phasesets = {phase: set() for phase in allphases}
507 507 lowerroots = set()
508 508 for phase in reversed(trackedphases):
509 509 roots = self._phaseroots[phase]
510 510 if roots:
511 511 ps = set(cl.descendants(roots))
512 512 for root in roots:
513 513 ps.add(root)
514 514 ps.difference_update(lowerroots)
515 515 lowerroots.update(ps)
516 516 self._phasesets[phase] = ps
517 517 self._loadedrevslen = len(cl)
518 518
519 519 def _ensure_phase_sets(self, repo: "localrepo.localrepository") -> None:
520 520 """ensure phase information is loaded in the object"""
521 521 assert repo.filtername is None
522 522 update = -1
523 523 cl = repo.changelog
524 524 cl_size = len(cl)
525 525 if self._phasesets is None:
526 526 update = 0
527 527 else:
528 528 if cl_size > self._loadedrevslen:
529 529 # check if an incremental update is worth it.
530 530 # note we need a tradeoff here because the whole logic is not
531 531 # stored and implemented in native code nd datastructure.
532 532 # Otherwise the incremental update woul always be a win.
533 533 missing = cl_size - self._loadedrevslen
534 534 if missing <= INCREMENTAL_PHASE_SETS_UPDATE_MAX_UPDATE:
535 535 update = self._loadedrevslen
536 536 else:
537 537 update = 0
538 538
539 539 if update == 0:
540 540 try:
541 541 res = self._getphaserevsnative(repo)
542 542 self._loadedrevslen, self._phasesets = res
543 543 except AttributeError:
544 544 self._computephaserevspure(repo)
545 545 assert self._loadedrevslen == len(repo.changelog)
546 546 elif update > 0:
547 547 # good candidate for native code
548 548 assert update == self._loadedrevslen
549 549 if self.hasnonpublicphases(repo):
550 550 start = self._loadedrevslen
551 551 get_phase = self.phase
552 552 rev_phases = [0] * missing
553 553 parents = cl.parentrevs
554 554 sets = {phase: set() for phase in self._phasesets}
555 555 for phase, roots in self._phaseroots.items():
556 556 # XXX should really store the max somewhere
557 557 for r in roots:
558 558 if r >= start:
559 559 rev_phases[r - start] = phase
560 560 for rev in range(start, cl_size):
561 561 phase = rev_phases[rev - start]
562 562 p1, p2 = parents(rev)
563 563 if p1 == nullrev:
564 564 p1_phase = public
565 565 elif p1 >= start:
566 566 p1_phase = rev_phases[p1 - start]
567 567 else:
568 568 p1_phase = max(phase, get_phase(repo, p1))
569 569 if p2 == nullrev:
570 570 p2_phase = public
571 571 elif p2 >= start:
572 572 p2_phase = rev_phases[p2 - start]
573 573 else:
574 574 p2_phase = max(phase, get_phase(repo, p2))
575 575 phase = max(phase, p1_phase, p2_phase)
576 576 if phase > public:
577 577 rev_phases[rev - start] = phase
578 578 sets[phase].add(rev)
579 579
580 580 # Be careful to preserve shallow-copied values: do not update
581 581 # phaseroots values, replace them.
582 582 for phase, extra in sets.items():
583 583 if extra:
584 584 self._phasesets[phase] = self._phasesets[phase] | extra
585 585 self._loadedrevslen = cl_size
586 586
587 587 def invalidate(self):
588 588 self._loadedrevslen = 0
589 589 self._phasesets = None
590 590
591 591 def phase(self, repo: "localrepo.localrepository", rev: int) -> int:
592 592 # We need a repo argument here to be able to build _phasesets
593 593 # if necessary. The repository instance is not stored in
594 594 # phasecache to avoid reference cycles. The changelog instance
595 595 # is not stored because it is a filecache() property and can
596 596 # be replaced without us being notified.
597 597 if rev == nullrev:
598 598 return public
599 599 if rev < nullrev:
600 600 raise ValueError(_(b'cannot lookup negative revision'))
601 601 # double check self._loadedrevslen to avoid an extra method call as
602 602 # python is slow for that.
603 603 if rev >= self._loadedrevslen:
604 604 self._ensure_phase_sets(repo.unfiltered())
605 605 for phase in trackedphases:
606 606 if rev in self._phasesets[phase]:
607 607 return phase
608 608 return public
609 609
610 610 def write(self, repo):
611 611 if not self.dirty:
612 612 return
613 613 f = repo.svfs(b'phaseroots', b'w', atomictemp=True, checkambig=True)
614 614 try:
615 615 self._write(repo.unfiltered(), f)
616 616 finally:
617 617 f.close()
618 618
619 619 def _write(self, repo, fp):
620 620 assert repo.filtername is None
621 621 to_node = repo.changelog.node
622 622 for phase, roots in self._phaseroots.items():
623 623 for r in sorted(roots):
624 624 h = to_node(r)
625 625 fp.write(b'%i %s\n' % (phase, hex(h)))
626 626 self.dirty = False
627 627
628 628 def _updateroots(self, repo, phase, newroots, tr, invalidate=True):
629 629 self._phaseroots[phase] = newroots
630 630 self.dirty = True
631 631 if invalidate:
632 632 self.invalidate()
633 633
634 634 assert repo.filtername is None
635 635 wrepo = weakref.ref(repo)
636 636
637 637 def tr_write(fp):
638 638 repo = wrepo()
639 639 assert repo is not None
640 640 self._write(repo, fp)
641 641
642 642 tr.addfilegenerator(b'phase', (b'phaseroots',), tr_write)
643 643 tr.hookargs[b'phases_moved'] = b'1'
644 644
645 645 def registernew(self, repo, tr, targetphase, revs):
646 646 repo = repo.unfiltered()
647 647 self._retractboundary(repo, tr, targetphase, [], revs=revs)
648 648 if tr is not None and b'phases' in tr.changes:
649 649 phasetracking = tr.changes[b'phases']
650 650 phase = self.phase
651 651 for rev in sorted(revs):
652 652 revphase = phase(repo, rev)
653 653 _trackphasechange(phasetracking, rev, None, revphase)
654 654 repo.invalidatevolatilesets()
655 655
656 656 def advanceboundary(
657 657 self, repo, tr, targetphase, nodes=None, revs=None, dryrun=None
658 658 ):
659 659 """Set all 'nodes' to phase 'targetphase'
660 660
661 661 Nodes with a phase lower than 'targetphase' are not affected.
662 662
663 663 If dryrun is True, no actions will be performed
664 664
665 665 Returns a set of revs whose phase is changed or should be changed
666 666 """
667 667 if targetphase == public and not self.hasnonpublicphases(repo):
668 668 return set()
669 669 repo = repo.unfiltered()
670 670 cl = repo.changelog
671 671 torev = cl.index.rev
672 672 # Be careful to preserve shallow-copied values: do not update
673 673 # phaseroots values, replace them.
674 674 new_revs = set()
675 675 if revs is not None:
676 676 new_revs.update(revs)
677 677 if nodes is not None:
678 678 new_revs.update(torev(node) for node in nodes)
679 679 if not new_revs: # bail out early to avoid the loadphaserevs call
680 680 return (
681 681 set()
682 682 ) # note: why do people call advanceboundary with nothing?
683 683
684 684 if tr is None:
685 685 phasetracking = None
686 686 else:
687 687 phasetracking = tr.changes.get(b'phases')
688 688
689 689 affectable_phases = sorted(
690 690 p for p in allphases if p > targetphase and self._phaseroots[p]
691 691 )
692 692 # filter revision already in the right phases
693 693 candidates = new_revs
694 694 new_revs = set()
695 695 self._ensure_phase_sets(repo)
696 696 for phase in affectable_phases:
697 697 found = candidates & self._phasesets[phase]
698 698 new_revs |= found
699 699 candidates -= found
700 700 if not candidates:
701 701 break
702 702 if not new_revs:
703 703 return set()
704 704
705 705 # search for affected high phase changesets and roots
706 706 seen = set(new_revs)
707 707 push = heapq.heappush
708 708 pop = heapq.heappop
709 709 parents = cl.parentrevs
710 710 get_phase = self.phase
711 711 changed = {} # set of revisions to be changed
712 712 # set of root deleted by this path
713 713 delroots = set()
714 714 new_roots = {p: set() for p in affectable_phases}
715 715 new_target_roots = set()
716 716 # revision to walk down
717 717 revs = [-r for r in new_revs]
718 718 heapq.heapify(revs)
719 719 while revs:
720 720 current = -pop(revs)
721 721 current_phase = get_phase(repo, current)
722 722 changed[current] = current_phase
723 723 p1, p2 = parents(current)
724 724 if p1 == nullrev:
725 725 p1_phase = public
726 726 else:
727 727 p1_phase = get_phase(repo, p1)
728 728 if p2 == nullrev:
729 729 p2_phase = public
730 730 else:
731 731 p2_phase = get_phase(repo, p2)
732 732 # do we have a root ?
733 733 if current_phase != p1_phase and current_phase != p2_phase:
734 734 # do not record phase, because we could have "duplicated"
735 735 # roots, were one root is shadowed by the very same roots of an
736 736 # higher phases
737 737 delroots.add(current)
738 738 # schedule a walk down if needed
739 739 if p1_phase > targetphase and p1 not in seen:
740 740 seen.add(p1)
741 741 push(revs, -p1)
742 742 if p2_phase > targetphase and p2 not in seen:
743 743 seen.add(p2)
744 744 push(revs, -p2)
745 745 if p1_phase < targetphase and p2_phase < targetphase:
746 746 new_target_roots.add(current)
747 747
748 748 # the last iteration was done with the smallest value
749 749 min_current = current
750 750 # do we have unwalked children that might be new roots
751 751 if (min_current + len(changed)) < len(cl):
752 752 for r in range(min_current, len(cl)):
753 753 if r in changed:
754 754 continue
755 755 phase = get_phase(repo, r)
756 756 if phase <= targetphase:
757 757 continue
758 758 p1, p2 = parents(r)
759 759 if not (p1 in changed or p2 in changed):
760 760 continue # not affected
761 761 if p1 != nullrev and p1 not in changed:
762 762 p1_phase = get_phase(repo, p1)
763 763 if p1_phase == phase:
764 764 continue # not a root
765 765 if p2 != nullrev and p2 not in changed:
766 766 p2_phase = get_phase(repo, p2)
767 767 if p2_phase == phase:
768 768 continue # not a root
769 769 new_roots[phase].add(r)
770 770
771 771 # apply the changes
772 772 if not dryrun:
773 773 for r, p in changed.items():
774 774 _trackphasechange(phasetracking, r, p, targetphase)
775 775 if targetphase > public:
776 776 self._phasesets[targetphase].update(changed)
777 777 for phase in affectable_phases:
778 778 roots = self._phaseroots[phase]
779 779 removed = roots & delroots
780 780 if removed or new_roots[phase]:
781 781 self._phasesets[phase].difference_update(changed)
782 782 # Be careful to preserve shallow-copied values: do not
783 783 # update phaseroots values, replace them.
784 784 final_roots = roots - delroots | new_roots[phase]
785 785 self._updateroots(
786 786 repo, phase, final_roots, tr, invalidate=False
787 787 )
788 788 if new_target_roots:
789 789 # Thanks for previous filtering, we can't replace existing
790 790 # roots
791 791 new_target_roots |= self._phaseroots[targetphase]
792 792 self._updateroots(
793 793 repo, targetphase, new_target_roots, tr, invalidate=False
794 794 )
795 795 repo.invalidatevolatilesets()
796 796 return changed
797 797
798 798 def retractboundary(self, repo, tr, targetphase, nodes):
799 799 if tr is None:
800 800 phasetracking = None
801 801 else:
802 802 phasetracking = tr.changes.get(b'phases')
803 803 repo = repo.unfiltered()
804 804 retracted = self._retractboundary(repo, tr, targetphase, nodes)
805 805 if retracted and phasetracking is not None:
806 806 for r, old_phase in sorted(retracted.items()):
807 807 _trackphasechange(phasetracking, r, old_phase, targetphase)
808 808 repo.invalidatevolatilesets()
809 809
810 810 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
811 811 if targetphase == public:
812 812 return {}
813 813 if (
814 814 targetphase == internal
815 815 and not supportinternal(repo)
816 816 or targetphase == archived
817 817 and not supportarchived(repo)
818 818 ):
819 819 name = phasenames[targetphase]
820 820 msg = b'this repository does not support the %s phase' % name
821 821 raise error.ProgrammingError(msg)
822 822 assert repo.filtername is None
823 823 cl = repo.changelog
824 824 torev = cl.index.rev
825 825 new_revs = set()
826 826 if revs is not None:
827 827 new_revs.update(revs)
828 828 if nodes is not None:
829 829 new_revs.update(torev(node) for node in nodes)
830 830 if not new_revs: # bail out early to avoid the loadphaserevs call
831 831 return {} # note: why do people call retractboundary with nothing ?
832 832
833 833 if nullrev in new_revs:
834 834 raise error.Abort(_(b'cannot change null revision phase'))
835 835
836 836 # Filter revision that are already in the right phase
837 837 self._ensure_phase_sets(repo)
838 838 for phase, revs in self._phasesets.items():
839 839 if phase >= targetphase:
840 840 new_revs -= revs
841 841 if not new_revs: # all revisions already in the right phases
842 842 return {}
843 843
844 844 # Compute change in phase roots by walking the graph
845 845 #
846 846 # note: If we had a cheap parent → children mapping we could do
847 847 # something even cheaper/more-bounded
848 848 #
849 849 # The idea would be to walk from item in new_revs stopping at
850 850 # descendant with phases >= target_phase.
851 851 #
852 852 # 1) This detect new_revs that are not new_roots (either already >=
853 853 # target_phase or reachable though another new_revs
854 854 # 2) This detect replaced current_roots as we reach them
855 855 # 3) This can avoid walking to the tip if we retract over a small
856 856 # branch.
857 857 #
858 858 # So instead, we do a variation of this, we walk from the smaller new
859 859 # revision to the tip to avoid missing any potential children.
860 860 #
861 861 # The following code would be a good candidate for native code… if only
862 862 # we could knew the phase of a changeset efficiently in native code.
863 863 parents = cl.parentrevs
864 864 phase = self.phase
865 865 new_roots = set() # roots added by this phases
866 866 changed_revs = {} # revision affected by this call
867 867 replaced_roots = set() # older roots replaced by this call
868 868 currentroots = self._phaseroots[targetphase]
869 869 start = min(new_revs)
870 870 end = len(cl)
871 871 rev_phases = [None] * (end - start)
872 872 for r in range(start, end):
873 873
874 874 # gather information about the current_rev
875 875 r_phase = phase(repo, r)
876 876 p_phase = None # phase inherited from parents
877 877 p1, p2 = parents(r)
878 878 if p1 >= start:
879 879 p1_phase = rev_phases[p1 - start]
880 880 if p1_phase is not None:
881 881 p_phase = p1_phase
882 882 if p2 >= start:
883 883 p2_phase = rev_phases[p2 - start]
884 884 if p2_phase is not None:
885 885 if p_phase is not None:
886 886 p_phase = max(p_phase, p2_phase)
887 887 else:
888 888 p_phase = p2_phase
889 889
890 890 # assess the situation
891 891 if r in new_revs and r_phase < targetphase:
892 892 if p_phase is None or p_phase < targetphase:
893 893 new_roots.add(r)
894 894 rev_phases[r - start] = targetphase
895 895 changed_revs[r] = r_phase
896 896 elif p_phase is None:
897 897 rev_phases[r - start] = r_phase
898 898 else:
899 899 if p_phase > r_phase:
900 900 rev_phases[r - start] = p_phase
901 901 else:
902 902 rev_phases[r - start] = r_phase
903 903 if p_phase == targetphase:
904 904 if p_phase > r_phase:
905 905 changed_revs[r] = r_phase
906 906 elif r in currentroots:
907 907 replaced_roots.add(r)
908 908 sets = self._phasesets
909 909 sets[targetphase].update(changed_revs)
910 for r, old in changed_revs.items():
911 if old > public:
912 sets[old].discard(r)
910 if targetphase > draft:
911 for r, old in changed_revs.items():
912 if old > public:
913 sets[old].discard(r)
913 914
914 915 if new_roots:
915 916 assert changed_revs
916 917
917 918 final_roots = new_roots | currentroots - replaced_roots
918 919 self._updateroots(
919 920 repo,
920 921 targetphase,
921 922 final_roots,
922 923 tr,
923 924 invalidate=False,
924 925 )
925 926 if targetphase > 1:
926 927 retracted = set(changed_revs)
927 928 for lower_phase in range(1, targetphase):
928 929 lower_roots = self._phaseroots.get(lower_phase)
929 930 if lower_roots is None:
930 931 continue
931 932 if lower_roots & retracted:
932 933 simpler_roots = lower_roots - retracted
933 934 self._updateroots(
934 935 repo,
935 936 lower_phase,
936 937 simpler_roots,
937 938 tr,
938 939 invalidate=False,
939 940 )
940 941 return changed_revs
941 942 else:
942 943 assert not changed_revs
943 944 assert not replaced_roots
944 945 return {}
945 946
946 947 def register_strip(
947 948 self,
948 949 repo,
949 950 tr,
950 951 strip_rev: int,
951 952 ):
952 953 """announce a strip to the phase cache
953 954
954 955 Any roots higher than the stripped revision should be dropped.
955 956 """
956 957 for targetphase, roots in list(self._phaseroots.items()):
957 958 filtered = {r for r in roots if r >= strip_rev}
958 959 if filtered:
959 960 self._updateroots(repo, targetphase, roots - filtered, tr)
960 961 self.invalidate()
961 962
962 963
963 964 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
964 965 """Add nodes to a phase changing other nodes phases if necessary.
965 966
966 967 This function move boundary *forward* this means that all nodes
967 968 are set in the target phase or kept in a *lower* phase.
968 969
969 970 Simplify boundary to contains phase roots only.
970 971
971 972 If dryrun is True, no actions will be performed
972 973
973 974 Returns a set of revs whose phase is changed or should be changed
974 975 """
975 976 if revs is None:
976 977 revs = []
977 978 phcache = repo._phasecache.copy()
978 979 changes = phcache.advanceboundary(
979 980 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
980 981 )
981 982 if not dryrun:
982 983 repo._phasecache.replace(phcache)
983 984 return changes
984 985
985 986
986 987 def retractboundary(repo, tr, targetphase, nodes):
987 988 """Set nodes back to a phase changing other nodes phases if
988 989 necessary.
989 990
990 991 This function move boundary *backward* this means that all nodes
991 992 are set in the target phase or kept in a *higher* phase.
992 993
993 994 Simplify boundary to contains phase roots only."""
994 995 phcache = repo._phasecache.copy()
995 996 phcache.retractboundary(repo, tr, targetphase, nodes)
996 997 repo._phasecache.replace(phcache)
997 998
998 999
999 1000 def registernew(repo, tr, targetphase, revs):
1000 1001 """register a new revision and its phase
1001 1002
1002 1003 Code adding revisions to the repository should use this function to
1003 1004 set new changeset in their target phase (or higher).
1004 1005 """
1005 1006 phcache = repo._phasecache.copy()
1006 1007 phcache.registernew(repo, tr, targetphase, revs)
1007 1008 repo._phasecache.replace(phcache)
1008 1009
1009 1010
1010 1011 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
1011 1012 """List phases root for serialization over pushkey"""
1012 1013 # Use ordered dictionary so behavior is deterministic.
1013 1014 keys = util.sortdict()
1014 1015 value = b'%i' % draft
1015 1016 cl = repo.unfiltered().changelog
1016 1017 to_node = cl.node
1017 1018 for root in repo._phasecache._phaseroots[draft]:
1018 1019 if repo._phasecache.phase(repo, root) <= draft:
1019 1020 keys[hex(to_node(root))] = value
1020 1021
1021 1022 if repo.publishing():
1022 1023 # Add an extra data to let remote know we are a publishing
1023 1024 # repo. Publishing repo can't just pretend they are old repo.
1024 1025 # When pushing to a publishing repo, the client still need to
1025 1026 # push phase boundary
1026 1027 #
1027 1028 # Push do not only push changeset. It also push phase data.
1028 1029 # New phase data may apply to common changeset which won't be
1029 1030 # push (as they are common). Here is a very simple example:
1030 1031 #
1031 1032 # 1) repo A push changeset X as draft to repo B
1032 1033 # 2) repo B make changeset X public
1033 1034 # 3) repo B push to repo A. X is not pushed but the data that
1034 1035 # X as now public should
1035 1036 #
1036 1037 # The server can't handle it on it's own as it has no idea of
1037 1038 # client phase data.
1038 1039 keys[b'publishing'] = b'True'
1039 1040 return keys
1040 1041
1041 1042
1042 1043 def pushphase(
1043 1044 repo: "localrepo.localrepository",
1044 1045 nhex: bytes,
1045 1046 oldphasestr: bytes,
1046 1047 newphasestr: bytes,
1047 1048 ) -> bool:
1048 1049 """List phases root for serialization over pushkey"""
1049 1050 repo = repo.unfiltered()
1050 1051 with repo.lock():
1051 1052 currentphase = repo[nhex].phase()
1052 1053 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
1053 1054 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
1054 1055 if currentphase == oldphase and newphase < oldphase:
1055 1056 with repo.transaction(b'pushkey-phase') as tr:
1056 1057 advanceboundary(repo, tr, newphase, [bin(nhex)])
1057 1058 return True
1058 1059 elif currentphase == newphase:
1059 1060 # raced, but got correct result
1060 1061 return True
1061 1062 else:
1062 1063 return False
1063 1064
1064 1065
1065 1066 def subsetphaseheads(repo, subset):
1066 1067 """Finds the phase heads for a subset of a history
1067 1068
1068 1069 Returns a list indexed by phase number where each item is a list of phase
1069 1070 head nodes.
1070 1071 """
1071 1072 cl = repo.changelog
1072 1073
1073 1074 headsbyphase = {i: [] for i in allphases}
1074 1075 for phase in allphases:
1075 1076 revset = b"heads(%%ln & _phase(%d))" % phase
1076 1077 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
1077 1078 return headsbyphase
1078 1079
1079 1080
1080 1081 def updatephases(repo, trgetter, headsbyphase):
1081 1082 """Updates the repo with the given phase heads"""
1082 1083 # Now advance phase boundaries of all phases
1083 1084 #
1084 1085 # run the update (and fetch transaction) only if there are actually things
1085 1086 # to update. This avoid creating empty transaction during no-op operation.
1086 1087
1087 1088 for phase in allphases:
1088 1089 revset = b'%ln - _phase(%s)'
1089 1090 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
1090 1091 if heads:
1091 1092 advanceboundary(repo, trgetter(), phase, heads)
1092 1093
1093 1094
1094 1095 def analyzeremotephases(repo, subset, roots):
1095 1096 """Compute phases heads and root in a subset of node from root dict
1096 1097
1097 1098 * subset is heads of the subset
1098 1099 * roots is {<nodeid> => phase} mapping. key and value are string.
1099 1100
1100 1101 Accept unknown element input
1101 1102 """
1102 1103 repo = repo.unfiltered()
1103 1104 # build list from dictionary
1104 1105 draftroots = []
1105 1106 has_node = repo.changelog.index.has_node # to filter unknown nodes
1106 1107 for nhex, phase in roots.items():
1107 1108 if nhex == b'publishing': # ignore data related to publish option
1108 1109 continue
1109 1110 node = bin(nhex)
1110 1111 phase = int(phase)
1111 1112 if phase == public:
1112 1113 if node != repo.nullid:
1113 1114 repo.ui.warn(
1114 1115 _(
1115 1116 b'ignoring inconsistent public root'
1116 1117 b' from remote: %s\n'
1117 1118 )
1118 1119 % nhex
1119 1120 )
1120 1121 elif phase == draft:
1121 1122 if has_node(node):
1122 1123 draftroots.append(node)
1123 1124 else:
1124 1125 repo.ui.warn(
1125 1126 _(b'ignoring unexpected root from remote: %i %s\n')
1126 1127 % (phase, nhex)
1127 1128 )
1128 1129 # compute heads
1129 1130 publicheads = newheads(repo, subset, draftroots)
1130 1131 return publicheads, draftroots
1131 1132
1132 1133
1133 1134 class remotephasessummary:
1134 1135 """summarize phase information on the remote side
1135 1136
1136 1137 :publishing: True is the remote is publishing
1137 1138 :publicheads: list of remote public phase heads (nodes)
1138 1139 :draftheads: list of remote draft phase heads (nodes)
1139 1140 :draftroots: list of remote draft phase root (nodes)
1140 1141 """
1141 1142
1142 1143 def __init__(self, repo, remotesubset, remoteroots):
1143 1144 unfi = repo.unfiltered()
1144 1145 self._allremoteroots = remoteroots
1145 1146
1146 1147 self.publishing = remoteroots.get(b'publishing', False)
1147 1148
1148 1149 ana = analyzeremotephases(repo, remotesubset, remoteroots)
1149 1150 self.publicheads, self.draftroots = ana
1150 1151 # Get the list of all "heads" revs draft on remote
1151 1152 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
1152 1153 self.draftheads = [c.node() for c in dheads]
1153 1154
1154 1155
1155 1156 def newheads(repo, heads, roots):
1156 1157 """compute new head of a subset minus another
1157 1158
1158 1159 * `heads`: define the first subset
1159 1160 * `roots`: define the second we subtract from the first"""
1160 1161 # prevent an import cycle
1161 1162 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
1162 1163 from . import dagop
1163 1164
1164 1165 repo = repo.unfiltered()
1165 1166 cl = repo.changelog
1166 1167 rev = cl.index.get_rev
1167 1168 if not roots:
1168 1169 return heads
1169 1170 if not heads or heads == [repo.nullid]:
1170 1171 return []
1171 1172 # The logic operated on revisions, convert arguments early for convenience
1172 1173 new_heads = {rev(n) for n in heads if n != repo.nullid}
1173 1174 roots = [rev(n) for n in roots]
1174 1175 # compute the area we need to remove
1175 1176 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
1176 1177 # heads in the area are no longer heads
1177 1178 new_heads.difference_update(affected_zone)
1178 1179 # revisions in the area have children outside of it,
1179 1180 # They might be new heads
1180 1181 candidates = repo.revs(
1181 1182 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
1182 1183 )
1183 1184 candidates -= affected_zone
1184 1185 if new_heads or candidates:
1185 1186 # remove candidate that are ancestors of other heads
1186 1187 new_heads.update(candidates)
1187 1188 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
1188 1189 pruned = dagop.reachableroots(repo, candidates, prunestart)
1189 1190 new_heads.difference_update(pruned)
1190 1191
1191 1192 return pycompat.maplist(cl.node, sorted(new_heads))
1192 1193
1193 1194
1194 1195 def newcommitphase(ui: "uimod.ui") -> int:
1195 1196 """helper to get the target phase of new commit
1196 1197
1197 1198 Handle all possible values for the phases.new-commit options.
1198 1199
1199 1200 """
1200 1201 v = ui.config(b'phases', b'new-commit')
1201 1202 try:
1202 1203 return phasenumber2[v]
1203 1204 except KeyError:
1204 1205 raise error.ConfigError(
1205 1206 _(b"phases.new-commit: not a valid phase name ('%s')") % v
1206 1207 )
1207 1208
1208 1209
1209 1210 def hassecret(repo: "localrepo.localrepository") -> bool:
1210 1211 """utility function that check if a repo have any secret changeset."""
1211 1212 return bool(repo._phasecache._phaseroots[secret])
1212 1213
1213 1214
1214 1215 def preparehookargs(
1215 1216 node: bytes,
1216 1217 old: Optional[int],
1217 1218 new: Optional[int],
1218 1219 ) -> Dict[bytes, bytes]:
1219 1220 if old is None:
1220 1221 old = b''
1221 1222 else:
1222 1223 old = phasenames[old]
1223 1224 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