##// END OF EJS Templates
phases: avoid N² behavior in `advanceboundary`...
marmoute -
r52398:c9ceb4f6 6.7 stable
parent child Browse files
Show More
@@ -1,1220 +1,1223 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 seen = set(new_revs)
706 707 push = heapq.heappush
707 708 pop = heapq.heappop
708 709 parents = cl.parentrevs
709 710 get_phase = self.phase
710 711 changed = {} # set of revisions to be changed
711 712 # set of root deleted by this path
712 713 delroots = set()
713 714 new_roots = {p: set() for p in affectable_phases}
714 715 new_target_roots = set()
715 716 # revision to walk down
716 717 revs = [-r for r in new_revs]
717 718 heapq.heapify(revs)
718 719 while revs:
719 720 current = -pop(revs)
720 721 current_phase = get_phase(repo, current)
721 722 changed[current] = current_phase
722 723 p1, p2 = parents(current)
723 724 if p1 == nullrev:
724 725 p1_phase = public
725 726 else:
726 727 p1_phase = get_phase(repo, p1)
727 728 if p2 == nullrev:
728 729 p2_phase = public
729 730 else:
730 731 p2_phase = get_phase(repo, p2)
731 732 # do we have a root ?
732 733 if current_phase != p1_phase and current_phase != p2_phase:
733 734 # do not record phase, because we could have "duplicated"
734 735 # roots, were one root is shadowed by the very same roots of an
735 736 # higher phases
736 737 delroots.add(current)
737 738 # schedule a walk down if needed
738 if p1_phase > targetphase:
739 if p1_phase > targetphase and p1 not in seen:
740 seen.add(p1)
739 741 push(revs, -p1)
740 if p2_phase > targetphase:
742 if p2_phase > targetphase and p2 not in seen:
743 seen.add(p2)
741 744 push(revs, -p2)
742 745 if p1_phase < targetphase and p2_phase < targetphase:
743 746 new_target_roots.add(current)
744 747
745 748 # the last iteration was done with the smallest value
746 749 min_current = current
747 750 # do we have unwalked children that might be new roots
748 751 if (min_current + len(changed)) < len(cl):
749 752 for r in range(min_current, len(cl)):
750 753 if r in changed:
751 754 continue
752 755 phase = get_phase(repo, r)
753 756 if phase <= targetphase:
754 757 continue
755 758 p1, p2 = parents(r)
756 759 if not (p1 in changed or p2 in changed):
757 760 continue # not affected
758 761 if p1 != nullrev and p1 not in changed:
759 762 p1_phase = get_phase(repo, p1)
760 763 if p1_phase == phase:
761 764 continue # not a root
762 765 if p2 != nullrev and p2 not in changed:
763 766 p2_phase = get_phase(repo, p2)
764 767 if p2_phase == phase:
765 768 continue # not a root
766 769 new_roots[phase].add(r)
767 770
768 771 # apply the changes
769 772 if not dryrun:
770 773 for r, p in changed.items():
771 774 _trackphasechange(phasetracking, r, p, targetphase)
772 775 if targetphase > public:
773 776 self._phasesets[targetphase].update(changed)
774 777 for phase in affectable_phases:
775 778 roots = self._phaseroots[phase]
776 779 removed = roots & delroots
777 780 if removed or new_roots[phase]:
778 781 self._phasesets[phase].difference_update(changed)
779 782 # Be careful to preserve shallow-copied values: do not
780 783 # update phaseroots values, replace them.
781 784 final_roots = roots - delroots | new_roots[phase]
782 785 self._updateroots(
783 786 repo, phase, final_roots, tr, invalidate=False
784 787 )
785 788 if new_target_roots:
786 789 # Thanks for previous filtering, we can't replace existing
787 790 # roots
788 791 new_target_roots |= self._phaseroots[targetphase]
789 792 self._updateroots(
790 793 repo, targetphase, new_target_roots, tr, invalidate=False
791 794 )
792 795 repo.invalidatevolatilesets()
793 796 return changed
794 797
795 798 def retractboundary(self, repo, tr, targetphase, nodes):
796 799 if tr is None:
797 800 phasetracking = None
798 801 else:
799 802 phasetracking = tr.changes.get(b'phases')
800 803 repo = repo.unfiltered()
801 804 retracted = self._retractboundary(repo, tr, targetphase, nodes)
802 805 if retracted and phasetracking is not None:
803 806 for r, old_phase in sorted(retracted.items()):
804 807 _trackphasechange(phasetracking, r, old_phase, targetphase)
805 808 repo.invalidatevolatilesets()
806 809
807 810 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
808 811 if targetphase == public:
809 812 return {}
810 813 if (
811 814 targetphase == internal
812 815 and not supportinternal(repo)
813 816 or targetphase == archived
814 817 and not supportarchived(repo)
815 818 ):
816 819 name = phasenames[targetphase]
817 820 msg = b'this repository does not support the %s phase' % name
818 821 raise error.ProgrammingError(msg)
819 822 assert repo.filtername is None
820 823 cl = repo.changelog
821 824 torev = cl.index.rev
822 825 new_revs = set()
823 826 if revs is not None:
824 827 new_revs.update(revs)
825 828 if nodes is not None:
826 829 new_revs.update(torev(node) for node in nodes)
827 830 if not new_revs: # bail out early to avoid the loadphaserevs call
828 831 return {} # note: why do people call retractboundary with nothing ?
829 832
830 833 if nullrev in new_revs:
831 834 raise error.Abort(_(b'cannot change null revision phase'))
832 835
833 836 # Filter revision that are already in the right phase
834 837 self._ensure_phase_sets(repo)
835 838 for phase, revs in self._phasesets.items():
836 839 if phase >= targetphase:
837 840 new_revs -= revs
838 841 if not new_revs: # all revisions already in the right phases
839 842 return {}
840 843
841 844 # Compute change in phase roots by walking the graph
842 845 #
843 846 # note: If we had a cheap parent → children mapping we could do
844 847 # something even cheaper/more-bounded
845 848 #
846 849 # The idea would be to walk from item in new_revs stopping at
847 850 # descendant with phases >= target_phase.
848 851 #
849 852 # 1) This detect new_revs that are not new_roots (either already >=
850 853 # target_phase or reachable though another new_revs
851 854 # 2) This detect replaced current_roots as we reach them
852 855 # 3) This can avoid walking to the tip if we retract over a small
853 856 # branch.
854 857 #
855 858 # So instead, we do a variation of this, we walk from the smaller new
856 859 # revision to the tip to avoid missing any potential children.
857 860 #
858 861 # The following code would be a good candidate for native code… if only
859 862 # we could knew the phase of a changeset efficiently in native code.
860 863 parents = cl.parentrevs
861 864 phase = self.phase
862 865 new_roots = set() # roots added by this phases
863 866 changed_revs = {} # revision affected by this call
864 867 replaced_roots = set() # older roots replaced by this call
865 868 currentroots = self._phaseroots[targetphase]
866 869 start = min(new_revs)
867 870 end = len(cl)
868 871 rev_phases = [None] * (end - start)
869 872 for r in range(start, end):
870 873
871 874 # gather information about the current_rev
872 875 r_phase = phase(repo, r)
873 876 p_phase = None # phase inherited from parents
874 877 p1, p2 = parents(r)
875 878 if p1 >= start:
876 879 p1_phase = rev_phases[p1 - start]
877 880 if p1_phase is not None:
878 881 p_phase = p1_phase
879 882 if p2 >= start:
880 883 p2_phase = rev_phases[p2 - start]
881 884 if p2_phase is not None:
882 885 if p_phase is not None:
883 886 p_phase = max(p_phase, p2_phase)
884 887 else:
885 888 p_phase = p2_phase
886 889
887 890 # assess the situation
888 891 if r in new_revs and r_phase < targetphase:
889 892 if p_phase is None or p_phase < targetphase:
890 893 new_roots.add(r)
891 894 rev_phases[r - start] = targetphase
892 895 changed_revs[r] = r_phase
893 896 elif p_phase is None:
894 897 rev_phases[r - start] = r_phase
895 898 else:
896 899 if p_phase > r_phase:
897 900 rev_phases[r - start] = p_phase
898 901 else:
899 902 rev_phases[r - start] = r_phase
900 903 if p_phase == targetphase:
901 904 if p_phase > r_phase:
902 905 changed_revs[r] = r_phase
903 906 elif r in currentroots:
904 907 replaced_roots.add(r)
905 908 sets = self._phasesets
906 909 sets[targetphase].update(changed_revs)
907 910 for r, old in changed_revs.items():
908 911 if old > public:
909 912 sets[old].discard(r)
910 913
911 914 if new_roots:
912 915 assert changed_revs
913 916
914 917 final_roots = new_roots | currentroots - replaced_roots
915 918 self._updateroots(
916 919 repo,
917 920 targetphase,
918 921 final_roots,
919 922 tr,
920 923 invalidate=False,
921 924 )
922 925 if targetphase > 1:
923 926 retracted = set(changed_revs)
924 927 for lower_phase in range(1, targetphase):
925 928 lower_roots = self._phaseroots.get(lower_phase)
926 929 if lower_roots is None:
927 930 continue
928 931 if lower_roots & retracted:
929 932 simpler_roots = lower_roots - retracted
930 933 self._updateroots(
931 934 repo,
932 935 lower_phase,
933 936 simpler_roots,
934 937 tr,
935 938 invalidate=False,
936 939 )
937 940 return changed_revs
938 941 else:
939 942 assert not changed_revs
940 943 assert not replaced_roots
941 944 return {}
942 945
943 946 def register_strip(
944 947 self,
945 948 repo,
946 949 tr,
947 950 strip_rev: int,
948 951 ):
949 952 """announce a strip to the phase cache
950 953
951 954 Any roots higher than the stripped revision should be dropped.
952 955 """
953 956 for targetphase, roots in list(self._phaseroots.items()):
954 957 filtered = {r for r in roots if r >= strip_rev}
955 958 if filtered:
956 959 self._updateroots(repo, targetphase, roots - filtered, tr)
957 960 self.invalidate()
958 961
959 962
960 963 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
961 964 """Add nodes to a phase changing other nodes phases if necessary.
962 965
963 966 This function move boundary *forward* this means that all nodes
964 967 are set in the target phase or kept in a *lower* phase.
965 968
966 969 Simplify boundary to contains phase roots only.
967 970
968 971 If dryrun is True, no actions will be performed
969 972
970 973 Returns a set of revs whose phase is changed or should be changed
971 974 """
972 975 if revs is None:
973 976 revs = []
974 977 phcache = repo._phasecache.copy()
975 978 changes = phcache.advanceboundary(
976 979 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
977 980 )
978 981 if not dryrun:
979 982 repo._phasecache.replace(phcache)
980 983 return changes
981 984
982 985
983 986 def retractboundary(repo, tr, targetphase, nodes):
984 987 """Set nodes back to a phase changing other nodes phases if
985 988 necessary.
986 989
987 990 This function move boundary *backward* this means that all nodes
988 991 are set in the target phase or kept in a *higher* phase.
989 992
990 993 Simplify boundary to contains phase roots only."""
991 994 phcache = repo._phasecache.copy()
992 995 phcache.retractboundary(repo, tr, targetphase, nodes)
993 996 repo._phasecache.replace(phcache)
994 997
995 998
996 999 def registernew(repo, tr, targetphase, revs):
997 1000 """register a new revision and its phase
998 1001
999 1002 Code adding revisions to the repository should use this function to
1000 1003 set new changeset in their target phase (or higher).
1001 1004 """
1002 1005 phcache = repo._phasecache.copy()
1003 1006 phcache.registernew(repo, tr, targetphase, revs)
1004 1007 repo._phasecache.replace(phcache)
1005 1008
1006 1009
1007 1010 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
1008 1011 """List phases root for serialization over pushkey"""
1009 1012 # Use ordered dictionary so behavior is deterministic.
1010 1013 keys = util.sortdict()
1011 1014 value = b'%i' % draft
1012 1015 cl = repo.unfiltered().changelog
1013 1016 to_node = cl.node
1014 1017 for root in repo._phasecache._phaseroots[draft]:
1015 1018 if repo._phasecache.phase(repo, root) <= draft:
1016 1019 keys[hex(to_node(root))] = value
1017 1020
1018 1021 if repo.publishing():
1019 1022 # Add an extra data to let remote know we are a publishing
1020 1023 # repo. Publishing repo can't just pretend they are old repo.
1021 1024 # When pushing to a publishing repo, the client still need to
1022 1025 # push phase boundary
1023 1026 #
1024 1027 # Push do not only push changeset. It also push phase data.
1025 1028 # New phase data may apply to common changeset which won't be
1026 1029 # push (as they are common). Here is a very simple example:
1027 1030 #
1028 1031 # 1) repo A push changeset X as draft to repo B
1029 1032 # 2) repo B make changeset X public
1030 1033 # 3) repo B push to repo A. X is not pushed but the data that
1031 1034 # X as now public should
1032 1035 #
1033 1036 # The server can't handle it on it's own as it has no idea of
1034 1037 # client phase data.
1035 1038 keys[b'publishing'] = b'True'
1036 1039 return keys
1037 1040
1038 1041
1039 1042 def pushphase(
1040 1043 repo: "localrepo.localrepository",
1041 1044 nhex: bytes,
1042 1045 oldphasestr: bytes,
1043 1046 newphasestr: bytes,
1044 1047 ) -> bool:
1045 1048 """List phases root for serialization over pushkey"""
1046 1049 repo = repo.unfiltered()
1047 1050 with repo.lock():
1048 1051 currentphase = repo[nhex].phase()
1049 1052 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
1050 1053 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
1051 1054 if currentphase == oldphase and newphase < oldphase:
1052 1055 with repo.transaction(b'pushkey-phase') as tr:
1053 1056 advanceboundary(repo, tr, newphase, [bin(nhex)])
1054 1057 return True
1055 1058 elif currentphase == newphase:
1056 1059 # raced, but got correct result
1057 1060 return True
1058 1061 else:
1059 1062 return False
1060 1063
1061 1064
1062 1065 def subsetphaseheads(repo, subset):
1063 1066 """Finds the phase heads for a subset of a history
1064 1067
1065 1068 Returns a list indexed by phase number where each item is a list of phase
1066 1069 head nodes.
1067 1070 """
1068 1071 cl = repo.changelog
1069 1072
1070 1073 headsbyphase = {i: [] for i in allphases}
1071 1074 for phase in allphases:
1072 1075 revset = b"heads(%%ln & _phase(%d))" % phase
1073 1076 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
1074 1077 return headsbyphase
1075 1078
1076 1079
1077 1080 def updatephases(repo, trgetter, headsbyphase):
1078 1081 """Updates the repo with the given phase heads"""
1079 1082 # Now advance phase boundaries of all phases
1080 1083 #
1081 1084 # run the update (and fetch transaction) only if there are actually things
1082 1085 # to update. This avoid creating empty transaction during no-op operation.
1083 1086
1084 1087 for phase in allphases:
1085 1088 revset = b'%ln - _phase(%s)'
1086 1089 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
1087 1090 if heads:
1088 1091 advanceboundary(repo, trgetter(), phase, heads)
1089 1092
1090 1093
1091 1094 def analyzeremotephases(repo, subset, roots):
1092 1095 """Compute phases heads and root in a subset of node from root dict
1093 1096
1094 1097 * subset is heads of the subset
1095 1098 * roots is {<nodeid> => phase} mapping. key and value are string.
1096 1099
1097 1100 Accept unknown element input
1098 1101 """
1099 1102 repo = repo.unfiltered()
1100 1103 # build list from dictionary
1101 1104 draftroots = []
1102 1105 has_node = repo.changelog.index.has_node # to filter unknown nodes
1103 1106 for nhex, phase in roots.items():
1104 1107 if nhex == b'publishing': # ignore data related to publish option
1105 1108 continue
1106 1109 node = bin(nhex)
1107 1110 phase = int(phase)
1108 1111 if phase == public:
1109 1112 if node != repo.nullid:
1110 1113 repo.ui.warn(
1111 1114 _(
1112 1115 b'ignoring inconsistent public root'
1113 1116 b' from remote: %s\n'
1114 1117 )
1115 1118 % nhex
1116 1119 )
1117 1120 elif phase == draft:
1118 1121 if has_node(node):
1119 1122 draftroots.append(node)
1120 1123 else:
1121 1124 repo.ui.warn(
1122 1125 _(b'ignoring unexpected root from remote: %i %s\n')
1123 1126 % (phase, nhex)
1124 1127 )
1125 1128 # compute heads
1126 1129 publicheads = newheads(repo, subset, draftroots)
1127 1130 return publicheads, draftroots
1128 1131
1129 1132
1130 1133 class remotephasessummary:
1131 1134 """summarize phase information on the remote side
1132 1135
1133 1136 :publishing: True is the remote is publishing
1134 1137 :publicheads: list of remote public phase heads (nodes)
1135 1138 :draftheads: list of remote draft phase heads (nodes)
1136 1139 :draftroots: list of remote draft phase root (nodes)
1137 1140 """
1138 1141
1139 1142 def __init__(self, repo, remotesubset, remoteroots):
1140 1143 unfi = repo.unfiltered()
1141 1144 self._allremoteroots = remoteroots
1142 1145
1143 1146 self.publishing = remoteroots.get(b'publishing', False)
1144 1147
1145 1148 ana = analyzeremotephases(repo, remotesubset, remoteroots)
1146 1149 self.publicheads, self.draftroots = ana
1147 1150 # Get the list of all "heads" revs draft on remote
1148 1151 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
1149 1152 self.draftheads = [c.node() for c in dheads]
1150 1153
1151 1154
1152 1155 def newheads(repo, heads, roots):
1153 1156 """compute new head of a subset minus another
1154 1157
1155 1158 * `heads`: define the first subset
1156 1159 * `roots`: define the second we subtract from the first"""
1157 1160 # prevent an import cycle
1158 1161 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
1159 1162 from . import dagop
1160 1163
1161 1164 repo = repo.unfiltered()
1162 1165 cl = repo.changelog
1163 1166 rev = cl.index.get_rev
1164 1167 if not roots:
1165 1168 return heads
1166 1169 if not heads or heads == [repo.nullid]:
1167 1170 return []
1168 1171 # The logic operated on revisions, convert arguments early for convenience
1169 1172 new_heads = {rev(n) for n in heads if n != repo.nullid}
1170 1173 roots = [rev(n) for n in roots]
1171 1174 # compute the area we need to remove
1172 1175 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
1173 1176 # heads in the area are no longer heads
1174 1177 new_heads.difference_update(affected_zone)
1175 1178 # revisions in the area have children outside of it,
1176 1179 # They might be new heads
1177 1180 candidates = repo.revs(
1178 1181 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
1179 1182 )
1180 1183 candidates -= affected_zone
1181 1184 if new_heads or candidates:
1182 1185 # remove candidate that are ancestors of other heads
1183 1186 new_heads.update(candidates)
1184 1187 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
1185 1188 pruned = dagop.reachableroots(repo, candidates, prunestart)
1186 1189 new_heads.difference_update(pruned)
1187 1190
1188 1191 return pycompat.maplist(cl.node, sorted(new_heads))
1189 1192
1190 1193
1191 1194 def newcommitphase(ui: "uimod.ui") -> int:
1192 1195 """helper to get the target phase of new commit
1193 1196
1194 1197 Handle all possible values for the phases.new-commit options.
1195 1198
1196 1199 """
1197 1200 v = ui.config(b'phases', b'new-commit')
1198 1201 try:
1199 1202 return phasenumber2[v]
1200 1203 except KeyError:
1201 1204 raise error.ConfigError(
1202 1205 _(b"phases.new-commit: not a valid phase name ('%s')") % v
1203 1206 )
1204 1207
1205 1208
1206 1209 def hassecret(repo: "localrepo.localrepository") -> bool:
1207 1210 """utility function that check if a repo have any secret changeset."""
1208 1211 return bool(repo._phasecache._phaseroots[secret])
1209 1212
1210 1213
1211 1214 def preparehookargs(
1212 1215 node: bytes,
1213 1216 old: Optional[int],
1214 1217 new: Optional[int],
1215 1218 ) -> Dict[bytes, bytes]:
1216 1219 if old is None:
1217 1220 old = b''
1218 1221 else:
1219 1222 old = phasenames[old]
1220 1223 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