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