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