##// END OF EJS Templates
phases: fix typos in docstrings
Martin Geisler -
r16724:00535da8 default
parent child Browse files
Show More
@@ -1,420 +1,418 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 of the
10 10 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 A 'changeset phases' is an indicator that tells us how a changeset is
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 below,
22 22 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 permanent and
25 25 leave no audit trail.
26 26
27 27 First, no changeset can be in two phases at once. Phases are ordered, so they
28 28 can be considered from lowest to highest. The default, lowest phase is 'public'
29 29 - this is the normal phase of existing changesets. A child changeset can not be
30 30 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 local commits are draft by default
39 Local commits are draft by default.
40 40
41 Phase movement and exchange
42 ============================
41 Phase Movement and Exchange
42 ===========================
43 43
44 Phase data are exchanged by pushkey on pull and push. Some server have a
45 publish option set, we call them publishing server. Pushing to such server make
46 draft changeset publish.
44 Phase data is exchanged by pushkey on pull and push. Some servers have a
45 publish option set, we call such a server a "publishing server". Pushing a
46 draft changeset to a publishing server changes the phase to public.
47 47
48 48 A small list of fact/rules define the exchange of phase:
49 49
50 50 * old client never changes server states
51 51 * pull never changes server states
52 * publish and old server csets are seen as public by client
52 * publish and old server changesets are seen as public by client
53 * any secret changeset seen in another repository is lowered to at least draft
53 54
54 * Any secret changeset seens in another repository is lowered to at least draft
55
56
57 Here is the final table summing up the 49 possible usecase of phase exchange:
55 Here is the final table summing up the 49 possible use cases of phase exchange:
58 56
59 57 server
60 58 old publish non-publish
61 59 N X N D P N D P
62 60 old client
63 61 pull
64 62 N - X/X - X/D X/P - X/D X/P
65 63 X - X/X - X/D X/P - X/D X/P
66 64 push
67 65 X X/X X/X X/P X/P X/P X/D X/D X/P
68 66 new client
69 67 pull
70 68 N - P/X - P/D P/P - D/D P/P
71 69 D - P/X - P/D P/P - D/D P/P
72 70 P - P/X - P/D P/P - P/D P/P
73 71 push
74 72 D P/X P/X P/P P/P P/P D/D D/D P/P
75 73 P P/X P/X P/P P/P P/P P/P P/P P/P
76 74
77 75 Legend:
78 76
79 77 A/B = final state on client / state on server
80 78
81 79 * N = new/not present,
82 80 * P = public,
83 81 * D = draft,
84 * X = not tracked (ie: the old client or server has no internal way of
82 * X = not tracked (i.e., the old client or server has no internal way of
85 83 recording the phase.)
86 84
87 85 passive = only pushes
88 86
89 87
90 88 A cell here can be read like this:
91 89
92 90 "When a new client pushes a draft changeset (D) to a publishing server
93 91 where it's not present (N), it's marked public on both sides (P/P)."
94 92
95 Note: old client behave as publish server with Draft only content
93 Note: old client behave as a publishing server with draft only content
96 94 - other people see it as public
97 95 - content is pushed as draft
98 96
99 97 """
100 98
101 99 import errno
102 100 from node import nullid, nullrev, bin, hex, short
103 101 from i18n import _
104 102 import util
105 103
106 104 allphases = public, draft, secret = range(3)
107 105 trackedphases = allphases[1:]
108 106 phasenames = ['public', 'draft', 'secret']
109 107
110 108 def _filterunknown(ui, changelog, phaseroots):
111 109 """remove unknown nodes from the phase boundary
112 110
113 Nothing is lost as unknown nodes only hold data for their descendants
111 Nothing is lost as unknown nodes only hold data for their descendants.
114 112 """
115 113 updated = False
116 114 nodemap = changelog.nodemap # to filter unknown nodes
117 115 for phase, nodes in enumerate(phaseroots):
118 116 missing = [node for node in nodes if node not in nodemap]
119 117 if missing:
120 118 for mnode in missing:
121 119 ui.debug(
122 120 'removing unknown node %s from %i-phase boundary\n'
123 121 % (short(mnode), phase))
124 122 nodes.symmetric_difference_update(missing)
125 123 updated = True
126 124 return updated
127 125
128 126 def _readroots(repo, phasedefaults=None):
129 127 """Read phase roots from disk
130 128
131 129 phasedefaults is a list of fn(repo, roots) callable, which are
132 130 executed if the phase roots file does not exist. When phases are
133 131 being initialized on an existing repository, this could be used to
134 132 set selected changesets phase to something else than public.
135 133
136 134 Return (roots, dirty) where dirty is true if roots differ from
137 135 what is being stored.
138 136 """
139 137 dirty = False
140 138 roots = [set() for i in allphases]
141 139 try:
142 140 f = repo.sopener('phaseroots')
143 141 try:
144 142 for line in f:
145 143 phase, nh = line.split()
146 144 roots[int(phase)].add(bin(nh))
147 145 finally:
148 146 f.close()
149 147 except IOError, inst:
150 148 if inst.errno != errno.ENOENT:
151 149 raise
152 150 if phasedefaults:
153 151 for f in phasedefaults:
154 152 roots = f(repo, roots)
155 153 dirty = True
156 154 if _filterunknown(repo.ui, repo.changelog, roots):
157 155 dirty = True
158 156 return roots, dirty
159 157
160 158 class phasecache(object):
161 159 def __init__(self, repo, phasedefaults, _load=True):
162 160 if _load:
163 161 # Cheap trick to allow shallow-copy without copy module
164 162 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
165 163 self.opener = repo.sopener
166 164 self._phaserevs = None
167 165
168 166 def copy(self):
169 167 # Shallow copy meant to ensure isolation in
170 168 # advance/retractboundary(), nothing more.
171 169 ph = phasecache(None, None, _load=False)
172 170 ph.phaseroots = self.phaseroots[:]
173 171 ph.dirty = self.dirty
174 172 ph.opener = self.opener
175 173 ph._phaserevs = self._phaserevs
176 174 return ph
177 175
178 176 def replace(self, phcache):
179 177 for a in 'phaseroots dirty opener _phaserevs'.split():
180 178 setattr(self, a, getattr(phcache, a))
181 179
182 180 def getphaserevs(self, repo, rebuild=False):
183 181 if rebuild or self._phaserevs is None:
184 182 revs = [public] * len(repo.changelog)
185 183 for phase in trackedphases:
186 184 roots = map(repo.changelog.rev, self.phaseroots[phase])
187 185 if roots:
188 186 for rev in roots:
189 187 revs[rev] = phase
190 188 for rev in repo.changelog.descendants(*roots):
191 189 revs[rev] = phase
192 190 self._phaserevs = revs
193 191 return self._phaserevs
194 192
195 193 def phase(self, repo, rev):
196 194 # We need a repo argument here to be able to build _phaserev
197 195 # if necessary. The repository instance is not stored in
198 196 # phasecache to avoid reference cycles. The changelog instance
199 197 # is not stored because it is a filecache() property and can
200 198 # be replaced without us being notified.
201 199 if rev == nullrev:
202 200 return public
203 201 if self._phaserevs is None or rev >= len(self._phaserevs):
204 202 self._phaserevs = self.getphaserevs(repo, rebuild=True)
205 203 return self._phaserevs[rev]
206 204
207 205 def write(self):
208 206 if not self.dirty:
209 207 return
210 208 f = self.opener('phaseroots', 'w', atomictemp=True)
211 209 try:
212 210 for phase, roots in enumerate(self.phaseroots):
213 211 for h in roots:
214 212 f.write('%i %s\n' % (phase, hex(h)))
215 213 finally:
216 214 f.close()
217 215 self.dirty = False
218 216
219 217 def _updateroots(self, phase, newroots):
220 218 self.phaseroots[phase] = newroots
221 219 self._phaserevs = None
222 220 self.dirty = True
223 221
224 222 def advanceboundary(self, repo, targetphase, nodes):
225 223 # Be careful to preserve shallow-copied values: do not update
226 224 # phaseroots values, replace them.
227 225
228 226 delroots = [] # set of root deleted by this path
229 227 for phase in xrange(targetphase + 1, len(allphases)):
230 228 # filter nodes that are not in a compatible phase already
231 229 nodes = [n for n in nodes
232 230 if self.phase(repo, repo[n].rev()) >= phase]
233 231 if not nodes:
234 232 break # no roots to move anymore
235 233 olds = self.phaseroots[phase]
236 234 roots = set(ctx.node() for ctx in repo.set(
237 235 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
238 236 if olds != roots:
239 237 self._updateroots(phase, roots)
240 238 # some roots may need to be declared for lower phases
241 239 delroots.extend(olds - roots)
242 240 # declare deleted root in the target phase
243 241 if targetphase != 0:
244 242 self.retractboundary(repo, targetphase, delroots)
245 243
246 244 def retractboundary(self, repo, targetphase, nodes):
247 245 # Be careful to preserve shallow-copied values: do not update
248 246 # phaseroots values, replace them.
249 247
250 248 currentroots = self.phaseroots[targetphase]
251 249 newroots = [n for n in nodes
252 250 if self.phase(repo, repo[n].rev()) < targetphase]
253 251 if newroots:
254 252 if nullid in newroots:
255 253 raise util.Abort(_('cannot change null revision phase'))
256 254 currentroots = currentroots.copy()
257 255 currentroots.update(newroots)
258 256 ctxs = repo.set('roots(%ln::)', currentroots)
259 257 currentroots.intersection_update(ctx.node() for ctx in ctxs)
260 258 self._updateroots(targetphase, currentroots)
261 259
262 260 def advanceboundary(repo, targetphase, nodes):
263 261 """Add nodes to a phase changing other nodes phases if necessary.
264 262
265 263 This function move boundary *forward* this means that all nodes are set
266 264 in the target phase or kept in a *lower* phase.
267 265
268 266 Simplify boundary to contains phase roots only."""
269 267 phcache = repo._phasecache.copy()
270 268 phcache.advanceboundary(repo, targetphase, nodes)
271 269 repo._phasecache.replace(phcache)
272 270
273 271 def retractboundary(repo, targetphase, nodes):
274 272 """Set nodes back to a phase changing other nodes phases if necessary.
275 273
276 274 This function move boundary *backward* this means that all nodes are set
277 275 in the target phase or kept in a *higher* phase.
278 276
279 277 Simplify boundary to contains phase roots only."""
280 278 phcache = repo._phasecache.copy()
281 279 phcache.retractboundary(repo, targetphase, nodes)
282 280 repo._phasecache.replace(phcache)
283 281
284 282 def listphases(repo):
285 """List phases root for serialisation over pushkey"""
283 """List phases root for serialization over pushkey"""
286 284 keys = {}
287 285 value = '%i' % draft
288 286 for root in repo._phasecache.phaseroots[draft]:
289 287 keys[hex(root)] = value
290 288
291 289 if repo.ui.configbool('phases', 'publish', True):
292 290 # Add an extra data to let remote know we are a publishing repo.
293 291 # Publishing repo can't just pretend they are old repo. When pushing to
294 292 # a publishing repo, the client still need to push phase boundary
295 293 #
296 294 # Push do not only push changeset. It also push phase data. New
297 295 # phase data may apply to common changeset which won't be push (as they
298 296 # are common). Here is a very simple example:
299 297 #
300 298 # 1) repo A push changeset X as draft to repo B
301 299 # 2) repo B make changeset X public
302 300 # 3) repo B push to repo A. X is not pushed but the data that X as now
303 301 # public should
304 302 #
305 303 # The server can't handle it on it's own as it has no idea of client
306 304 # phase data.
307 305 keys['publishing'] = 'True'
308 306 return keys
309 307
310 308 def pushphase(repo, nhex, oldphasestr, newphasestr):
311 309 """List phases root for serialisation over pushkey"""
312 310 lock = repo.lock()
313 311 try:
314 312 currentphase = repo[nhex].phase()
315 313 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
316 314 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
317 315 if currentphase == oldphase and newphase < oldphase:
318 316 advanceboundary(repo, newphase, [bin(nhex)])
319 317 return 1
320 318 elif currentphase == newphase:
321 319 # raced, but got correct result
322 320 return 1
323 321 else:
324 322 return 0
325 323 finally:
326 324 lock.release()
327 325
328 326 def visibleheads(repo):
329 327 """return the set of visible head of this repo"""
330 328 # XXX we want a cache on this
331 329 sroots = repo._phasecache.phaseroots[secret]
332 330 if sroots:
333 331 # XXX very slow revset. storing heads or secret "boundary" would help.
334 332 revset = repo.set('heads(not (%ln::))', sroots)
335 333
336 334 vheads = [ctx.node() for ctx in revset]
337 335 if not vheads:
338 336 vheads.append(nullid)
339 337 else:
340 338 vheads = repo.heads()
341 339 return vheads
342 340
343 341 def visiblebranchmap(repo):
344 342 """return a branchmap for the visible set"""
345 343 # XXX Recomputing this data on the fly is very slow. We should build a
346 344 # XXX cached version while computin the standard branchmap version.
347 345 sroots = repo._phasecache.phaseroots[secret]
348 346 if sroots:
349 347 vbranchmap = {}
350 348 for branch, nodes in repo.branchmap().iteritems():
351 349 # search for secret heads.
352 350 for n in nodes:
353 351 if repo[n].phase() >= secret:
354 352 nodes = None
355 353 break
356 354 # if secreat heads where found we must compute them again
357 355 if nodes is None:
358 356 s = repo.set('heads(branch(%s) - secret())', branch)
359 357 nodes = [c.node() for c in s]
360 358 vbranchmap[branch] = nodes
361 359 else:
362 360 vbranchmap = repo.branchmap()
363 361 return vbranchmap
364 362
365 363 def analyzeremotephases(repo, subset, roots):
366 364 """Compute phases heads and root in a subset of node from root dict
367 365
368 366 * subset is heads of the subset
369 367 * roots is {<nodeid> => phase} mapping. key and value are string.
370 368
371 369 Accept unknown element input
372 370 """
373 371 # build list from dictionary
374 372 draftroots = []
375 373 nodemap = repo.changelog.nodemap # to filter unknown nodes
376 374 for nhex, phase in roots.iteritems():
377 375 if nhex == 'publishing': # ignore data related to publish option
378 376 continue
379 377 node = bin(nhex)
380 378 phase = int(phase)
381 379 if phase == 0:
382 380 if node != nullid:
383 381 repo.ui.warn(_('ignoring inconsistent public root'
384 382 ' from remote: %s\n') % nhex)
385 383 elif phase == 1:
386 384 if node in nodemap:
387 385 draftroots.append(node)
388 386 else:
389 387 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
390 388 % (phase, nhex))
391 389 # compute heads
392 390 publicheads = newheads(repo, subset, draftroots)
393 391 return publicheads, draftroots
394 392
395 393 def newheads(repo, heads, roots):
396 394 """compute new head of a subset minus another
397 395
398 396 * `heads`: define the first subset
399 397 * `rroots`: define the second we substract to the first"""
400 398 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
401 399 heads, roots, roots, heads)
402 400 return [c.node() for c in revset]
403 401
404 402
405 403 def newcommitphase(ui):
406 404 """helper to get the target phase of new commit
407 405
408 406 Handle all possible values for the phases.new-commit options.
409 407
410 408 """
411 409 v = ui.config('phases', 'new-commit', draft)
412 410 try:
413 411 return phasenames.index(v)
414 412 except ValueError:
415 413 try:
416 414 return int(v)
417 415 except ValueError:
418 416 msg = _("phases.new-commit: not a valid phase name ('%s')")
419 417 raise error.ConfigError(msg % v)
420 418
General Comments 0
You need to be logged in to leave comments. Login now