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