##// END OF EJS Templates
phases: track phase changes from 'retractboundary'...
Boris Feld -
r33458:cf694e64 default
parent child Browse files
Show More
@@ -1,573 +1,596 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
107 107 from .i18n import _
108 108 from .node import (
109 109 bin,
110 110 hex,
111 111 nullid,
112 112 nullrev,
113 113 short,
114 114 )
115 115 from . import (
116 116 error,
117 117 smartset,
118 118 txnutil,
119 119 util,
120 120 )
121 121
122 122 allphases = public, draft, secret = range(3)
123 123 trackedphases = allphases[1:]
124 124 phasenames = ['public', 'draft', 'secret']
125 125
126 126 def _readroots(repo, phasedefaults=None):
127 127 """Read phase roots from disk
128 128
129 129 phasedefaults is a list of fn(repo, roots) callable, which are
130 130 executed if the phase roots file does not exist. When phases are
131 131 being initialized on an existing repository, this could be used to
132 132 set selected changesets phase to something else than public.
133 133
134 134 Return (roots, dirty) where dirty is true if roots differ from
135 135 what is being stored.
136 136 """
137 137 repo = repo.unfiltered()
138 138 dirty = False
139 139 roots = [set() for i in allphases]
140 140 try:
141 141 f, pending = txnutil.trypending(repo.root, repo.svfs, 'phaseroots')
142 142 try:
143 143 for line in f:
144 144 phase, nh = line.split()
145 145 roots[int(phase)].add(bin(nh))
146 146 finally:
147 147 f.close()
148 148 except IOError as inst:
149 149 if inst.errno != errno.ENOENT:
150 150 raise
151 151 if phasedefaults:
152 152 for f in phasedefaults:
153 153 roots = f(repo, roots)
154 154 dirty = True
155 155 return roots, dirty
156 156
157 157 def _trackphasechange(data, rev, old, new):
158 158 """add a phase move the <data> dictionnary
159 159
160 160 If data is None, nothing happens.
161 161 """
162 162 if data is None:
163 163 return
164 164 existing = data.get(rev)
165 165 if existing is not None:
166 166 old = existing[0]
167 167 data[rev] = (old, new)
168 168
169 169 class phasecache(object):
170 170 def __init__(self, repo, phasedefaults, _load=True):
171 171 if _load:
172 172 # Cheap trick to allow shallow-copy without copy module
173 173 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
174 174 self._phaserevs = None
175 175 self._phasesets = None
176 176 self.filterunknown(repo)
177 177 self.opener = repo.svfs
178 178
179 179 def getrevset(self, repo, phases):
180 180 """return a smartset for the given phases"""
181 181 self.loadphaserevs(repo) # ensure phase's sets are loaded
182 182
183 183 if self._phasesets and all(self._phasesets[p] is not None
184 184 for p in phases):
185 185 # fast path - use _phasesets
186 186 revs = self._phasesets[phases[0]]
187 187 if len(phases) > 1:
188 188 revs = revs.copy() # only copy when needed
189 189 for p in phases[1:]:
190 190 revs.update(self._phasesets[p])
191 191 if repo.changelog.filteredrevs:
192 192 revs = revs - repo.changelog.filteredrevs
193 193 return smartset.baseset(revs)
194 194 else:
195 195 # slow path - enumerate all revisions
196 196 phase = self.phase
197 197 revs = (r for r in repo if phase(repo, r) in phases)
198 198 return smartset.generatorset(revs, iterasc=True)
199 199
200 200 def copy(self):
201 201 # Shallow copy meant to ensure isolation in
202 202 # advance/retractboundary(), nothing more.
203 203 ph = self.__class__(None, None, _load=False)
204 204 ph.phaseroots = self.phaseroots[:]
205 205 ph.dirty = self.dirty
206 206 ph.opener = self.opener
207 207 ph._phaserevs = self._phaserevs
208 208 ph._phasesets = self._phasesets
209 209 return ph
210 210
211 211 def replace(self, phcache):
212 212 """replace all values in 'self' with content of phcache"""
213 213 for a in ('phaseroots', 'dirty', 'opener', '_phaserevs', '_phasesets'):
214 214 setattr(self, a, getattr(phcache, a))
215 215
216 216 def _getphaserevsnative(self, repo):
217 217 repo = repo.unfiltered()
218 218 nativeroots = []
219 219 for phase in trackedphases:
220 220 nativeroots.append(map(repo.changelog.rev, self.phaseroots[phase]))
221 221 return repo.changelog.computephases(nativeroots)
222 222
223 223 def _computephaserevspure(self, repo):
224 224 repo = repo.unfiltered()
225 225 revs = [public] * len(repo.changelog)
226 226 self._phaserevs = revs
227 227 self._populatephaseroots(repo)
228 228 for phase in trackedphases:
229 229 roots = list(map(repo.changelog.rev, self.phaseroots[phase]))
230 230 if roots:
231 231 for rev in roots:
232 232 revs[rev] = phase
233 233 for rev in repo.changelog.descendants(roots):
234 234 revs[rev] = phase
235 235
236 236 def loadphaserevs(self, repo):
237 237 """ensure phase information is loaded in the object"""
238 238 if self._phaserevs is None:
239 239 try:
240 240 res = self._getphaserevsnative(repo)
241 241 self._phaserevs, self._phasesets = res
242 242 except AttributeError:
243 243 self._computephaserevspure(repo)
244 244
245 245 def invalidate(self):
246 246 self._phaserevs = None
247 247 self._phasesets = None
248 248
249 249 def _populatephaseroots(self, repo):
250 250 """Fills the _phaserevs cache with phases for the roots.
251 251 """
252 252 cl = repo.changelog
253 253 phaserevs = self._phaserevs
254 254 for phase in trackedphases:
255 255 roots = map(cl.rev, self.phaseroots[phase])
256 256 for root in roots:
257 257 phaserevs[root] = phase
258 258
259 259 def phase(self, repo, rev):
260 260 # We need a repo argument here to be able to build _phaserevs
261 261 # if necessary. The repository instance is not stored in
262 262 # phasecache to avoid reference cycles. The changelog instance
263 263 # is not stored because it is a filecache() property and can
264 264 # be replaced without us being notified.
265 265 if rev == nullrev:
266 266 return public
267 267 if rev < nullrev:
268 268 raise ValueError(_('cannot lookup negative revision'))
269 269 if self._phaserevs is None or rev >= len(self._phaserevs):
270 270 self.invalidate()
271 271 self.loadphaserevs(repo)
272 272 return self._phaserevs[rev]
273 273
274 274 def write(self):
275 275 if not self.dirty:
276 276 return
277 277 f = self.opener('phaseroots', 'w', atomictemp=True, checkambig=True)
278 278 try:
279 279 self._write(f)
280 280 finally:
281 281 f.close()
282 282
283 283 def _write(self, fp):
284 284 for phase, roots in enumerate(self.phaseroots):
285 285 for h in roots:
286 286 fp.write('%i %s\n' % (phase, hex(h)))
287 287 self.dirty = False
288 288
289 289 def _updateroots(self, phase, newroots, tr):
290 290 self.phaseroots[phase] = newroots
291 291 self.invalidate()
292 292 self.dirty = True
293 293
294 294 tr.addfilegenerator('phase', ('phaseroots',), self._write)
295 295 tr.hookargs['phases_moved'] = '1'
296 296
297 297 def registernew(self, repo, tr, targetphase, nodes):
298 298 repo = repo.unfiltered()
299 299 self._retractboundary(repo, tr, targetphase, nodes)
300 300 if tr is not None and 'phases' in tr.changes:
301 301 phasetracking = tr.changes['phases']
302 302 torev = repo.changelog.rev
303 303 phase = self.phase
304 304 for n in nodes:
305 305 rev = torev(n)
306 306 revphase = phase(repo, rev)
307 307 _trackphasechange(phasetracking, rev, None, revphase)
308 308 repo.invalidatevolatilesets()
309 309
310 310 def advanceboundary(self, repo, tr, targetphase, nodes):
311 311 """Set all 'nodes' to phase 'targetphase'
312 312
313 313 Nodes with a phase lower than 'targetphase' are not affected.
314 314 """
315 315 # Be careful to preserve shallow-copied values: do not update
316 316 # phaseroots values, replace them.
317 317 if tr is None:
318 318 phasetracking = None
319 319 else:
320 320 phasetracking = tr.changes.get('phases')
321 321
322 322 repo = repo.unfiltered()
323 323
324 324 delroots = [] # set of root deleted by this path
325 325 for phase in xrange(targetphase + 1, len(allphases)):
326 326 # filter nodes that are not in a compatible phase already
327 327 nodes = [n for n in nodes
328 328 if self.phase(repo, repo[n].rev()) >= phase]
329 329 if not nodes:
330 330 break # no roots to move anymore
331 331
332 332 olds = self.phaseroots[phase]
333 333
334 334 affected = repo.revs('%ln::%ln', olds, nodes)
335 335 for r in affected:
336 336 _trackphasechange(phasetracking, r, self.phase(repo, r),
337 337 targetphase)
338 338
339 339 roots = set(ctx.node() for ctx in repo.set(
340 340 'roots((%ln::) - %ld)', olds, affected))
341 341 if olds != roots:
342 342 self._updateroots(phase, roots, tr)
343 343 # some roots may need to be declared for lower phases
344 344 delroots.extend(olds - roots)
345 345 # declare deleted root in the target phase
346 346 if targetphase != 0:
347 347 self._retractboundary(repo, tr, targetphase, delroots)
348 348 repo.invalidatevolatilesets()
349 349
350 350 def retractboundary(self, repo, tr, targetphase, nodes):
351 self._retractboundary(repo, tr, targetphase, nodes)
351 oldroots = self.phaseroots[:targetphase + 1]
352 if tr is None:
353 phasetracking = None
354 else:
355 phasetracking = tr.changes.get('phases')
356 repo = repo.unfiltered()
357 if (self._retractboundary(repo, tr, targetphase, nodes)
358 and phasetracking is not None):
359
360 # find the affected revisions
361 new = self.phaseroots[targetphase]
362 old = oldroots[targetphase]
363 affected = set(repo.revs('(%ln::) - (%ln::)', new, old))
364
365 # find the phase of the affected revision
366 for phase in xrange(targetphase, -1, -1):
367 if phase:
368 roots = oldroots[phase]
369 revs = set(repo.revs('%ln::%ld', roots, affected))
370 affected -= revs
371 else: # public phase
372 revs = affected
373 for r in revs:
374 _trackphasechange(phasetracking, r, phase, targetphase)
352 375 repo.invalidatevolatilesets()
353 376
354 377 def _retractboundary(self, repo, tr, targetphase, nodes):
355 378 # Be careful to preserve shallow-copied values: do not update
356 379 # phaseroots values, replace them.
357 380
358 381 repo = repo.unfiltered()
359 382 currentroots = self.phaseroots[targetphase]
360 383 finalroots = oldroots = set(currentroots)
361 384 newroots = [n for n in nodes
362 385 if self.phase(repo, repo[n].rev()) < targetphase]
363 386 if newroots:
364 387
365 388 if nullid in newroots:
366 389 raise error.Abort(_('cannot change null revision phase'))
367 390 currentroots = currentroots.copy()
368 391 currentroots.update(newroots)
369 392
370 393 # Only compute new roots for revs above the roots that are being
371 394 # retracted.
372 395 minnewroot = min(repo[n].rev() for n in newroots)
373 396 aboveroots = [n for n in currentroots
374 397 if repo[n].rev() >= minnewroot]
375 398 updatedroots = repo.set('roots(%ln::)', aboveroots)
376 399
377 400 finalroots = set(n for n in currentroots if repo[n].rev() <
378 401 minnewroot)
379 402 finalroots.update(ctx.node() for ctx in updatedroots)
380 403 if finalroots != oldroots:
381 404 self._updateroots(targetphase, finalroots, tr)
382 405 return True
383 406 return False
384 407
385 408 def filterunknown(self, repo):
386 409 """remove unknown nodes from the phase boundary
387 410
388 411 Nothing is lost as unknown nodes only hold data for their descendants.
389 412 """
390 413 filtered = False
391 414 nodemap = repo.changelog.nodemap # to filter unknown nodes
392 415 for phase, nodes in enumerate(self.phaseroots):
393 416 missing = sorted(node for node in nodes if node not in nodemap)
394 417 if missing:
395 418 for mnode in missing:
396 419 repo.ui.debug(
397 420 'removing unknown node %s from %i-phase boundary\n'
398 421 % (short(mnode), phase))
399 422 nodes.symmetric_difference_update(missing)
400 423 filtered = True
401 424 if filtered:
402 425 self.dirty = True
403 426 # filterunknown is called by repo.destroyed, we may have no changes in
404 427 # root but phaserevs contents is certainly invalid (or at least we
405 428 # have not proper way to check that). related to issue 3858.
406 429 #
407 430 # The other caller is __init__ that have no _phaserevs initialized
408 431 # anyway. If this change we should consider adding a dedicated
409 432 # "destroyed" function to phasecache or a proper cache key mechanism
410 433 # (see branchmap one)
411 434 self.invalidate()
412 435
413 436 def advanceboundary(repo, tr, targetphase, nodes):
414 437 """Add nodes to a phase changing other nodes phases if necessary.
415 438
416 439 This function move boundary *forward* this means that all nodes
417 440 are set in the target phase or kept in a *lower* phase.
418 441
419 442 Simplify boundary to contains phase roots only."""
420 443 phcache = repo._phasecache.copy()
421 444 phcache.advanceboundary(repo, tr, targetphase, nodes)
422 445 repo._phasecache.replace(phcache)
423 446
424 447 def retractboundary(repo, tr, targetphase, nodes):
425 448 """Set nodes back to a phase changing other nodes phases if
426 449 necessary.
427 450
428 451 This function move boundary *backward* this means that all nodes
429 452 are set in the target phase or kept in a *higher* phase.
430 453
431 454 Simplify boundary to contains phase roots only."""
432 455 phcache = repo._phasecache.copy()
433 456 phcache.retractboundary(repo, tr, targetphase, nodes)
434 457 repo._phasecache.replace(phcache)
435 458
436 459 def registernew(repo, tr, targetphase, nodes):
437 460 """register a new revision and its phase
438 461
439 462 Code adding revisions to the repository should use this function to
440 463 set new changeset in their target phase (or higher).
441 464 """
442 465 phcache = repo._phasecache.copy()
443 466 phcache.registernew(repo, tr, targetphase, nodes)
444 467 repo._phasecache.replace(phcache)
445 468
446 469 def listphases(repo):
447 470 """List phases root for serialization over pushkey"""
448 471 # Use ordered dictionary so behavior is deterministic.
449 472 keys = util.sortdict()
450 473 value = '%i' % draft
451 474 for root in repo._phasecache.phaseroots[draft]:
452 475 keys[hex(root)] = value
453 476
454 477 if repo.publishing():
455 478 # Add an extra data to let remote know we are a publishing
456 479 # repo. Publishing repo can't just pretend they are old repo.
457 480 # When pushing to a publishing repo, the client still need to
458 481 # push phase boundary
459 482 #
460 483 # Push do not only push changeset. It also push phase data.
461 484 # New phase data may apply to common changeset which won't be
462 485 # push (as they are common). Here is a very simple example:
463 486 #
464 487 # 1) repo A push changeset X as draft to repo B
465 488 # 2) repo B make changeset X public
466 489 # 3) repo B push to repo A. X is not pushed but the data that
467 490 # X as now public should
468 491 #
469 492 # The server can't handle it on it's own as it has no idea of
470 493 # client phase data.
471 494 keys['publishing'] = 'True'
472 495 return keys
473 496
474 497 def pushphase(repo, nhex, oldphasestr, newphasestr):
475 498 """List phases root for serialization over pushkey"""
476 499 repo = repo.unfiltered()
477 500 with repo.lock():
478 501 currentphase = repo[nhex].phase()
479 502 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
480 503 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
481 504 if currentphase == oldphase and newphase < oldphase:
482 505 with repo.transaction('pushkey-phase') as tr:
483 506 advanceboundary(repo, tr, newphase, [bin(nhex)])
484 507 return True
485 508 elif currentphase == newphase:
486 509 # raced, but got correct result
487 510 return True
488 511 else:
489 512 return False
490 513
491 514 def subsetphaseheads(repo, subset):
492 515 """Finds the phase heads for a subset of a history
493 516
494 517 Returns a list indexed by phase number where each item is a list of phase
495 518 head nodes.
496 519 """
497 520 cl = repo.changelog
498 521
499 522 headsbyphase = [[] for i in allphases]
500 523 # No need to keep track of secret phase; any heads in the subset that
501 524 # are not mentioned are implicitly secret.
502 525 for phase in allphases[:-1]:
503 526 revset = "heads(%%ln & %s())" % phasenames[phase]
504 527 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
505 528 return headsbyphase
506 529
507 530 def updatephases(repo, tr, headsbyphase, addednodes):
508 531 """Updates the repo with the given phase heads"""
509 532 # Now advance phase boundaries of all but secret phase
510 533 for phase in allphases[:-1]:
511 534 advanceboundary(repo, tr, phase, headsbyphase[phase])
512 535
513 536 def analyzeremotephases(repo, subset, roots):
514 537 """Compute phases heads and root in a subset of node from root dict
515 538
516 539 * subset is heads of the subset
517 540 * roots is {<nodeid> => phase} mapping. key and value are string.
518 541
519 542 Accept unknown element input
520 543 """
521 544 repo = repo.unfiltered()
522 545 # build list from dictionary
523 546 draftroots = []
524 547 nodemap = repo.changelog.nodemap # to filter unknown nodes
525 548 for nhex, phase in roots.iteritems():
526 549 if nhex == 'publishing': # ignore data related to publish option
527 550 continue
528 551 node = bin(nhex)
529 552 phase = int(phase)
530 553 if phase == public:
531 554 if node != nullid:
532 555 repo.ui.warn(_('ignoring inconsistent public root'
533 556 ' from remote: %s\n') % nhex)
534 557 elif phase == draft:
535 558 if node in nodemap:
536 559 draftroots.append(node)
537 560 else:
538 561 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
539 562 % (phase, nhex))
540 563 # compute heads
541 564 publicheads = newheads(repo, subset, draftroots)
542 565 return publicheads, draftroots
543 566
544 567 def newheads(repo, heads, roots):
545 568 """compute new head of a subset minus another
546 569
547 570 * `heads`: define the first subset
548 571 * `roots`: define the second we subtract from the first"""
549 572 repo = repo.unfiltered()
550 573 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
551 574 heads, roots, roots, heads)
552 575 return [c.node() for c in revset]
553 576
554 577
555 578 def newcommitphase(ui):
556 579 """helper to get the target phase of new commit
557 580
558 581 Handle all possible values for the phases.new-commit options.
559 582
560 583 """
561 584 v = ui.config('phases', 'new-commit', draft)
562 585 try:
563 586 return phasenames.index(v)
564 587 except ValueError:
565 588 try:
566 589 return int(v)
567 590 except ValueError:
568 591 msg = _("phases.new-commit: not a valid phase name ('%s')")
569 592 raise error.ConfigError(msg % v)
570 593
571 594 def hassecret(repo):
572 595 """utility function that check if a repo have any secret changeset."""
573 596 return bool(repo._phasecache.phaseroots[2])
General Comments 0
You need to be logged in to leave comments. Login now