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