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