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