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