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