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