##// END OF EJS Templates
phases: line.strip().split() == line.split()
Martin Geisler -
r16588:72319bfd default
parent child Browse files
Show More
@@ -1,343 +1,343 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 20 A 'changeset phases' 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 39 local commits are draft by default
40 40
41 41 Phase movement and exchange
42 42 ============================
43 43
44 44 Phase data are exchanged by pushkey on pull and push. Some server have a
45 45 publish option set, we call them publishing server. Pushing to such server make
46 46 draft changeset publish.
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 52 * publish and old server csets are seen as public by client
53 53
54 54 * Any secret changeset seens in another repository is lowered to at least draft
55 55
56 56
57 57 Here is the final table summing up the 49 possible usecase of phase exchange:
58 58
59 59 server
60 60 old publish non-publish
61 61 N X N D P N D P
62 62 old client
63 63 pull
64 64 N - X/X - X/D X/P - X/D X/P
65 65 X - X/X - X/D X/P - X/D X/P
66 66 push
67 67 X X/X X/X X/P X/P X/P X/D X/D X/P
68 68 new client
69 69 pull
70 70 N - P/X - P/D P/P - D/D P/P
71 71 D - P/X - P/D P/P - D/D P/P
72 72 P - P/X - P/D P/P - P/D P/P
73 73 push
74 74 D P/X P/X P/P P/P P/P D/D D/D P/P
75 75 P P/X P/X P/P P/P P/P P/P P/P P/P
76 76
77 77 Legend:
78 78
79 79 A/B = final state on client / state on server
80 80
81 81 * N = new/not present,
82 82 * P = public,
83 83 * D = draft,
84 84 * X = not tracked (ie: the old client or server has no internal way of
85 85 recording the phase.)
86 86
87 87 passive = only pushes
88 88
89 89
90 90 A cell here can be read like this:
91 91
92 92 "When a new client pushes a draft changeset (D) to a publishing server
93 93 where it's not present (N), it's marked public on both sides (P/P)."
94 94
95 95 Note: old client behave as publish server with Draft only content
96 96 - other people see it as public
97 97 - content is pushed as draft
98 98
99 99 """
100 100
101 101 import errno
102 102 from node import nullid, bin, hex, short
103 103 from i18n import _
104 104
105 105 allphases = public, draft, secret = range(3)
106 106 trackedphases = allphases[1:]
107 107 phasenames = ['public', 'draft', 'secret']
108 108
109 109 def readroots(repo):
110 110 """Read phase roots from disk"""
111 111 roots = [set() for i in allphases]
112 112 try:
113 113 f = repo.sopener('phaseroots')
114 114 try:
115 115 for line in f:
116 phase, nh = line.strip().split()
116 phase, nh = line.split()
117 117 roots[int(phase)].add(bin(nh))
118 118 finally:
119 119 f.close()
120 120 except IOError, inst:
121 121 if inst.errno != errno.ENOENT:
122 122 raise
123 123 for f in repo._phasedefaults:
124 124 roots = f(repo, roots)
125 125 repo._dirtyphases = True
126 126 return roots
127 127
128 128 def writeroots(repo):
129 129 """Write phase roots from disk"""
130 130 f = repo.sopener('phaseroots', 'w', atomictemp=True)
131 131 try:
132 132 for phase, roots in enumerate(repo._phaseroots):
133 133 for h in roots:
134 134 f.write('%i %s\n' % (phase, hex(h)))
135 135 repo._dirtyphases = False
136 136 finally:
137 137 f.close()
138 138
139 139 def filterunknown(repo, phaseroots=None):
140 140 """remove unknown nodes from the phase boundary
141 141
142 142 no data is lost as unknown node only old data for their descentants
143 143 """
144 144 if phaseroots is None:
145 145 phaseroots = repo._phaseroots
146 146 nodemap = repo.changelog.nodemap # to filter unknown nodes
147 147 for phase, nodes in enumerate(phaseroots):
148 148 missing = [node for node in nodes if node not in nodemap]
149 149 if missing:
150 150 for mnode in missing:
151 151 repo.ui.debug(
152 152 'removing unknown node %s from %i-phase boundary\n'
153 153 % (short(mnode), phase))
154 154 nodes.symmetric_difference_update(missing)
155 155 repo._dirtyphases = True
156 156
157 157 def advanceboundary(repo, targetphase, nodes):
158 158 """Add nodes to a phase changing other nodes phases if necessary.
159 159
160 160 This function move boundary *forward* this means that all nodes are set
161 161 in the target phase or kept in a *lower* phase.
162 162
163 163 Simplify boundary to contains phase roots only."""
164 164 delroots = [] # set of root deleted by this path
165 165 for phase in xrange(targetphase + 1, len(allphases)):
166 166 # filter nodes that are not in a compatible phase already
167 167 # XXX rev phase cache might have been invalidated by a previous loop
168 168 # XXX we need to be smarter here
169 169 nodes = [n for n in nodes if repo[n].phase() >= phase]
170 170 if not nodes:
171 171 break # no roots to move anymore
172 172 roots = repo._phaseroots[phase]
173 173 olds = roots.copy()
174 174 ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
175 175 roots.clear()
176 176 roots.update(ctx.node() for ctx in ctxs)
177 177 if olds != roots:
178 178 # invalidate cache (we probably could be smarter here
179 179 if '_phaserev' in vars(repo):
180 180 del repo._phaserev
181 181 repo._dirtyphases = True
182 182 # some roots may need to be declared for lower phases
183 183 delroots.extend(olds - roots)
184 184 # declare deleted root in the target phase
185 185 if targetphase != 0:
186 186 retractboundary(repo, targetphase, delroots)
187 187
188 188
189 189 def retractboundary(repo, targetphase, nodes):
190 190 """Set nodes back to a phase changing other nodes phases if necessary.
191 191
192 192 This function move boundary *backward* this means that all nodes are set
193 193 in the target phase or kept in a *higher* phase.
194 194
195 195 Simplify boundary to contains phase roots only."""
196 196 currentroots = repo._phaseroots[targetphase]
197 197 newroots = [n for n in nodes if repo[n].phase() < targetphase]
198 198 if newroots:
199 199 currentroots.update(newroots)
200 200 ctxs = repo.set('roots(%ln::)', currentroots)
201 201 currentroots.intersection_update(ctx.node() for ctx in ctxs)
202 202 if '_phaserev' in vars(repo):
203 203 del repo._phaserev
204 204 repo._dirtyphases = True
205 205
206 206
207 207 def listphases(repo):
208 208 """List phases root for serialisation over pushkey"""
209 209 keys = {}
210 210 value = '%i' % draft
211 211 for root in repo._phaseroots[draft]:
212 212 keys[hex(root)] = value
213 213
214 214 if repo.ui.configbool('phases', 'publish', True):
215 215 # Add an extra data to let remote know we are a publishing repo.
216 216 # Publishing repo can't just pretend they are old repo. When pushing to
217 217 # a publishing repo, the client still need to push phase boundary
218 218 #
219 219 # Push do not only push changeset. It also push phase data. New
220 220 # phase data may apply to common changeset which won't be push (as they
221 221 # are common). Here is a very simple example:
222 222 #
223 223 # 1) repo A push changeset X as draft to repo B
224 224 # 2) repo B make changeset X public
225 225 # 3) repo B push to repo A. X is not pushed but the data that X as now
226 226 # public should
227 227 #
228 228 # The server can't handle it on it's own as it has no idea of client
229 229 # phase data.
230 230 keys['publishing'] = 'True'
231 231 return keys
232 232
233 233 def pushphase(repo, nhex, oldphasestr, newphasestr):
234 234 """List phases root for serialisation over pushkey"""
235 235 lock = repo.lock()
236 236 try:
237 237 currentphase = repo[nhex].phase()
238 238 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
239 239 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
240 240 if currentphase == oldphase and newphase < oldphase:
241 241 advanceboundary(repo, newphase, [bin(nhex)])
242 242 return 1
243 243 elif currentphase == newphase:
244 244 # raced, but got correct result
245 245 return 1
246 246 else:
247 247 return 0
248 248 finally:
249 249 lock.release()
250 250
251 251 def visibleheads(repo):
252 252 """return the set of visible head of this repo"""
253 253 # XXX we want a cache on this
254 254 sroots = repo._phaseroots[secret]
255 255 if sroots:
256 256 # XXX very slow revset. storing heads or secret "boundary" would help.
257 257 revset = repo.set('heads(not (%ln::))', sroots)
258 258
259 259 vheads = [ctx.node() for ctx in revset]
260 260 if not vheads:
261 261 vheads.append(nullid)
262 262 else:
263 263 vheads = repo.heads()
264 264 return vheads
265 265
266 266 def visiblebranchmap(repo):
267 267 """return a branchmap for the visible set"""
268 268 # XXX Recomputing this data on the fly is very slow. We should build a
269 269 # XXX cached version while computin the standard branchmap version.
270 270 sroots = repo._phaseroots[secret]
271 271 if sroots:
272 272 vbranchmap = {}
273 273 for branch, nodes in repo.branchmap().iteritems():
274 274 # search for secret heads.
275 275 for n in nodes:
276 276 if repo[n].phase() >= secret:
277 277 nodes = None
278 278 break
279 279 # if secreat heads where found we must compute them again
280 280 if nodes is None:
281 281 s = repo.set('heads(branch(%s) - secret())', branch)
282 282 nodes = [c.node() for c in s]
283 283 vbranchmap[branch] = nodes
284 284 else:
285 285 vbranchmap = repo.branchmap()
286 286 return vbranchmap
287 287
288 288 def analyzeremotephases(repo, subset, roots):
289 289 """Compute phases heads and root in a subset of node from root dict
290 290
291 291 * subset is heads of the subset
292 292 * roots is {<nodeid> => phase} mapping. key and value are string.
293 293
294 294 Accept unknown element input
295 295 """
296 296 # build list from dictionary
297 297 draftroots = []
298 298 nodemap = repo.changelog.nodemap # to filter unknown nodes
299 299 for nhex, phase in roots.iteritems():
300 300 if nhex == 'publishing': # ignore data related to publish option
301 301 continue
302 302 node = bin(nhex)
303 303 phase = int(phase)
304 304 if phase == 0:
305 305 if node != nullid:
306 306 repo.ui.warn(_('ignoring inconsistent public root'
307 307 ' from remote: %s\n') % nhex)
308 308 elif phase == 1:
309 309 if node in nodemap:
310 310 draftroots.append(node)
311 311 else:
312 312 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
313 313 % (phase, nhex))
314 314 # compute heads
315 315 publicheads = newheads(repo, subset, draftroots)
316 316 return publicheads, draftroots
317 317
318 318 def newheads(repo, heads, roots):
319 319 """compute new head of a subset minus another
320 320
321 321 * `heads`: define the first subset
322 322 * `rroots`: define the second we substract to the first"""
323 323 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
324 324 heads, roots, roots, heads)
325 325 return [c.node() for c in revset]
326 326
327 327
328 328 def newcommitphase(ui):
329 329 """helper to get the target phase of new commit
330 330
331 331 Handle all possible values for the phases.new-commit options.
332 332
333 333 """
334 334 v = ui.config('phases', 'new-commit', draft)
335 335 try:
336 336 return phasenames.index(v)
337 337 except ValueError:
338 338 try:
339 339 return int(v)
340 340 except ValueError:
341 341 msg = _("phases.new-commit: not a valid phase name ('%s')")
342 342 raise error.ConfigError(msg % v)
343 343
General Comments 0
You need to be logged in to leave comments. Login now