##// END OF EJS Templates
advanceboundary: add dryrun parameter...
Sushil khanchi -
r38218:36ba5dba default
parent child Browse files
Show More
@@ -1,682 +1,700 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 roots = pycompat.maplist(cl.rev, self.phaseroots[secret])
276 276 if roots:
277 277 ps = set(cl.descendants(roots))
278 278 for root in roots:
279 279 ps.add(root)
280 280 self._phasesets[secret] = ps
281 281 roots = pycompat.maplist(cl.rev, self.phaseroots[draft])
282 282 if roots:
283 283 ps = set(cl.descendants(roots))
284 284 for root in roots:
285 285 ps.add(root)
286 286 ps.difference_update(self._phasesets[secret])
287 287 self._phasesets[draft] = ps
288 288 self._loadedrevslen = len(cl)
289 289
290 290 def loadphaserevs(self, repo):
291 291 """ensure phase information is loaded in the object"""
292 292 if self._phasesets is None:
293 293 try:
294 294 res = self._getphaserevsnative(repo)
295 295 self._loadedrevslen, self._phasesets = res
296 296 except AttributeError:
297 297 self._computephaserevspure(repo)
298 298
299 299 def invalidate(self):
300 300 self._loadedrevslen = 0
301 301 self._phasesets = None
302 302
303 303 def phase(self, repo, rev):
304 304 # We need a repo argument here to be able to build _phasesets
305 305 # if necessary. The repository instance is not stored in
306 306 # phasecache to avoid reference cycles. The changelog instance
307 307 # is not stored because it is a filecache() property and can
308 308 # be replaced without us being notified.
309 309 if rev == nullrev:
310 310 return public
311 311 if rev < nullrev:
312 312 raise ValueError(_('cannot lookup negative revision'))
313 313 if rev >= self._loadedrevslen:
314 314 self.invalidate()
315 315 self.loadphaserevs(repo)
316 316 for phase in trackedphases:
317 317 if rev in self._phasesets[phase]:
318 318 return phase
319 319 return public
320 320
321 321 def write(self):
322 322 if not self.dirty:
323 323 return
324 324 f = self.opener('phaseroots', 'w', atomictemp=True, checkambig=True)
325 325 try:
326 326 self._write(f)
327 327 finally:
328 328 f.close()
329 329
330 330 def _write(self, fp):
331 331 for phase, roots in enumerate(self.phaseroots):
332 332 for h in sorted(roots):
333 333 fp.write('%i %s\n' % (phase, hex(h)))
334 334 self.dirty = False
335 335
336 336 def _updateroots(self, phase, newroots, tr):
337 337 self.phaseroots[phase] = newroots
338 338 self.invalidate()
339 339 self.dirty = True
340 340
341 341 tr.addfilegenerator('phase', ('phaseroots',), self._write)
342 342 tr.hookargs['phases_moved'] = '1'
343 343
344 344 def registernew(self, repo, tr, targetphase, nodes):
345 345 repo = repo.unfiltered()
346 346 self._retractboundary(repo, tr, targetphase, nodes)
347 347 if tr is not None and 'phases' in tr.changes:
348 348 phasetracking = tr.changes['phases']
349 349 torev = repo.changelog.rev
350 350 phase = self.phase
351 351 for n in nodes:
352 352 rev = torev(n)
353 353 revphase = phase(repo, rev)
354 354 _trackphasechange(phasetracking, rev, None, revphase)
355 355 repo.invalidatevolatilesets()
356 356
357 def advanceboundary(self, repo, tr, targetphase, nodes):
357 def advanceboundary(self, repo, tr, targetphase, nodes, dryrun=None):
358 358 """Set all 'nodes' to phase 'targetphase'
359 359
360 360 Nodes with a phase lower than 'targetphase' are not affected.
361
362 If dryrun is True, no actions will be performed
363
364 Returns a set of revs whose phase is changed or should be changed
361 365 """
362 366 # Be careful to preserve shallow-copied values: do not update
363 367 # phaseroots values, replace them.
364 368 if tr is None:
365 369 phasetracking = None
366 370 else:
367 371 phasetracking = tr.changes.get('phases')
368 372
369 373 repo = repo.unfiltered()
370 374
375 changes = set() # set of revisions to be changed
371 376 delroots = [] # set of root deleted by this path
372 377 for phase in xrange(targetphase + 1, len(allphases)):
373 378 # filter nodes that are not in a compatible phase already
374 379 nodes = [n for n in nodes
375 380 if self.phase(repo, repo[n].rev()) >= phase]
376 381 if not nodes:
377 382 break # no roots to move anymore
378 383
379 384 olds = self.phaseroots[phase]
380 385
381 386 affected = repo.revs('%ln::%ln', olds, nodes)
387 changes.update(affected)
388 if dryrun:
389 continue
382 390 for r in affected:
383 391 _trackphasechange(phasetracking, r, self.phase(repo, r),
384 392 targetphase)
385 393
386 394 roots = set(ctx.node() for ctx in repo.set(
387 395 'roots((%ln::) - %ld)', olds, affected))
388 396 if olds != roots:
389 397 self._updateroots(phase, roots, tr)
390 398 # some roots may need to be declared for lower phases
391 399 delroots.extend(olds - roots)
392 # declare deleted root in the target phase
393 if targetphase != 0:
394 self._retractboundary(repo, tr, targetphase, delroots)
395 repo.invalidatevolatilesets()
400 if not dryrun:
401 # declare deleted root in the target phase
402 if targetphase != 0:
403 self._retractboundary(repo, tr, targetphase, delroots)
404 repo.invalidatevolatilesets()
405 return changes
396 406
397 407 def retractboundary(self, repo, tr, targetphase, nodes):
398 408 oldroots = self.phaseroots[:targetphase + 1]
399 409 if tr is None:
400 410 phasetracking = None
401 411 else:
402 412 phasetracking = tr.changes.get('phases')
403 413 repo = repo.unfiltered()
404 414 if (self._retractboundary(repo, tr, targetphase, nodes)
405 415 and phasetracking is not None):
406 416
407 417 # find the affected revisions
408 418 new = self.phaseroots[targetphase]
409 419 old = oldroots[targetphase]
410 420 affected = set(repo.revs('(%ln::) - (%ln::)', new, old))
411 421
412 422 # find the phase of the affected revision
413 423 for phase in xrange(targetphase, -1, -1):
414 424 if phase:
415 425 roots = oldroots[phase]
416 426 revs = set(repo.revs('%ln::%ld', roots, affected))
417 427 affected -= revs
418 428 else: # public phase
419 429 revs = affected
420 430 for r in revs:
421 431 _trackphasechange(phasetracking, r, phase, targetphase)
422 432 repo.invalidatevolatilesets()
423 433
424 434 def _retractboundary(self, repo, tr, targetphase, nodes):
425 435 # Be careful to preserve shallow-copied values: do not update
426 436 # phaseroots values, replace them.
427 437
428 438 repo = repo.unfiltered()
429 439 currentroots = self.phaseroots[targetphase]
430 440 finalroots = oldroots = set(currentroots)
431 441 newroots = [n for n in nodes
432 442 if self.phase(repo, repo[n].rev()) < targetphase]
433 443 if newroots:
434 444
435 445 if nullid in newroots:
436 446 raise error.Abort(_('cannot change null revision phase'))
437 447 currentroots = currentroots.copy()
438 448 currentroots.update(newroots)
439 449
440 450 # Only compute new roots for revs above the roots that are being
441 451 # retracted.
442 452 minnewroot = min(repo[n].rev() for n in newroots)
443 453 aboveroots = [n for n in currentroots
444 454 if repo[n].rev() >= minnewroot]
445 455 updatedroots = repo.set('roots(%ln::)', aboveroots)
446 456
447 457 finalroots = set(n for n in currentroots if repo[n].rev() <
448 458 minnewroot)
449 459 finalroots.update(ctx.node() for ctx in updatedroots)
450 460 if finalroots != oldroots:
451 461 self._updateroots(targetphase, finalroots, tr)
452 462 return True
453 463 return False
454 464
455 465 def filterunknown(self, repo):
456 466 """remove unknown nodes from the phase boundary
457 467
458 468 Nothing is lost as unknown nodes only hold data for their descendants.
459 469 """
460 470 filtered = False
461 471 nodemap = repo.changelog.nodemap # to filter unknown nodes
462 472 for phase, nodes in enumerate(self.phaseroots):
463 473 missing = sorted(node for node in nodes if node not in nodemap)
464 474 if missing:
465 475 for mnode in missing:
466 476 repo.ui.debug(
467 477 'removing unknown node %s from %i-phase boundary\n'
468 478 % (short(mnode), phase))
469 479 nodes.symmetric_difference_update(missing)
470 480 filtered = True
471 481 if filtered:
472 482 self.dirty = True
473 483 # filterunknown is called by repo.destroyed, we may have no changes in
474 484 # root but _phasesets contents is certainly invalid (or at least we
475 485 # have not proper way to check that). related to issue 3858.
476 486 #
477 487 # The other caller is __init__ that have no _phasesets initialized
478 488 # anyway. If this change we should consider adding a dedicated
479 489 # "destroyed" function to phasecache or a proper cache key mechanism
480 490 # (see branchmap one)
481 491 self.invalidate()
482 492
483 def advanceboundary(repo, tr, targetphase, nodes):
493 def advanceboundary(repo, tr, targetphase, nodes, dryrun=None):
484 494 """Add nodes to a phase changing other nodes phases if necessary.
485 495
486 496 This function move boundary *forward* this means that all nodes
487 497 are set in the target phase or kept in a *lower* phase.
488 498
489 Simplify boundary to contains phase roots only."""
499 Simplify boundary to contains phase roots only.
500
501 If dryrun is True, no actions will be performed
502
503 Returns a set of revs whose phase is changed or should be changed
504 """
490 505 phcache = repo._phasecache.copy()
491 phcache.advanceboundary(repo, tr, targetphase, nodes)
492 repo._phasecache.replace(phcache)
506 changes = phcache.advanceboundary(repo, tr, targetphase, nodes,
507 dryrun=dryrun)
508 if not dryrun:
509 repo._phasecache.replace(phcache)
510 return changes
493 511
494 512 def retractboundary(repo, tr, targetphase, nodes):
495 513 """Set nodes back to a phase changing other nodes phases if
496 514 necessary.
497 515
498 516 This function move boundary *backward* this means that all nodes
499 517 are set in the target phase or kept in a *higher* phase.
500 518
501 519 Simplify boundary to contains phase roots only."""
502 520 phcache = repo._phasecache.copy()
503 521 phcache.retractboundary(repo, tr, targetphase, nodes)
504 522 repo._phasecache.replace(phcache)
505 523
506 524 def registernew(repo, tr, targetphase, nodes):
507 525 """register a new revision and its phase
508 526
509 527 Code adding revisions to the repository should use this function to
510 528 set new changeset in their target phase (or higher).
511 529 """
512 530 phcache = repo._phasecache.copy()
513 531 phcache.registernew(repo, tr, targetphase, nodes)
514 532 repo._phasecache.replace(phcache)
515 533
516 534 def listphases(repo):
517 535 """List phases root for serialization over pushkey"""
518 536 # Use ordered dictionary so behavior is deterministic.
519 537 keys = util.sortdict()
520 538 value = '%i' % draft
521 539 cl = repo.unfiltered().changelog
522 540 for root in repo._phasecache.phaseroots[draft]:
523 541 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
524 542 keys[hex(root)] = value
525 543
526 544 if repo.publishing():
527 545 # Add an extra data to let remote know we are a publishing
528 546 # repo. Publishing repo can't just pretend they are old repo.
529 547 # When pushing to a publishing repo, the client still need to
530 548 # push phase boundary
531 549 #
532 550 # Push do not only push changeset. It also push phase data.
533 551 # New phase data may apply to common changeset which won't be
534 552 # push (as they are common). Here is a very simple example:
535 553 #
536 554 # 1) repo A push changeset X as draft to repo B
537 555 # 2) repo B make changeset X public
538 556 # 3) repo B push to repo A. X is not pushed but the data that
539 557 # X as now public should
540 558 #
541 559 # The server can't handle it on it's own as it has no idea of
542 560 # client phase data.
543 561 keys['publishing'] = 'True'
544 562 return keys
545 563
546 564 def pushphase(repo, nhex, oldphasestr, newphasestr):
547 565 """List phases root for serialization over pushkey"""
548 566 repo = repo.unfiltered()
549 567 with repo.lock():
550 568 currentphase = repo[nhex].phase()
551 569 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
552 570 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
553 571 if currentphase == oldphase and newphase < oldphase:
554 572 with repo.transaction('pushkey-phase') as tr:
555 573 advanceboundary(repo, tr, newphase, [bin(nhex)])
556 574 return True
557 575 elif currentphase == newphase:
558 576 # raced, but got correct result
559 577 return True
560 578 else:
561 579 return False
562 580
563 581 def subsetphaseheads(repo, subset):
564 582 """Finds the phase heads for a subset of a history
565 583
566 584 Returns a list indexed by phase number where each item is a list of phase
567 585 head nodes.
568 586 """
569 587 cl = repo.changelog
570 588
571 589 headsbyphase = [[] for i in allphases]
572 590 # No need to keep track of secret phase; any heads in the subset that
573 591 # are not mentioned are implicitly secret.
574 592 for phase in allphases[:-1]:
575 593 revset = "heads(%%ln & %s())" % phasenames[phase]
576 594 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
577 595 return headsbyphase
578 596
579 597 def updatephases(repo, trgetter, headsbyphase):
580 598 """Updates the repo with the given phase heads"""
581 599 # Now advance phase boundaries of all but secret phase
582 600 #
583 601 # run the update (and fetch transaction) only if there are actually things
584 602 # to update. This avoid creating empty transaction during no-op operation.
585 603
586 604 for phase in allphases[:-1]:
587 605 revset = '%%ln - %s()' % phasenames[phase]
588 606 heads = [c.node() for c in repo.set(revset, headsbyphase[phase])]
589 607 if heads:
590 608 advanceboundary(repo, trgetter(), phase, heads)
591 609
592 610 def analyzeremotephases(repo, subset, roots):
593 611 """Compute phases heads and root in a subset of node from root dict
594 612
595 613 * subset is heads of the subset
596 614 * roots is {<nodeid> => phase} mapping. key and value are string.
597 615
598 616 Accept unknown element input
599 617 """
600 618 repo = repo.unfiltered()
601 619 # build list from dictionary
602 620 draftroots = []
603 621 nodemap = repo.changelog.nodemap # to filter unknown nodes
604 622 for nhex, phase in roots.iteritems():
605 623 if nhex == 'publishing': # ignore data related to publish option
606 624 continue
607 625 node = bin(nhex)
608 626 phase = int(phase)
609 627 if phase == public:
610 628 if node != nullid:
611 629 repo.ui.warn(_('ignoring inconsistent public root'
612 630 ' from remote: %s\n') % nhex)
613 631 elif phase == draft:
614 632 if node in nodemap:
615 633 draftroots.append(node)
616 634 else:
617 635 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
618 636 % (phase, nhex))
619 637 # compute heads
620 638 publicheads = newheads(repo, subset, draftroots)
621 639 return publicheads, draftroots
622 640
623 641 class remotephasessummary(object):
624 642 """summarize phase information on the remote side
625 643
626 644 :publishing: True is the remote is publishing
627 645 :publicheads: list of remote public phase heads (nodes)
628 646 :draftheads: list of remote draft phase heads (nodes)
629 647 :draftroots: list of remote draft phase root (nodes)
630 648 """
631 649
632 650 def __init__(self, repo, remotesubset, remoteroots):
633 651 unfi = repo.unfiltered()
634 652 self._allremoteroots = remoteroots
635 653
636 654 self.publishing = remoteroots.get('publishing', False)
637 655
638 656 ana = analyzeremotephases(repo, remotesubset, remoteroots)
639 657 self.publicheads, self.draftroots = ana
640 658 # Get the list of all "heads" revs draft on remote
641 659 dheads = unfi.set('heads(%ln::%ln)', self.draftroots, remotesubset)
642 660 self.draftheads = [c.node() for c in dheads]
643 661
644 662 def newheads(repo, heads, roots):
645 663 """compute new head of a subset minus another
646 664
647 665 * `heads`: define the first subset
648 666 * `roots`: define the second we subtract from the first"""
649 667 repo = repo.unfiltered()
650 668 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
651 669 heads, roots, roots, heads)
652 670 return [c.node() for c in revset]
653 671
654 672
655 673 def newcommitphase(ui):
656 674 """helper to get the target phase of new commit
657 675
658 676 Handle all possible values for the phases.new-commit options.
659 677
660 678 """
661 679 v = ui.config('phases', 'new-commit')
662 680 try:
663 681 return phasenames.index(v)
664 682 except ValueError:
665 683 try:
666 684 return int(v)
667 685 except ValueError:
668 686 msg = _("phases.new-commit: not a valid phase name ('%s')")
669 687 raise error.ConfigError(msg % v)
670 688
671 689 def hassecret(repo):
672 690 """utility function that check if a repo have any secret changeset."""
673 691 return bool(repo._phasecache.phaseroots[2])
674 692
675 693 def preparehookargs(node, old, new):
676 694 if old is None:
677 695 old = ''
678 696 else:
679 697 old = phasenames[old]
680 698 return {'node': node,
681 699 'oldphase': old,
682 700 'phase': phasenames[new]}
General Comments 0
You need to be logged in to leave comments. Login now