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