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