##// END OF EJS Templates
phases: fast path public phase advance when everything is public...
marmoute -
r52301:330d7475 default
parent child Browse files
Show More
@@ -1,1002 +1,1004 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 struct
105 105 import typing
106 106 import weakref
107 107
108 108 from typing import (
109 109 Any,
110 110 Callable,
111 111 Dict,
112 112 Iterable,
113 113 List,
114 114 Optional,
115 115 Set,
116 116 Tuple,
117 117 )
118 118
119 119 from .i18n import _
120 120 from .node import (
121 121 bin,
122 122 hex,
123 123 nullrev,
124 124 short,
125 125 wdirrev,
126 126 )
127 127 from . import (
128 128 error,
129 129 pycompat,
130 130 requirements,
131 131 smartset,
132 132 txnutil,
133 133 util,
134 134 )
135 135
136 136 Phaseroots = Dict[int, Set[int]]
137 137
138 138 if typing.TYPE_CHECKING:
139 139 from . import (
140 140 localrepo,
141 141 ui as uimod,
142 142 )
143 143
144 144 # keeps pyflakes happy
145 145 assert [uimod]
146 146
147 147 Phasedefaults = List[
148 148 Callable[[localrepo.localrepository, Phaseroots], Phaseroots]
149 149 ]
150 150
151 151
152 152 _fphasesentry = struct.Struct(b'>i20s')
153 153
154 154 # record phase index
155 155 public: int = 0
156 156 draft: int = 1
157 157 secret: int = 2
158 158 archived = 32 # non-continuous for compatibility
159 159 internal = 96 # non-continuous for compatibility
160 160 allphases = (public, draft, secret, archived, internal)
161 161 trackedphases = (draft, secret, archived, internal)
162 162 not_public_phases = trackedphases
163 163 # record phase names
164 164 cmdphasenames = [b'public', b'draft', b'secret'] # known to `hg phase` command
165 165 phasenames = dict(enumerate(cmdphasenames))
166 166 phasenames[archived] = b'archived'
167 167 phasenames[internal] = b'internal'
168 168 # map phase name to phase number
169 169 phasenumber = {name: phase for phase, name in phasenames.items()}
170 170 # like phasenumber, but also include maps for the numeric and binary
171 171 # phase number to the phase number
172 172 phasenumber2 = phasenumber.copy()
173 173 phasenumber2.update({phase: phase for phase in phasenames})
174 174 phasenumber2.update({b'%i' % phase: phase for phase in phasenames})
175 175 # record phase property
176 176 mutablephases = (draft, secret, archived, internal)
177 177 relevant_mutable_phases = (draft, secret) # could be obsolete or unstable
178 178 remotehiddenphases = (secret, archived, internal)
179 179 localhiddenphases = (internal, archived)
180 180
181 181 all_internal_phases = tuple(p for p in allphases if p & internal)
182 182 # We do not want any internal content to exit the repository, ever.
183 183 no_bundle_phases = all_internal_phases
184 184
185 185
186 186 def supportinternal(repo: "localrepo.localrepository") -> bool:
187 187 """True if the internal phase can be used on a repository"""
188 188 return requirements.INTERNAL_PHASE_REQUIREMENT in repo.requirements
189 189
190 190
191 191 def supportarchived(repo: "localrepo.localrepository") -> bool:
192 192 """True if the archived phase can be used on a repository"""
193 193 return requirements.ARCHIVED_PHASE_REQUIREMENT in repo.requirements
194 194
195 195
196 196 def _readroots(
197 197 repo: "localrepo.localrepository",
198 198 phasedefaults: Optional["Phasedefaults"] = None,
199 199 ) -> Tuple[Phaseroots, bool]:
200 200 """Read phase roots from disk
201 201
202 202 phasedefaults is a list of fn(repo, roots) callable, which are
203 203 executed if the phase roots file does not exist. When phases are
204 204 being initialized on an existing repository, this could be used to
205 205 set selected changesets phase to something else than public.
206 206
207 207 Return (roots, dirty) where dirty is true if roots differ from
208 208 what is being stored.
209 209 """
210 210 repo = repo.unfiltered()
211 211 dirty = False
212 212 roots = {i: set() for i in allphases}
213 213 to_rev = repo.changelog.index.get_rev
214 214 unknown_msg = b'removing unknown node %s from %i-phase boundary\n'
215 215 try:
216 216 f, pending = txnutil.trypending(repo.root, repo.svfs, b'phaseroots')
217 217 try:
218 218 for line in f:
219 219 str_phase, hex_node = line.split()
220 220 phase = int(str_phase)
221 221 node = bin(hex_node)
222 222 rev = to_rev(node)
223 223 if rev is None:
224 224 repo.ui.debug(unknown_msg % (short(hex_node), phase))
225 225 dirty = True
226 226 else:
227 227 roots[phase].add(rev)
228 228 finally:
229 229 f.close()
230 230 except FileNotFoundError:
231 231 if phasedefaults:
232 232 for f in phasedefaults:
233 233 roots = f(repo, roots)
234 234 dirty = True
235 235 return roots, dirty
236 236
237 237
238 238 def binaryencode(phasemapping: Dict[int, List[bytes]]) -> bytes:
239 239 """encode a 'phase -> nodes' mapping into a binary stream
240 240
241 241 The revision lists are encoded as (phase, root) pairs.
242 242 """
243 243 binarydata = []
244 244 for phase, nodes in phasemapping.items():
245 245 for head in nodes:
246 246 binarydata.append(_fphasesentry.pack(phase, head))
247 247 return b''.join(binarydata)
248 248
249 249
250 250 def binarydecode(stream) -> Dict[int, List[bytes]]:
251 251 """decode a binary stream into a 'phase -> nodes' mapping
252 252
253 253 The (phase, root) pairs are turned back into a dictionary with
254 254 the phase as index and the aggregated roots of that phase as value."""
255 255 headsbyphase = {i: [] for i in allphases}
256 256 entrysize = _fphasesentry.size
257 257 while True:
258 258 entry = stream.read(entrysize)
259 259 if len(entry) < entrysize:
260 260 if entry:
261 261 raise error.Abort(_(b'bad phase-heads stream'))
262 262 break
263 263 phase, node = _fphasesentry.unpack(entry)
264 264 headsbyphase[phase].append(node)
265 265 return headsbyphase
266 266
267 267
268 268 def _sortedrange_insert(data, idx, rev, t):
269 269 merge_before = False
270 270 if idx:
271 271 r1, t1 = data[idx - 1]
272 272 merge_before = r1[-1] + 1 == rev and t1 == t
273 273 merge_after = False
274 274 if idx < len(data):
275 275 r2, t2 = data[idx]
276 276 merge_after = r2[0] == rev + 1 and t2 == t
277 277
278 278 if merge_before and merge_after:
279 279 data[idx - 1] = (range(r1[0], r2[-1] + 1), t)
280 280 data.pop(idx)
281 281 elif merge_before:
282 282 data[idx - 1] = (range(r1[0], rev + 1), t)
283 283 elif merge_after:
284 284 data[idx] = (range(rev, r2[-1] + 1), t)
285 285 else:
286 286 data.insert(idx, (range(rev, rev + 1), t))
287 287
288 288
289 289 def _sortedrange_split(data, idx, rev, t):
290 290 r1, t1 = data[idx]
291 291 if t == t1:
292 292 return
293 293 t = (t1[0], t[1])
294 294 if len(r1) == 1:
295 295 data.pop(idx)
296 296 _sortedrange_insert(data, idx, rev, t)
297 297 elif r1[0] == rev:
298 298 data[idx] = (range(rev + 1, r1[-1] + 1), t1)
299 299 _sortedrange_insert(data, idx, rev, t)
300 300 elif r1[-1] == rev:
301 301 data[idx] = (range(r1[0], rev), t1)
302 302 _sortedrange_insert(data, idx + 1, rev, t)
303 303 else:
304 304 data[idx : idx + 1] = [
305 305 (range(r1[0], rev), t1),
306 306 (range(rev, rev + 1), t),
307 307 (range(rev + 1, r1[-1] + 1), t1),
308 308 ]
309 309
310 310
311 311 def _trackphasechange(data, rev, old, new):
312 312 """add a phase move to the <data> list of ranges
313 313
314 314 If data is None, nothing happens.
315 315 """
316 316 if data is None:
317 317 return
318 318
319 319 # If data is empty, create a one-revision range and done
320 320 if not data:
321 321 data.insert(0, (range(rev, rev + 1), (old, new)))
322 322 return
323 323
324 324 low = 0
325 325 high = len(data)
326 326 t = (old, new)
327 327 while low < high:
328 328 mid = (low + high) // 2
329 329 revs = data[mid][0]
330 330 revs_low = revs[0]
331 331 revs_high = revs[-1]
332 332
333 333 if rev >= revs_low and rev <= revs_high:
334 334 _sortedrange_split(data, mid, rev, t)
335 335 return
336 336
337 337 if revs_low == rev + 1:
338 338 if mid and data[mid - 1][0][-1] == rev:
339 339 _sortedrange_split(data, mid - 1, rev, t)
340 340 else:
341 341 _sortedrange_insert(data, mid, rev, t)
342 342 return
343 343
344 344 if revs_high == rev - 1:
345 345 if mid + 1 < len(data) and data[mid + 1][0][0] == rev:
346 346 _sortedrange_split(data, mid + 1, rev, t)
347 347 else:
348 348 _sortedrange_insert(data, mid + 1, rev, t)
349 349 return
350 350
351 351 if revs_low > rev:
352 352 high = mid
353 353 else:
354 354 low = mid + 1
355 355
356 356 if low == len(data):
357 357 data.append((range(rev, rev + 1), t))
358 358 return
359 359
360 360 r1, t1 = data[low]
361 361 if r1[0] > rev:
362 362 data.insert(low, (range(rev, rev + 1), t))
363 363 else:
364 364 data.insert(low + 1, (range(rev, rev + 1), t))
365 365
366 366
367 367 class phasecache:
368 368 def __init__(
369 369 self,
370 370 repo: "localrepo.localrepository",
371 371 phasedefaults: Optional["Phasedefaults"],
372 372 _load: bool = True,
373 373 ):
374 374 if _load:
375 375 # Cheap trick to allow shallow-copy without copy module
376 376 loaded = _readroots(repo, phasedefaults)
377 377 self._phaseroots: Phaseroots = loaded[0]
378 378 self.dirty: bool = loaded[1]
379 379 self._loadedrevslen = 0
380 380 self._phasesets = None
381 381
382 382 def hasnonpublicphases(self, repo: "localrepo.localrepository") -> bool:
383 383 """detect if there are revisions with non-public phase"""
384 384 repo = repo.unfiltered()
385 385 cl = repo.changelog
386 386 if len(cl) >= self._loadedrevslen:
387 387 self.invalidate()
388 388 self.loadphaserevs(repo)
389 389 return any(
390 390 revs for phase, revs in self._phaseroots.items() if phase != public
391 391 )
392 392
393 393 def nonpublicphaseroots(
394 394 self, repo: "localrepo.localrepository"
395 395 ) -> Set[int]:
396 396 """returns the roots of all non-public phases
397 397
398 398 The roots are not minimized, so if the secret revisions are
399 399 descendants of draft revisions, their roots will still be present.
400 400 """
401 401 repo = repo.unfiltered()
402 402 cl = repo.changelog
403 403 if len(cl) >= self._loadedrevslen:
404 404 self.invalidate()
405 405 self.loadphaserevs(repo)
406 406 return set().union(
407 407 *[
408 408 revs
409 409 for phase, revs in self._phaseroots.items()
410 410 if phase != public
411 411 ]
412 412 )
413 413
414 414 def getrevset(
415 415 self,
416 416 repo: "localrepo.localrepository",
417 417 phases: Iterable[int],
418 418 subset: Optional[Any] = None,
419 419 ) -> Any:
420 420 # TODO: finish typing this
421 421 """return a smartset for the given phases"""
422 422 self.loadphaserevs(repo) # ensure phase's sets are loaded
423 423 phases = set(phases)
424 424 publicphase = public in phases
425 425
426 426 if publicphase:
427 427 # In this case, phases keeps all the *other* phases.
428 428 phases = set(allphases).difference(phases)
429 429 if not phases:
430 430 return smartset.fullreposet(repo)
431 431
432 432 # fast path: _phasesets contains the interesting sets,
433 433 # might only need a union and post-filtering.
434 434 revsneedscopy = False
435 435 if len(phases) == 1:
436 436 [p] = phases
437 437 revs = self._phasesets[p]
438 438 revsneedscopy = True # Don't modify _phasesets
439 439 else:
440 440 # revs has the revisions in all *other* phases.
441 441 revs = set.union(*[self._phasesets[p] for p in phases])
442 442
443 443 def _addwdir(wdirsubset, wdirrevs):
444 444 if wdirrev in wdirsubset and repo[None].phase() in phases:
445 445 if revsneedscopy:
446 446 wdirrevs = wdirrevs.copy()
447 447 # The working dir would never be in the # cache, but it was in
448 448 # the subset being filtered for its phase (or filtered out,
449 449 # depending on publicphase), so add it to the output to be
450 450 # included (or filtered out).
451 451 wdirrevs.add(wdirrev)
452 452 return wdirrevs
453 453
454 454 if not publicphase:
455 455 if repo.changelog.filteredrevs:
456 456 revs = revs - repo.changelog.filteredrevs
457 457
458 458 if subset is None:
459 459 return smartset.baseset(revs)
460 460 else:
461 461 revs = _addwdir(subset, revs)
462 462 return subset & smartset.baseset(revs)
463 463 else:
464 464 if subset is None:
465 465 subset = smartset.fullreposet(repo)
466 466
467 467 revs = _addwdir(subset, revs)
468 468
469 469 if not revs:
470 470 return subset
471 471 return subset.filter(lambda r: r not in revs)
472 472
473 473 def copy(self):
474 474 # Shallow copy meant to ensure isolation in
475 475 # advance/retractboundary(), nothing more.
476 476 ph = self.__class__(None, None, _load=False)
477 477 ph._phaseroots = self._phaseroots.copy()
478 478 ph.dirty = self.dirty
479 479 ph._loadedrevslen = self._loadedrevslen
480 480 ph._phasesets = self._phasesets
481 481 return ph
482 482
483 483 def replace(self, phcache):
484 484 """replace all values in 'self' with content of phcache"""
485 485 for a in (
486 486 '_phaseroots',
487 487 'dirty',
488 488 '_loadedrevslen',
489 489 '_phasesets',
490 490 ):
491 491 setattr(self, a, getattr(phcache, a))
492 492
493 493 def _getphaserevsnative(self, repo):
494 494 repo = repo.unfiltered()
495 495 return repo.changelog.computephases(self._phaseroots)
496 496
497 497 def _computephaserevspure(self, repo):
498 498 repo = repo.unfiltered()
499 499 cl = repo.changelog
500 500 self._phasesets = {phase: set() for phase in allphases}
501 501 lowerroots = set()
502 502 for phase in reversed(trackedphases):
503 503 roots = self._phaseroots[phase]
504 504 if roots:
505 505 ps = set(cl.descendants(roots))
506 506 for root in roots:
507 507 ps.add(root)
508 508 ps.difference_update(lowerroots)
509 509 lowerroots.update(ps)
510 510 self._phasesets[phase] = ps
511 511 self._loadedrevslen = len(cl)
512 512
513 513 def loadphaserevs(self, repo: "localrepo.localrepository") -> None:
514 514 """ensure phase information is loaded in the object"""
515 515 if self._phasesets is None:
516 516 try:
517 517 res = self._getphaserevsnative(repo)
518 518 self._loadedrevslen, self._phasesets = res
519 519 except AttributeError:
520 520 self._computephaserevspure(repo)
521 521
522 522 def invalidate(self):
523 523 self._loadedrevslen = 0
524 524 self._phasesets = None
525 525
526 526 def phase(self, repo: "localrepo.localrepository", rev: int) -> int:
527 527 # We need a repo argument here to be able to build _phasesets
528 528 # if necessary. The repository instance is not stored in
529 529 # phasecache to avoid reference cycles. The changelog instance
530 530 # is not stored because it is a filecache() property and can
531 531 # be replaced without us being notified.
532 532 if rev == nullrev:
533 533 return public
534 534 if rev < nullrev:
535 535 raise ValueError(_(b'cannot lookup negative revision'))
536 536 if rev >= self._loadedrevslen:
537 537 self.invalidate()
538 538 self.loadphaserevs(repo)
539 539 for phase in trackedphases:
540 540 if rev in self._phasesets[phase]:
541 541 return phase
542 542 return public
543 543
544 544 def write(self, repo):
545 545 if not self.dirty:
546 546 return
547 547 f = repo.svfs(b'phaseroots', b'w', atomictemp=True, checkambig=True)
548 548 try:
549 549 self._write(repo.unfiltered(), f)
550 550 finally:
551 551 f.close()
552 552
553 553 def _write(self, repo, fp):
554 554 assert repo.filtername is None
555 555 to_node = repo.changelog.node
556 556 for phase, roots in self._phaseroots.items():
557 557 for r in sorted(roots):
558 558 h = to_node(r)
559 559 fp.write(b'%i %s\n' % (phase, hex(h)))
560 560 self.dirty = False
561 561
562 562 def _updateroots(self, repo, phase, newroots, tr):
563 563 self._phaseroots[phase] = newroots
564 564 self.invalidate()
565 565 self.dirty = True
566 566
567 567 assert repo.filtername is None
568 568 wrepo = weakref.ref(repo)
569 569
570 570 def tr_write(fp):
571 571 repo = wrepo()
572 572 assert repo is not None
573 573 self._write(repo, fp)
574 574
575 575 tr.addfilegenerator(b'phase', (b'phaseroots',), tr_write)
576 576 tr.hookargs[b'phases_moved'] = b'1'
577 577
578 578 def registernew(self, repo, tr, targetphase, revs):
579 579 repo = repo.unfiltered()
580 580 self._retractboundary(repo, tr, targetphase, [], revs=revs)
581 581 if tr is not None and b'phases' in tr.changes:
582 582 phasetracking = tr.changes[b'phases']
583 583 phase = self.phase
584 584 for rev in sorted(revs):
585 585 revphase = phase(repo, rev)
586 586 _trackphasechange(phasetracking, rev, None, revphase)
587 587 repo.invalidatevolatilesets()
588 588
589 589 def advanceboundary(
590 590 self, repo, tr, targetphase, nodes=None, revs=None, dryrun=None
591 591 ):
592 592 """Set all 'nodes' to phase 'targetphase'
593 593
594 594 Nodes with a phase lower than 'targetphase' are not affected.
595 595
596 596 If dryrun is True, no actions will be performed
597 597
598 598 Returns a set of revs whose phase is changed or should be changed
599 599 """
600 if targetphase == public and not self.hasnonpublicphases(repo):
601 return set()
600 602 # Be careful to preserve shallow-copied values: do not update
601 603 # phaseroots values, replace them.
602 604 if revs is None:
603 605 revs = []
604 606 if not revs and not nodes:
605 607 return set()
606 608 if tr is None:
607 609 phasetracking = None
608 610 else:
609 611 phasetracking = tr.changes.get(b'phases')
610 612
611 613 repo = repo.unfiltered()
612 614 revs = [repo[n].rev() for n in nodes] + [r for r in revs]
613 615
614 616 changes = set() # set of revisions to be changed
615 617 delroots = [] # set of root deleted by this path
616 618 for phase in (phase for phase in allphases if phase > targetphase):
617 619 # filter nodes that are not in a compatible phase already
618 620 revs = [rev for rev in revs if self.phase(repo, rev) >= phase]
619 621 if not revs:
620 622 break # no roots to move anymore
621 623
622 624 olds = self._phaseroots[phase]
623 625
624 626 affected = repo.revs(b'%ld::%ld', olds, revs)
625 627 changes.update(affected)
626 628 if dryrun:
627 629 continue
628 630 for r in affected:
629 631 _trackphasechange(
630 632 phasetracking, r, self.phase(repo, r), targetphase
631 633 )
632 634
633 635 roots = set(repo.revs(b'roots((%ld::) - %ld)', olds, affected))
634 636 if olds != roots:
635 637 self._updateroots(repo, phase, roots, tr)
636 638 # some roots may need to be declared for lower phases
637 639 delroots.extend(olds - roots)
638 640 if not dryrun:
639 641 # declare deleted root in the target phase
640 642 if targetphase != 0:
641 643 self._retractboundary(repo, tr, targetphase, revs=delroots)
642 644 repo.invalidatevolatilesets()
643 645 return changes
644 646
645 647 def retractboundary(self, repo, tr, targetphase, nodes):
646 648 oldroots = {
647 649 phase: revs
648 650 for phase, revs in self._phaseroots.items()
649 651 if phase <= targetphase
650 652 }
651 653 if tr is None:
652 654 phasetracking = None
653 655 else:
654 656 phasetracking = tr.changes.get(b'phases')
655 657 repo = repo.unfiltered()
656 658 retracted = self._retractboundary(repo, tr, targetphase, nodes)
657 659 if retracted and phasetracking is not None:
658 660
659 661 # find the affected revisions
660 662 new = self._phaseroots[targetphase]
661 663 old = oldroots[targetphase]
662 664 affected = set(repo.revs(b'(%ld::) - (%ld::)', new, old))
663 665
664 666 # find the phase of the affected revision
665 667 for phase in range(targetphase, -1, -1):
666 668 if phase:
667 669 roots = oldroots.get(phase, [])
668 670 revs = set(repo.revs(b'%ld::%ld', roots, affected))
669 671 affected -= revs
670 672 else: # public phase
671 673 revs = affected
672 674 for r in sorted(revs):
673 675 _trackphasechange(phasetracking, r, phase, targetphase)
674 676 repo.invalidatevolatilesets()
675 677
676 678 def _retractboundary(self, repo, tr, targetphase, nodes=None, revs=None):
677 679 if targetphase == public:
678 680 return False
679 681 # Be careful to preserve shallow-copied values: do not update
680 682 # phaseroots values, replace them.
681 683 if revs is None:
682 684 revs = []
683 685 if nodes is None:
684 686 nodes = []
685 687 if not revs and not nodes:
686 688 return False
687 689 if (
688 690 targetphase == internal
689 691 and not supportinternal(repo)
690 692 or targetphase == archived
691 693 and not supportarchived(repo)
692 694 ):
693 695 name = phasenames[targetphase]
694 696 msg = b'this repository does not support the %s phase' % name
695 697 raise error.ProgrammingError(msg)
696 698
697 699 torev = repo.changelog.index.rev
698 700 currentroots = self._phaseroots[targetphase]
699 701 finalroots = oldroots = set(currentroots)
700 702 newroots = [torev(node) for node in nodes] + [r for r in revs]
701 703 newroots = [
702 704 rev for rev in newroots if self.phase(repo, rev) < targetphase
703 705 ]
704 706
705 707 if newroots:
706 708 if nullrev in newroots:
707 709 raise error.Abort(_(b'cannot change null revision phase'))
708 710 # do not break the CoW assumption of the shallow copy
709 711 currentroots = currentroots.copy()
710 712 currentroots.update(newroots)
711 713
712 714 # Only compute new roots for revs above the roots that are being
713 715 # retracted.
714 716 minnewroot = min(newroots)
715 717 aboveroots = [rev for rev in currentroots if rev >= minnewroot]
716 718 updatedroots = repo.revs(b'roots(%ld::)', aboveroots)
717 719
718 720 finalroots = {rev for rev in currentroots if rev < minnewroot}
719 721 finalroots.update(updatedroots)
720 722 if finalroots != oldroots:
721 723 self._updateroots(repo, targetphase, finalroots, tr)
722 724 return True
723 725 return False
724 726
725 727 def register_strip(
726 728 self,
727 729 repo,
728 730 tr,
729 731 strip_rev: int,
730 732 ):
731 733 """announce a strip to the phase cache
732 734
733 735 Any roots higher than the stripped revision should be dropped.
734 736 """
735 737 for targetphase, roots in list(self._phaseroots.items()):
736 738 filtered = {r for r in roots if r >= strip_rev}
737 739 if filtered:
738 740 self._updateroots(repo, targetphase, roots - filtered, tr)
739 741 self.invalidate()
740 742
741 743
742 744 def advanceboundary(repo, tr, targetphase, nodes, revs=None, dryrun=None):
743 745 """Add nodes to a phase changing other nodes phases if necessary.
744 746
745 747 This function move boundary *forward* this means that all nodes
746 748 are set in the target phase or kept in a *lower* phase.
747 749
748 750 Simplify boundary to contains phase roots only.
749 751
750 752 If dryrun is True, no actions will be performed
751 753
752 754 Returns a set of revs whose phase is changed or should be changed
753 755 """
754 756 if revs is None:
755 757 revs = []
756 758 phcache = repo._phasecache.copy()
757 759 changes = phcache.advanceboundary(
758 760 repo, tr, targetphase, nodes, revs=revs, dryrun=dryrun
759 761 )
760 762 if not dryrun:
761 763 repo._phasecache.replace(phcache)
762 764 return changes
763 765
764 766
765 767 def retractboundary(repo, tr, targetphase, nodes):
766 768 """Set nodes back to a phase changing other nodes phases if
767 769 necessary.
768 770
769 771 This function move boundary *backward* this means that all nodes
770 772 are set in the target phase or kept in a *higher* phase.
771 773
772 774 Simplify boundary to contains phase roots only."""
773 775 phcache = repo._phasecache.copy()
774 776 phcache.retractboundary(repo, tr, targetphase, nodes)
775 777 repo._phasecache.replace(phcache)
776 778
777 779
778 780 def registernew(repo, tr, targetphase, revs):
779 781 """register a new revision and its phase
780 782
781 783 Code adding revisions to the repository should use this function to
782 784 set new changeset in their target phase (or higher).
783 785 """
784 786 phcache = repo._phasecache.copy()
785 787 phcache.registernew(repo, tr, targetphase, revs)
786 788 repo._phasecache.replace(phcache)
787 789
788 790
789 791 def listphases(repo: "localrepo.localrepository") -> Dict[bytes, bytes]:
790 792 """List phases root for serialization over pushkey"""
791 793 # Use ordered dictionary so behavior is deterministic.
792 794 keys = util.sortdict()
793 795 value = b'%i' % draft
794 796 cl = repo.unfiltered().changelog
795 797 to_node = cl.node
796 798 for root in repo._phasecache._phaseroots[draft]:
797 799 if repo._phasecache.phase(repo, root) <= draft:
798 800 keys[hex(to_node(root))] = value
799 801
800 802 if repo.publishing():
801 803 # Add an extra data to let remote know we are a publishing
802 804 # repo. Publishing repo can't just pretend they are old repo.
803 805 # When pushing to a publishing repo, the client still need to
804 806 # push phase boundary
805 807 #
806 808 # Push do not only push changeset. It also push phase data.
807 809 # New phase data may apply to common changeset which won't be
808 810 # push (as they are common). Here is a very simple example:
809 811 #
810 812 # 1) repo A push changeset X as draft to repo B
811 813 # 2) repo B make changeset X public
812 814 # 3) repo B push to repo A. X is not pushed but the data that
813 815 # X as now public should
814 816 #
815 817 # The server can't handle it on it's own as it has no idea of
816 818 # client phase data.
817 819 keys[b'publishing'] = b'True'
818 820 return keys
819 821
820 822
821 823 def pushphase(
822 824 repo: "localrepo.localrepository",
823 825 nhex: bytes,
824 826 oldphasestr: bytes,
825 827 newphasestr: bytes,
826 828 ) -> bool:
827 829 """List phases root for serialization over pushkey"""
828 830 repo = repo.unfiltered()
829 831 with repo.lock():
830 832 currentphase = repo[nhex].phase()
831 833 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
832 834 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
833 835 if currentphase == oldphase and newphase < oldphase:
834 836 with repo.transaction(b'pushkey-phase') as tr:
835 837 advanceboundary(repo, tr, newphase, [bin(nhex)])
836 838 return True
837 839 elif currentphase == newphase:
838 840 # raced, but got correct result
839 841 return True
840 842 else:
841 843 return False
842 844
843 845
844 846 def subsetphaseheads(repo, subset):
845 847 """Finds the phase heads for a subset of a history
846 848
847 849 Returns a list indexed by phase number where each item is a list of phase
848 850 head nodes.
849 851 """
850 852 cl = repo.changelog
851 853
852 854 headsbyphase = {i: [] for i in allphases}
853 855 for phase in allphases:
854 856 revset = b"heads(%%ln & _phase(%d))" % phase
855 857 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
856 858 return headsbyphase
857 859
858 860
859 861 def updatephases(repo, trgetter, headsbyphase):
860 862 """Updates the repo with the given phase heads"""
861 863 # Now advance phase boundaries of all phases
862 864 #
863 865 # run the update (and fetch transaction) only if there are actually things
864 866 # to update. This avoid creating empty transaction during no-op operation.
865 867
866 868 for phase in allphases:
867 869 revset = b'%ln - _phase(%s)'
868 870 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
869 871 if heads:
870 872 advanceboundary(repo, trgetter(), phase, heads)
871 873
872 874
873 875 def analyzeremotephases(repo, subset, roots):
874 876 """Compute phases heads and root in a subset of node from root dict
875 877
876 878 * subset is heads of the subset
877 879 * roots is {<nodeid> => phase} mapping. key and value are string.
878 880
879 881 Accept unknown element input
880 882 """
881 883 repo = repo.unfiltered()
882 884 # build list from dictionary
883 885 draftroots = []
884 886 has_node = repo.changelog.index.has_node # to filter unknown nodes
885 887 for nhex, phase in roots.items():
886 888 if nhex == b'publishing': # ignore data related to publish option
887 889 continue
888 890 node = bin(nhex)
889 891 phase = int(phase)
890 892 if phase == public:
891 893 if node != repo.nullid:
892 894 repo.ui.warn(
893 895 _(
894 896 b'ignoring inconsistent public root'
895 897 b' from remote: %s\n'
896 898 )
897 899 % nhex
898 900 )
899 901 elif phase == draft:
900 902 if has_node(node):
901 903 draftroots.append(node)
902 904 else:
903 905 repo.ui.warn(
904 906 _(b'ignoring unexpected root from remote: %i %s\n')
905 907 % (phase, nhex)
906 908 )
907 909 # compute heads
908 910 publicheads = newheads(repo, subset, draftroots)
909 911 return publicheads, draftroots
910 912
911 913
912 914 class remotephasessummary:
913 915 """summarize phase information on the remote side
914 916
915 917 :publishing: True is the remote is publishing
916 918 :publicheads: list of remote public phase heads (nodes)
917 919 :draftheads: list of remote draft phase heads (nodes)
918 920 :draftroots: list of remote draft phase root (nodes)
919 921 """
920 922
921 923 def __init__(self, repo, remotesubset, remoteroots):
922 924 unfi = repo.unfiltered()
923 925 self._allremoteroots = remoteroots
924 926
925 927 self.publishing = remoteroots.get(b'publishing', False)
926 928
927 929 ana = analyzeremotephases(repo, remotesubset, remoteroots)
928 930 self.publicheads, self.draftroots = ana
929 931 # Get the list of all "heads" revs draft on remote
930 932 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
931 933 self.draftheads = [c.node() for c in dheads]
932 934
933 935
934 936 def newheads(repo, heads, roots):
935 937 """compute new head of a subset minus another
936 938
937 939 * `heads`: define the first subset
938 940 * `roots`: define the second we subtract from the first"""
939 941 # prevent an import cycle
940 942 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
941 943 from . import dagop
942 944
943 945 repo = repo.unfiltered()
944 946 cl = repo.changelog
945 947 rev = cl.index.get_rev
946 948 if not roots:
947 949 return heads
948 950 if not heads or heads == [repo.nullid]:
949 951 return []
950 952 # The logic operated on revisions, convert arguments early for convenience
951 953 new_heads = {rev(n) for n in heads if n != repo.nullid}
952 954 roots = [rev(n) for n in roots]
953 955 # compute the area we need to remove
954 956 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
955 957 # heads in the area are no longer heads
956 958 new_heads.difference_update(affected_zone)
957 959 # revisions in the area have children outside of it,
958 960 # They might be new heads
959 961 candidates = repo.revs(
960 962 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
961 963 )
962 964 candidates -= affected_zone
963 965 if new_heads or candidates:
964 966 # remove candidate that are ancestors of other heads
965 967 new_heads.update(candidates)
966 968 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
967 969 pruned = dagop.reachableroots(repo, candidates, prunestart)
968 970 new_heads.difference_update(pruned)
969 971
970 972 return pycompat.maplist(cl.node, sorted(new_heads))
971 973
972 974
973 975 def newcommitphase(ui: "uimod.ui") -> int:
974 976 """helper to get the target phase of new commit
975 977
976 978 Handle all possible values for the phases.new-commit options.
977 979
978 980 """
979 981 v = ui.config(b'phases', b'new-commit')
980 982 try:
981 983 return phasenumber2[v]
982 984 except KeyError:
983 985 raise error.ConfigError(
984 986 _(b"phases.new-commit: not a valid phase name ('%s')") % v
985 987 )
986 988
987 989
988 990 def hassecret(repo: "localrepo.localrepository") -> bool:
989 991 """utility function that check if a repo have any secret changeset."""
990 992 return bool(repo._phasecache._phaseroots[secret])
991 993
992 994
993 995 def preparehookargs(
994 996 node: bytes,
995 997 old: Optional[int],
996 998 new: Optional[int],
997 999 ) -> Dict[bytes, bytes]:
998 1000 if old is None:
999 1001 old = b''
1000 1002 else:
1001 1003 old = phasenames[old]
1002 1004 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