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