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