##// END OF EJS Templates
phases: updatephases should not skip internal phase...
Joerg Sonnenberger -
r45676:361a7444 default
parent child Browse files
Show More
@@ -1,919 +1,919 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 hasnonpublicphases(self, repo):
327 327 """detect if there are revisions with non-public phase"""
328 328 repo = repo.unfiltered()
329 329 cl = repo.changelog
330 330 if len(cl) >= self._loadedrevslen:
331 331 self.invalidate()
332 332 self.loadphaserevs(repo)
333 333 return any(self.phaseroots[1:])
334 334
335 335 def nonpublicphaseroots(self, repo):
336 336 """returns the roots of all non-public phases
337 337
338 338 The roots are not minimized, so if the secret revisions are
339 339 descendants of draft revisions, their roots will still be present.
340 340 """
341 341 repo = repo.unfiltered()
342 342 cl = repo.changelog
343 343 if len(cl) >= self._loadedrevslen:
344 344 self.invalidate()
345 345 self.loadphaserevs(repo)
346 346 return set().union(*[roots for roots in self.phaseroots[1:] if roots])
347 347
348 348 def getrevset(self, repo, phases, subset=None):
349 349 """return a smartset for the given phases"""
350 350 self.loadphaserevs(repo) # ensure phase's sets are loaded
351 351 phases = set(phases)
352 352 publicphase = public in phases
353 353
354 354 if publicphase:
355 355 # In this case, phases keeps all the *other* phases.
356 356 phases = set(allphases).difference(phases)
357 357 if not phases:
358 358 return smartset.fullreposet(repo)
359 359
360 360 # fast path: _phasesets contains the interesting sets,
361 361 # might only need a union and post-filtering.
362 362 revsneedscopy = False
363 363 if len(phases) == 1:
364 364 [p] = phases
365 365 revs = self._phasesets[p]
366 366 revsneedscopy = True # Don't modify _phasesets
367 367 else:
368 368 # revs has the revisions in all *other* phases.
369 369 revs = set.union(*[self._phasesets[p] for p in phases])
370 370
371 371 def _addwdir(wdirsubset, wdirrevs):
372 372 if wdirrev in wdirsubset and repo[None].phase() in phases:
373 373 if revsneedscopy:
374 374 wdirrevs = wdirrevs.copy()
375 375 # The working dir would never be in the # cache, but it was in
376 376 # the subset being filtered for its phase (or filtered out,
377 377 # depending on publicphase), so add it to the output to be
378 378 # included (or filtered out).
379 379 wdirrevs.add(wdirrev)
380 380 return wdirrevs
381 381
382 382 if not publicphase:
383 383 if repo.changelog.filteredrevs:
384 384 revs = revs - repo.changelog.filteredrevs
385 385
386 386 if subset is None:
387 387 return smartset.baseset(revs)
388 388 else:
389 389 revs = _addwdir(subset, revs)
390 390 return subset & smartset.baseset(revs)
391 391 else:
392 392 if subset is None:
393 393 subset = smartset.fullreposet(repo)
394 394
395 395 revs = _addwdir(subset, revs)
396 396
397 397 if not revs:
398 398 return subset
399 399 return subset.filter(lambda r: r not in revs)
400 400
401 401 def copy(self):
402 402 # Shallow copy meant to ensure isolation in
403 403 # advance/retractboundary(), nothing more.
404 404 ph = self.__class__(None, None, _load=False)
405 405 ph.phaseroots = self.phaseroots[:]
406 406 ph.dirty = self.dirty
407 407 ph.opener = self.opener
408 408 ph._loadedrevslen = self._loadedrevslen
409 409 ph._phasesets = self._phasesets
410 410 return ph
411 411
412 412 def replace(self, phcache):
413 413 """replace all values in 'self' with content of phcache"""
414 414 for a in (
415 415 b'phaseroots',
416 416 b'dirty',
417 417 b'opener',
418 418 b'_loadedrevslen',
419 419 b'_phasesets',
420 420 ):
421 421 setattr(self, a, getattr(phcache, a))
422 422
423 423 def _getphaserevsnative(self, repo):
424 424 repo = repo.unfiltered()
425 425 nativeroots = []
426 426 for phase in trackedphases:
427 427 nativeroots.append(
428 428 pycompat.maplist(repo.changelog.rev, self.phaseroots[phase])
429 429 )
430 430 return repo.changelog.computephases(nativeroots)
431 431
432 432 def _computephaserevspure(self, repo):
433 433 repo = repo.unfiltered()
434 434 cl = repo.changelog
435 435 self._phasesets = [set() for phase in allphases]
436 436 lowerroots = set()
437 437 for phase in reversed(trackedphases):
438 438 roots = pycompat.maplist(cl.rev, self.phaseroots[phase])
439 439 if roots:
440 440 ps = set(cl.descendants(roots))
441 441 for root in roots:
442 442 ps.add(root)
443 443 ps.difference_update(lowerroots)
444 444 lowerroots.update(ps)
445 445 self._phasesets[phase] = ps
446 446 self._loadedrevslen = len(cl)
447 447
448 448 def loadphaserevs(self, repo):
449 449 """ensure phase information is loaded in the object"""
450 450 if self._phasesets is None:
451 451 try:
452 452 res = self._getphaserevsnative(repo)
453 453 self._loadedrevslen, self._phasesets = res
454 454 except AttributeError:
455 455 self._computephaserevspure(repo)
456 456
457 457 def invalidate(self):
458 458 self._loadedrevslen = 0
459 459 self._phasesets = None
460 460
461 461 def phase(self, repo, rev):
462 462 # We need a repo argument here to be able to build _phasesets
463 463 # if necessary. The repository instance is not stored in
464 464 # phasecache to avoid reference cycles. The changelog instance
465 465 # is not stored because it is a filecache() property and can
466 466 # be replaced without us being notified.
467 467 if rev == nullrev:
468 468 return public
469 469 if rev < nullrev:
470 470 raise ValueError(_(b'cannot lookup negative revision'))
471 471 if rev >= self._loadedrevslen:
472 472 self.invalidate()
473 473 self.loadphaserevs(repo)
474 474 for phase in trackedphases:
475 475 if rev in self._phasesets[phase]:
476 476 return phase
477 477 return public
478 478
479 479 def write(self):
480 480 if not self.dirty:
481 481 return
482 482 f = self.opener(b'phaseroots', b'w', atomictemp=True, checkambig=True)
483 483 try:
484 484 self._write(f)
485 485 finally:
486 486 f.close()
487 487
488 488 def _write(self, fp):
489 489 for phase, roots in enumerate(self.phaseroots):
490 490 for h in sorted(roots):
491 491 fp.write(b'%i %s\n' % (phase, hex(h)))
492 492 self.dirty = False
493 493
494 494 def _updateroots(self, phase, newroots, tr):
495 495 self.phaseroots[phase] = newroots
496 496 self.invalidate()
497 497 self.dirty = True
498 498
499 499 tr.addfilegenerator(b'phase', (b'phaseroots',), self._write)
500 500 tr.hookargs[b'phases_moved'] = b'1'
501 501
502 502 def registernew(self, repo, tr, targetphase, nodes):
503 503 repo = repo.unfiltered()
504 504 self._retractboundary(repo, tr, targetphase, nodes)
505 505 if tr is not None and b'phases' in tr.changes:
506 506 phasetracking = tr.changes[b'phases']
507 507 torev = repo.changelog.rev
508 508 phase = self.phase
509 509 revs = [torev(node) for node in nodes]
510 510 revs.sort()
511 511 for rev in revs:
512 512 revphase = phase(repo, rev)
513 513 _trackphasechange(phasetracking, rev, None, revphase)
514 514 repo.invalidatevolatilesets()
515 515
516 516 def advanceboundary(self, repo, tr, targetphase, nodes, dryrun=None):
517 517 """Set all 'nodes' to phase 'targetphase'
518 518
519 519 Nodes with a phase lower than 'targetphase' are not affected.
520 520
521 521 If dryrun is True, no actions will be performed
522 522
523 523 Returns a set of revs whose phase is changed or should be changed
524 524 """
525 525 # Be careful to preserve shallow-copied values: do not update
526 526 # phaseroots values, replace them.
527 527 if tr is None:
528 528 phasetracking = None
529 529 else:
530 530 phasetracking = tr.changes.get(b'phases')
531 531
532 532 repo = repo.unfiltered()
533 533
534 534 changes = set() # set of revisions to be changed
535 535 delroots = [] # set of root deleted by this path
536 536 for phase in pycompat.xrange(targetphase + 1, len(allphases)):
537 537 # filter nodes that are not in a compatible phase already
538 538 nodes = [
539 539 n for n in nodes if self.phase(repo, repo[n].rev()) >= phase
540 540 ]
541 541 if not nodes:
542 542 break # no roots to move anymore
543 543
544 544 olds = self.phaseroots[phase]
545 545
546 546 affected = repo.revs(b'%ln::%ln', olds, nodes)
547 547 changes.update(affected)
548 548 if dryrun:
549 549 continue
550 550 for r in affected:
551 551 _trackphasechange(
552 552 phasetracking, r, self.phase(repo, r), targetphase
553 553 )
554 554
555 555 roots = {
556 556 ctx.node()
557 557 for ctx in repo.set(b'roots((%ln::) - %ld)', olds, affected)
558 558 }
559 559 if olds != roots:
560 560 self._updateroots(phase, roots, tr)
561 561 # some roots may need to be declared for lower phases
562 562 delroots.extend(olds - roots)
563 563 if not dryrun:
564 564 # declare deleted root in the target phase
565 565 if targetphase != 0:
566 566 self._retractboundary(repo, tr, targetphase, delroots)
567 567 repo.invalidatevolatilesets()
568 568 return changes
569 569
570 570 def retractboundary(self, repo, tr, targetphase, nodes):
571 571 oldroots = self.phaseroots[: targetphase + 1]
572 572 if tr is None:
573 573 phasetracking = None
574 574 else:
575 575 phasetracking = tr.changes.get(b'phases')
576 576 repo = repo.unfiltered()
577 577 if (
578 578 self._retractboundary(repo, tr, targetphase, nodes)
579 579 and phasetracking is not None
580 580 ):
581 581
582 582 # find the affected revisions
583 583 new = self.phaseroots[targetphase]
584 584 old = oldroots[targetphase]
585 585 affected = set(repo.revs(b'(%ln::) - (%ln::)', new, old))
586 586
587 587 # find the phase of the affected revision
588 588 for phase in pycompat.xrange(targetphase, -1, -1):
589 589 if phase:
590 590 roots = oldroots[phase]
591 591 revs = set(repo.revs(b'%ln::%ld', roots, affected))
592 592 affected -= revs
593 593 else: # public phase
594 594 revs = affected
595 595 for r in sorted(revs):
596 596 _trackphasechange(phasetracking, r, phase, targetphase)
597 597 repo.invalidatevolatilesets()
598 598
599 599 def _retractboundary(self, repo, tr, targetphase, nodes):
600 600 # Be careful to preserve shallow-copied values: do not update
601 601 # phaseroots values, replace them.
602 602 if targetphase in (archived, internal) and not supportinternal(repo):
603 603 name = phasenames[targetphase]
604 604 msg = b'this repository does not support the %s phase' % name
605 605 raise error.ProgrammingError(msg)
606 606
607 607 repo = repo.unfiltered()
608 608 torev = repo.changelog.rev
609 609 tonode = repo.changelog.node
610 610 currentroots = {torev(node) for node in self.phaseroots[targetphase]}
611 611 finalroots = oldroots = set(currentroots)
612 612 newroots = [torev(node) for node in nodes]
613 613 newroots = [
614 614 rev for rev in newroots if self.phase(repo, rev) < targetphase
615 615 ]
616 616
617 617 if newroots:
618 618 if nullrev in newroots:
619 619 raise error.Abort(_(b'cannot change null revision phase'))
620 620 currentroots.update(newroots)
621 621
622 622 # Only compute new roots for revs above the roots that are being
623 623 # retracted.
624 624 minnewroot = min(newroots)
625 625 aboveroots = [rev for rev in currentroots if rev >= minnewroot]
626 626 updatedroots = repo.revs(b'roots(%ld::)', aboveroots)
627 627
628 628 finalroots = {rev for rev in currentroots if rev < minnewroot}
629 629 finalroots.update(updatedroots)
630 630 if finalroots != oldroots:
631 631 self._updateroots(
632 632 targetphase, {tonode(rev) for rev in finalroots}, tr
633 633 )
634 634 return True
635 635 return False
636 636
637 637 def filterunknown(self, repo):
638 638 """remove unknown nodes from the phase boundary
639 639
640 640 Nothing is lost as unknown nodes only hold data for their descendants.
641 641 """
642 642 filtered = False
643 643 has_node = repo.changelog.index.has_node # to filter unknown nodes
644 644 for phase, nodes in enumerate(self.phaseroots):
645 645 missing = sorted(node for node in nodes if not has_node(node))
646 646 if missing:
647 647 for mnode in missing:
648 648 repo.ui.debug(
649 649 b'removing unknown node %s from %i-phase boundary\n'
650 650 % (short(mnode), phase)
651 651 )
652 652 nodes.symmetric_difference_update(missing)
653 653 filtered = True
654 654 if filtered:
655 655 self.dirty = True
656 656 # filterunknown is called by repo.destroyed, we may have no changes in
657 657 # root but _phasesets contents is certainly invalid (or at least we
658 658 # have not proper way to check that). related to issue 3858.
659 659 #
660 660 # The other caller is __init__ that have no _phasesets initialized
661 661 # anyway. If this change we should consider adding a dedicated
662 662 # "destroyed" function to phasecache or a proper cache key mechanism
663 663 # (see branchmap one)
664 664 self.invalidate()
665 665
666 666
667 667 def advanceboundary(repo, tr, targetphase, nodes, dryrun=None):
668 668 """Add nodes to a phase changing other nodes phases if necessary.
669 669
670 670 This function move boundary *forward* this means that all nodes
671 671 are set in the target phase or kept in a *lower* phase.
672 672
673 673 Simplify boundary to contains phase roots only.
674 674
675 675 If dryrun is True, no actions will be performed
676 676
677 677 Returns a set of revs whose phase is changed or should be changed
678 678 """
679 679 phcache = repo._phasecache.copy()
680 680 changes = phcache.advanceboundary(
681 681 repo, tr, targetphase, nodes, dryrun=dryrun
682 682 )
683 683 if not dryrun:
684 684 repo._phasecache.replace(phcache)
685 685 return changes
686 686
687 687
688 688 def retractboundary(repo, tr, targetphase, nodes):
689 689 """Set nodes back to a phase changing other nodes phases if
690 690 necessary.
691 691
692 692 This function move boundary *backward* this means that all nodes
693 693 are set in the target phase or kept in a *higher* phase.
694 694
695 695 Simplify boundary to contains phase roots only."""
696 696 phcache = repo._phasecache.copy()
697 697 phcache.retractboundary(repo, tr, targetphase, nodes)
698 698 repo._phasecache.replace(phcache)
699 699
700 700
701 701 def registernew(repo, tr, targetphase, nodes):
702 702 """register a new revision and its phase
703 703
704 704 Code adding revisions to the repository should use this function to
705 705 set new changeset in their target phase (or higher).
706 706 """
707 707 phcache = repo._phasecache.copy()
708 708 phcache.registernew(repo, tr, targetphase, nodes)
709 709 repo._phasecache.replace(phcache)
710 710
711 711
712 712 def listphases(repo):
713 713 """List phases root for serialization over pushkey"""
714 714 # Use ordered dictionary so behavior is deterministic.
715 715 keys = util.sortdict()
716 716 value = b'%i' % draft
717 717 cl = repo.unfiltered().changelog
718 718 for root in repo._phasecache.phaseroots[draft]:
719 719 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
720 720 keys[hex(root)] = value
721 721
722 722 if repo.publishing():
723 723 # Add an extra data to let remote know we are a publishing
724 724 # repo. Publishing repo can't just pretend they are old repo.
725 725 # When pushing to a publishing repo, the client still need to
726 726 # push phase boundary
727 727 #
728 728 # Push do not only push changeset. It also push phase data.
729 729 # New phase data may apply to common changeset which won't be
730 730 # push (as they are common). Here is a very simple example:
731 731 #
732 732 # 1) repo A push changeset X as draft to repo B
733 733 # 2) repo B make changeset X public
734 734 # 3) repo B push to repo A. X is not pushed but the data that
735 735 # X as now public should
736 736 #
737 737 # The server can't handle it on it's own as it has no idea of
738 738 # client phase data.
739 739 keys[b'publishing'] = b'True'
740 740 return keys
741 741
742 742
743 743 def pushphase(repo, nhex, oldphasestr, newphasestr):
744 744 """List phases root for serialization over pushkey"""
745 745 repo = repo.unfiltered()
746 746 with repo.lock():
747 747 currentphase = repo[nhex].phase()
748 748 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
749 749 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
750 750 if currentphase == oldphase and newphase < oldphase:
751 751 with repo.transaction(b'pushkey-phase') as tr:
752 752 advanceboundary(repo, tr, newphase, [bin(nhex)])
753 753 return True
754 754 elif currentphase == newphase:
755 755 # raced, but got correct result
756 756 return True
757 757 else:
758 758 return False
759 759
760 760
761 761 def subsetphaseheads(repo, subset):
762 762 """Finds the phase heads for a subset of a history
763 763
764 764 Returns a list indexed by phase number where each item is a list of phase
765 765 head nodes.
766 766 """
767 767 cl = repo.changelog
768 768
769 769 headsbyphase = [[] for i in allphases]
770 770 # No need to keep track of secret phase; any heads in the subset that
771 771 # are not mentioned are implicitly secret.
772 772 for phase in allphases[:secret]:
773 773 revset = b"heads(%%ln & %s())" % phasenames[phase]
774 774 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
775 775 return headsbyphase
776 776
777 777
778 778 def updatephases(repo, trgetter, headsbyphase):
779 779 """Updates the repo with the given phase heads"""
780 # Now advance phase boundaries of all but secret phase
780 # Now advance phase boundaries of all phases
781 781 #
782 782 # run the update (and fetch transaction) only if there are actually things
783 783 # to update. This avoid creating empty transaction during no-op operation.
784 784
785 for phase in allphases[:-1]:
785 for phase in allphases:
786 786 revset = b'%ln - _phase(%s)'
787 787 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
788 788 if heads:
789 789 advanceboundary(repo, trgetter(), phase, heads)
790 790
791 791
792 792 def analyzeremotephases(repo, subset, roots):
793 793 """Compute phases heads and root in a subset of node from root dict
794 794
795 795 * subset is heads of the subset
796 796 * roots is {<nodeid> => phase} mapping. key and value are string.
797 797
798 798 Accept unknown element input
799 799 """
800 800 repo = repo.unfiltered()
801 801 # build list from dictionary
802 802 draftroots = []
803 803 has_node = repo.changelog.index.has_node # to filter unknown nodes
804 804 for nhex, phase in pycompat.iteritems(roots):
805 805 if nhex == b'publishing': # ignore data related to publish option
806 806 continue
807 807 node = bin(nhex)
808 808 phase = int(phase)
809 809 if phase == public:
810 810 if node != nullid:
811 811 repo.ui.warn(
812 812 _(
813 813 b'ignoring inconsistent public root'
814 814 b' from remote: %s\n'
815 815 )
816 816 % nhex
817 817 )
818 818 elif phase == draft:
819 819 if has_node(node):
820 820 draftroots.append(node)
821 821 else:
822 822 repo.ui.warn(
823 823 _(b'ignoring unexpected root from remote: %i %s\n')
824 824 % (phase, nhex)
825 825 )
826 826 # compute heads
827 827 publicheads = newheads(repo, subset, draftroots)
828 828 return publicheads, draftroots
829 829
830 830
831 831 class remotephasessummary(object):
832 832 """summarize phase information on the remote side
833 833
834 834 :publishing: True is the remote is publishing
835 835 :publicheads: list of remote public phase heads (nodes)
836 836 :draftheads: list of remote draft phase heads (nodes)
837 837 :draftroots: list of remote draft phase root (nodes)
838 838 """
839 839
840 840 def __init__(self, repo, remotesubset, remoteroots):
841 841 unfi = repo.unfiltered()
842 842 self._allremoteroots = remoteroots
843 843
844 844 self.publishing = remoteroots.get(b'publishing', False)
845 845
846 846 ana = analyzeremotephases(repo, remotesubset, remoteroots)
847 847 self.publicheads, self.draftroots = ana
848 848 # Get the list of all "heads" revs draft on remote
849 849 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
850 850 self.draftheads = [c.node() for c in dheads]
851 851
852 852
853 853 def newheads(repo, heads, roots):
854 854 """compute new head of a subset minus another
855 855
856 856 * `heads`: define the first subset
857 857 * `roots`: define the second we subtract from the first"""
858 858 # prevent an import cycle
859 859 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
860 860 from . import dagop
861 861
862 862 repo = repo.unfiltered()
863 863 cl = repo.changelog
864 864 rev = cl.index.get_rev
865 865 if not roots:
866 866 return heads
867 867 if not heads or heads == [nullid]:
868 868 return []
869 869 # The logic operated on revisions, convert arguments early for convenience
870 870 new_heads = {rev(n) for n in heads if n != nullid}
871 871 roots = [rev(n) for n in roots]
872 872 # compute the area we need to remove
873 873 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
874 874 # heads in the area are no longer heads
875 875 new_heads.difference_update(affected_zone)
876 876 # revisions in the area have children outside of it,
877 877 # They might be new heads
878 878 candidates = repo.revs(
879 879 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
880 880 )
881 881 candidates -= affected_zone
882 882 if new_heads or candidates:
883 883 # remove candidate that are ancestors of other heads
884 884 new_heads.update(candidates)
885 885 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
886 886 pruned = dagop.reachableroots(repo, candidates, prunestart)
887 887 new_heads.difference_update(pruned)
888 888
889 889 return pycompat.maplist(cl.node, sorted(new_heads))
890 890
891 891
892 892 def newcommitphase(ui):
893 893 """helper to get the target phase of new commit
894 894
895 895 Handle all possible values for the phases.new-commit options.
896 896
897 897 """
898 898 v = ui.config(b'phases', b'new-commit')
899 899 try:
900 900 return phasenames.index(v)
901 901 except ValueError:
902 902 try:
903 903 return int(v)
904 904 except ValueError:
905 905 msg = _(b"phases.new-commit: not a valid phase name ('%s')")
906 906 raise error.ConfigError(msg % v)
907 907
908 908
909 909 def hassecret(repo):
910 910 """utility function that check if a repo have any secret changeset."""
911 911 return bool(repo._phasecache.phaseroots[secret])
912 912
913 913
914 914 def preparehookargs(node, old, new):
915 915 if old is None:
916 916 old = b''
917 917 else:
918 918 old = phasenames[old]
919 919 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