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