##// END OF EJS Templates
phase: document the replace method...
Pierre-Yves David -
r25613:a13c18c1 default
parent child Browse files
Show More
@@ -1,466 +1,467 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 import os
104 104 import errno
105 105 from node import nullid, nullrev, bin, hex, short
106 106 from i18n import _
107 107 import util, error
108 108
109 109 allphases = public, draft, secret = range(3)
110 110 trackedphases = allphases[1:]
111 111 phasenames = ['public', 'draft', 'secret']
112 112
113 113 def _readroots(repo, phasedefaults=None):
114 114 """Read phase roots from disk
115 115
116 116 phasedefaults is a list of fn(repo, roots) callable, which are
117 117 executed if the phase roots file does not exist. When phases are
118 118 being initialized on an existing repository, this could be used to
119 119 set selected changesets phase to something else than public.
120 120
121 121 Return (roots, dirty) where dirty is true if roots differ from
122 122 what is being stored.
123 123 """
124 124 repo = repo.unfiltered()
125 125 dirty = False
126 126 roots = [set() for i in allphases]
127 127 try:
128 128 f = None
129 129 if 'HG_PENDING' in os.environ:
130 130 try:
131 131 f = repo.svfs('phaseroots.pending')
132 132 except IOError, inst:
133 133 if inst.errno != errno.ENOENT:
134 134 raise
135 135 if f is None:
136 136 f = repo.svfs('phaseroots')
137 137 try:
138 138 for line in f:
139 139 phase, nh = line.split()
140 140 roots[int(phase)].add(bin(nh))
141 141 finally:
142 142 f.close()
143 143 except IOError, inst:
144 144 if inst.errno != errno.ENOENT:
145 145 raise
146 146 if phasedefaults:
147 147 for f in phasedefaults:
148 148 roots = f(repo, roots)
149 149 dirty = True
150 150 return roots, dirty
151 151
152 152 class phasecache(object):
153 153 def __init__(self, repo, phasedefaults, _load=True):
154 154 if _load:
155 155 # Cheap trick to allow shallow-copy without copy module
156 156 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
157 157 self._phaserevs = None
158 158 self._phasesets = None
159 159 self.filterunknown(repo)
160 160 self.opener = repo.svfs
161 161
162 162 def copy(self):
163 163 # Shallow copy meant to ensure isolation in
164 164 # advance/retractboundary(), nothing more.
165 165 ph = self.__class__(None, None, _load=False)
166 166 ph.phaseroots = self.phaseroots[:]
167 167 ph.dirty = self.dirty
168 168 ph.opener = self.opener
169 169 ph._phaserevs = self._phaserevs
170 170 ph._phasesets = self._phasesets
171 171 return ph
172 172
173 173 def replace(self, phcache):
174 """replace all values in 'self' with content of phcache"""
174 175 for a in 'phaseroots dirty opener _phaserevs _phasesets'.split():
175 176 setattr(self, a, getattr(phcache, a))
176 177
177 178 def _getphaserevsnative(self, repo):
178 179 repo = repo.unfiltered()
179 180 nativeroots = []
180 181 for phase in trackedphases:
181 182 nativeroots.append(map(repo.changelog.rev, self.phaseroots[phase]))
182 183 return repo.changelog.computephases(nativeroots)
183 184
184 185 def _computephaserevspure(self, repo):
185 186 repo = repo.unfiltered()
186 187 revs = [public] * len(repo.changelog)
187 188 self._phaserevs = revs
188 189 self._populatephaseroots(repo)
189 190 for phase in trackedphases:
190 191 roots = map(repo.changelog.rev, self.phaseroots[phase])
191 192 if roots:
192 193 for rev in roots:
193 194 revs[rev] = phase
194 195 for rev in repo.changelog.descendants(roots):
195 196 revs[rev] = phase
196 197
197 198 def loadphaserevs(self, repo):
198 199 """ensure phase information is loaded in the object"""
199 200 if self._phaserevs is None:
200 201 try:
201 202 if repo.ui.configbool('experimental',
202 203 'nativephaseskillswitch'):
203 204 self._computephaserevspure(repo)
204 205 else:
205 206 res = self._getphaserevsnative(repo)
206 207 self._phaserevs, self._phasesets = res
207 208 except AttributeError:
208 209 self._computephaserevspure(repo)
209 210
210 211 def invalidate(self):
211 212 self._phaserevs = None
212 213 self._phasesets = None
213 214
214 215 def _populatephaseroots(self, repo):
215 216 """Fills the _phaserevs cache with phases for the roots.
216 217 """
217 218 cl = repo.changelog
218 219 phaserevs = self._phaserevs
219 220 for phase in trackedphases:
220 221 roots = map(cl.rev, self.phaseroots[phase])
221 222 for root in roots:
222 223 phaserevs[root] = phase
223 224
224 225 def phase(self, repo, rev):
225 226 # We need a repo argument here to be able to build _phaserevs
226 227 # if necessary. The repository instance is not stored in
227 228 # phasecache to avoid reference cycles. The changelog instance
228 229 # is not stored because it is a filecache() property and can
229 230 # be replaced without us being notified.
230 231 if rev == nullrev:
231 232 return public
232 233 if rev < nullrev:
233 234 raise ValueError(_('cannot lookup negative revision'))
234 235 if self._phaserevs is None or rev >= len(self._phaserevs):
235 236 self.invalidate()
236 237 self.loadphaserevs(repo)
237 238 return self._phaserevs[rev]
238 239
239 240 def write(self):
240 241 if not self.dirty:
241 242 return
242 243 f = self.opener('phaseroots', 'w', atomictemp=True)
243 244 try:
244 245 self._write(f)
245 246 finally:
246 247 f.close()
247 248
248 249 def _write(self, fp):
249 250 for phase, roots in enumerate(self.phaseroots):
250 251 for h in roots:
251 252 fp.write('%i %s\n' % (phase, hex(h)))
252 253 self.dirty = False
253 254
254 255 def _updateroots(self, phase, newroots, tr):
255 256 self.phaseroots[phase] = newroots
256 257 self.invalidate()
257 258 self.dirty = True
258 259
259 260 tr.addfilegenerator('phase', ('phaseroots',), self._write)
260 261 tr.hookargs['phases_moved'] = '1'
261 262
262 263 def advanceboundary(self, repo, tr, targetphase, nodes):
263 264 # Be careful to preserve shallow-copied values: do not update
264 265 # phaseroots values, replace them.
265 266
266 267 repo = repo.unfiltered()
267 268 delroots = [] # set of root deleted by this path
268 269 for phase in xrange(targetphase + 1, len(allphases)):
269 270 # filter nodes that are not in a compatible phase already
270 271 nodes = [n for n in nodes
271 272 if self.phase(repo, repo[n].rev()) >= phase]
272 273 if not nodes:
273 274 break # no roots to move anymore
274 275 olds = self.phaseroots[phase]
275 276 roots = set(ctx.node() for ctx in repo.set(
276 277 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
277 278 if olds != roots:
278 279 self._updateroots(phase, roots, tr)
279 280 # some roots may need to be declared for lower phases
280 281 delroots.extend(olds - roots)
281 282 # declare deleted root in the target phase
282 283 if targetphase != 0:
283 284 self.retractboundary(repo, tr, targetphase, delroots)
284 285 repo.invalidatevolatilesets()
285 286
286 287 def retractboundary(self, repo, tr, targetphase, nodes):
287 288 # Be careful to preserve shallow-copied values: do not update
288 289 # phaseroots values, replace them.
289 290
290 291 repo = repo.unfiltered()
291 292 currentroots = self.phaseroots[targetphase]
292 293 newroots = [n for n in nodes
293 294 if self.phase(repo, repo[n].rev()) < targetphase]
294 295 if newroots:
295 296 if nullid in newroots:
296 297 raise util.Abort(_('cannot change null revision phase'))
297 298 currentroots = currentroots.copy()
298 299 currentroots.update(newroots)
299 300 ctxs = repo.set('roots(%ln::)', currentroots)
300 301 currentroots.intersection_update(ctx.node() for ctx in ctxs)
301 302 self._updateroots(targetphase, currentroots, tr)
302 303 repo.invalidatevolatilesets()
303 304
304 305 def filterunknown(self, repo):
305 306 """remove unknown nodes from the phase boundary
306 307
307 308 Nothing is lost as unknown nodes only hold data for their descendants.
308 309 """
309 310 filtered = False
310 311 nodemap = repo.changelog.nodemap # to filter unknown nodes
311 312 for phase, nodes in enumerate(self.phaseroots):
312 313 missing = sorted(node for node in nodes if node not in nodemap)
313 314 if missing:
314 315 for mnode in missing:
315 316 repo.ui.debug(
316 317 'removing unknown node %s from %i-phase boundary\n'
317 318 % (short(mnode), phase))
318 319 nodes.symmetric_difference_update(missing)
319 320 filtered = True
320 321 if filtered:
321 322 self.dirty = True
322 323 # filterunknown is called by repo.destroyed, we may have no changes in
323 324 # root but phaserevs contents is certainly invalid (or at least we
324 325 # have not proper way to check that). related to issue 3858.
325 326 #
326 327 # The other caller is __init__ that have no _phaserevs initialized
327 328 # anyway. If this change we should consider adding a dedicated
328 329 # "destroyed" function to phasecache or a proper cache key mechanism
329 330 # (see branchmap one)
330 331 self.invalidate()
331 332
332 333 def advanceboundary(repo, tr, targetphase, nodes):
333 334 """Add nodes to a phase changing other nodes phases if necessary.
334 335
335 336 This function move boundary *forward* this means that all nodes
336 337 are set in the target phase or kept in a *lower* phase.
337 338
338 339 Simplify boundary to contains phase roots only."""
339 340 phcache = repo._phasecache.copy()
340 341 phcache.advanceboundary(repo, tr, targetphase, nodes)
341 342 repo._phasecache.replace(phcache)
342 343
343 344 def retractboundary(repo, tr, targetphase, nodes):
344 345 """Set nodes back to a phase changing other nodes phases if
345 346 necessary.
346 347
347 348 This function move boundary *backward* this means that all nodes
348 349 are set in the target phase or kept in a *higher* phase.
349 350
350 351 Simplify boundary to contains phase roots only."""
351 352 phcache = repo._phasecache.copy()
352 353 phcache.retractboundary(repo, tr, targetphase, nodes)
353 354 repo._phasecache.replace(phcache)
354 355
355 356 def listphases(repo):
356 357 """List phases root for serialization over pushkey"""
357 358 keys = {}
358 359 value = '%i' % draft
359 360 for root in repo._phasecache.phaseroots[draft]:
360 361 keys[hex(root)] = value
361 362
362 363 if repo.ui.configbool('phases', 'publish', True):
363 364 # Add an extra data to let remote know we are a publishing
364 365 # repo. Publishing repo can't just pretend they are old repo.
365 366 # When pushing to a publishing repo, the client still need to
366 367 # push phase boundary
367 368 #
368 369 # Push do not only push changeset. It also push phase data.
369 370 # New phase data may apply to common changeset which won't be
370 371 # push (as they are common). Here is a very simple example:
371 372 #
372 373 # 1) repo A push changeset X as draft to repo B
373 374 # 2) repo B make changeset X public
374 375 # 3) repo B push to repo A. X is not pushed but the data that
375 376 # X as now public should
376 377 #
377 378 # The server can't handle it on it's own as it has no idea of
378 379 # client phase data.
379 380 keys['publishing'] = 'True'
380 381 return keys
381 382
382 383 def pushphase(repo, nhex, oldphasestr, newphasestr):
383 384 """List phases root for serialization over pushkey"""
384 385 repo = repo.unfiltered()
385 386 tr = None
386 387 lock = repo.lock()
387 388 try:
388 389 currentphase = repo[nhex].phase()
389 390 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
390 391 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
391 392 if currentphase == oldphase and newphase < oldphase:
392 393 tr = repo.transaction('pushkey-phase')
393 394 advanceboundary(repo, tr, newphase, [bin(nhex)])
394 395 tr.close()
395 396 return 1
396 397 elif currentphase == newphase:
397 398 # raced, but got correct result
398 399 return 1
399 400 else:
400 401 return 0
401 402 finally:
402 403 if tr:
403 404 tr.release()
404 405 lock.release()
405 406
406 407 def analyzeremotephases(repo, subset, roots):
407 408 """Compute phases heads and root in a subset of node from root dict
408 409
409 410 * subset is heads of the subset
410 411 * roots is {<nodeid> => phase} mapping. key and value are string.
411 412
412 413 Accept unknown element input
413 414 """
414 415 repo = repo.unfiltered()
415 416 # build list from dictionary
416 417 draftroots = []
417 418 nodemap = repo.changelog.nodemap # to filter unknown nodes
418 419 for nhex, phase in roots.iteritems():
419 420 if nhex == 'publishing': # ignore data related to publish option
420 421 continue
421 422 node = bin(nhex)
422 423 phase = int(phase)
423 424 if phase == 0:
424 425 if node != nullid:
425 426 repo.ui.warn(_('ignoring inconsistent public root'
426 427 ' from remote: %s\n') % nhex)
427 428 elif phase == 1:
428 429 if node in nodemap:
429 430 draftroots.append(node)
430 431 else:
431 432 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
432 433 % (phase, nhex))
433 434 # compute heads
434 435 publicheads = newheads(repo, subset, draftroots)
435 436 return publicheads, draftroots
436 437
437 438 def newheads(repo, heads, roots):
438 439 """compute new head of a subset minus another
439 440
440 441 * `heads`: define the first subset
441 442 * `roots`: define the second we subtract from the first"""
442 443 repo = repo.unfiltered()
443 444 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
444 445 heads, roots, roots, heads)
445 446 return [c.node() for c in revset]
446 447
447 448
448 449 def newcommitphase(ui):
449 450 """helper to get the target phase of new commit
450 451
451 452 Handle all possible values for the phases.new-commit options.
452 453
453 454 """
454 455 v = ui.config('phases', 'new-commit', draft)
455 456 try:
456 457 return phasenames.index(v)
457 458 except ValueError:
458 459 try:
459 460 return int(v)
460 461 except ValueError:
461 462 msg = _("phases.new-commit: not a valid phase name ('%s')")
462 463 raise error.ConfigError(msg % v)
463 464
464 465 def hassecret(repo):
465 466 """utility function that check if a repo have any secret changeset."""
466 467 return bool(repo._phasecache.phaseroots[2])
General Comments 0
You need to be logged in to leave comments. Login now