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