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