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