##// END OF EJS Templates
phases: fix a non-standard debug message...
Greg Ward -
r16293:bc1d9492 stable
parent child Browse files
Show More
@@ -1,320 +1,321 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 116 phase, nh = line.strip().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 msg = 'Removing unknown node %(n)s from %(p)i-phase boundary'
152 repo.ui.debug(msg, {'n': short(mnode), 'p': phase})
151 repo.ui.debug(
152 'removing unknown node %s from %i-phase boundary\n'
153 % (short(mnode), phase))
153 154 nodes.symmetric_difference_update(missing)
154 155 repo._dirtyphases = True
155 156
156 157 def advanceboundary(repo, targetphase, nodes):
157 158 """Add nodes to a phase changing other nodes phases if necessary.
158 159
159 160 This function move boundary *forward* this means that all nodes are set
160 161 in the target phase or kept in a *lower* phase.
161 162
162 163 Simplify boundary to contains phase roots only."""
163 164 delroots = [] # set of root deleted by this path
164 165 for phase in xrange(targetphase + 1, len(allphases)):
165 166 # filter nodes that are not in a compatible phase already
166 167 # XXX rev phase cache might have been invalidated by a previous loop
167 168 # XXX we need to be smarter here
168 169 nodes = [n for n in nodes if repo[n].phase() >= phase]
169 170 if not nodes:
170 171 break # no roots to move anymore
171 172 roots = repo._phaseroots[phase]
172 173 olds = roots.copy()
173 174 ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
174 175 roots.clear()
175 176 roots.update(ctx.node() for ctx in ctxs)
176 177 if olds != roots:
177 178 # invalidate cache (we probably could be smarter here
178 179 if '_phaserev' in vars(repo):
179 180 del repo._phaserev
180 181 repo._dirtyphases = True
181 182 # some roots may need to be declared for lower phases
182 183 delroots.extend(olds - roots)
183 184 # declare deleted root in the target phase
184 185 if targetphase != 0:
185 186 retractboundary(repo, targetphase, delroots)
186 187
187 188
188 189 def retractboundary(repo, targetphase, nodes):
189 190 """Set nodes back to a phase changing other nodes phases if necessary.
190 191
191 192 This function move boundary *backward* this means that all nodes are set
192 193 in the target phase or kept in a *higher* phase.
193 194
194 195 Simplify boundary to contains phase roots only."""
195 196 currentroots = repo._phaseroots[targetphase]
196 197 newroots = [n for n in nodes if repo[n].phase() < targetphase]
197 198 if newroots:
198 199 currentroots.update(newroots)
199 200 ctxs = repo.set('roots(%ln::)', currentroots)
200 201 currentroots.intersection_update(ctx.node() for ctx in ctxs)
201 202 if '_phaserev' in vars(repo):
202 203 del repo._phaserev
203 204 repo._dirtyphases = True
204 205
205 206
206 207 def listphases(repo):
207 208 """List phases root for serialisation over pushkey"""
208 209 keys = {}
209 210 value = '%i' % draft
210 211 for root in repo._phaseroots[draft]:
211 212 keys[hex(root)] = value
212 213
213 214 if repo.ui.configbool('phases', 'publish', True):
214 215 # Add an extra data to let remote know we are a publishing repo.
215 216 # Publishing repo can't just pretend they are old repo. When pushing to
216 217 # a publishing repo, the client still need to push phase boundary
217 218 #
218 219 # Push do not only push changeset. It also push phase data. New
219 220 # phase data may apply to common changeset which won't be push (as they
220 221 # are common). Here is a very simple example:
221 222 #
222 223 # 1) repo A push changeset X as draft to repo B
223 224 # 2) repo B make changeset X public
224 225 # 3) repo B push to repo A. X is not pushed but the data that X as now
225 226 # public should
226 227 #
227 228 # The server can't handle it on it's own as it has no idea of client
228 229 # phase data.
229 230 keys['publishing'] = 'True'
230 231 return keys
231 232
232 233 def pushphase(repo, nhex, oldphasestr, newphasestr):
233 234 """List phases root for serialisation over pushkey"""
234 235 lock = repo.lock()
235 236 try:
236 237 currentphase = repo[nhex].phase()
237 238 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
238 239 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
239 240 if currentphase == oldphase and newphase < oldphase:
240 241 advanceboundary(repo, newphase, [bin(nhex)])
241 242 return 1
242 243 elif currentphase == newphase:
243 244 # raced, but got correct result
244 245 return 1
245 246 else:
246 247 return 0
247 248 finally:
248 249 lock.release()
249 250
250 251 def visibleheads(repo):
251 252 """return the set of visible head of this repo"""
252 253 # XXX we want a cache on this
253 254 sroots = repo._phaseroots[secret]
254 255 if sroots:
255 256 # XXX very slow revset. storing heads or secret "boundary" would help.
256 257 revset = repo.set('heads(not (%ln::))', sroots)
257 258
258 259 vheads = [ctx.node() for ctx in revset]
259 260 if not vheads:
260 261 vheads.append(nullid)
261 262 else:
262 263 vheads = repo.heads()
263 264 return vheads
264 265
265 266 def analyzeremotephases(repo, subset, roots):
266 267 """Compute phases heads and root in a subset of node from root dict
267 268
268 269 * subset is heads of the subset
269 270 * roots is {<nodeid> => phase} mapping. key and value are string.
270 271
271 272 Accept unknown element input
272 273 """
273 274 # build list from dictionary
274 275 draftroots = []
275 276 nodemap = repo.changelog.nodemap # to filter unknown nodes
276 277 for nhex, phase in roots.iteritems():
277 278 if nhex == 'publishing': # ignore data related to publish option
278 279 continue
279 280 node = bin(nhex)
280 281 phase = int(phase)
281 282 if phase == 0:
282 283 if node != nullid:
283 284 repo.ui.warn(_('ignoring inconsistent public root'
284 285 ' from remote: %s\n') % nhex)
285 286 elif phase == 1:
286 287 if node in nodemap:
287 288 draftroots.append(node)
288 289 else:
289 290 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
290 291 % (phase, nhex))
291 292 # compute heads
292 293 publicheads = newheads(repo, subset, draftroots)
293 294 return publicheads, draftroots
294 295
295 296 def newheads(repo, heads, roots):
296 297 """compute new head of a subset minus another
297 298
298 299 * `heads`: define the first subset
299 300 * `rroots`: define the second we substract to the first"""
300 301 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
301 302 heads, roots, roots, heads)
302 303 return [c.node() for c in revset]
303 304
304 305
305 306 def newcommitphase(ui):
306 307 """helper to get the target phase of new commit
307 308
308 309 Handle all possible values for the phases.new-commit options.
309 310
310 311 """
311 312 v = ui.config('phases', 'new-commit', draft)
312 313 try:
313 314 return phasenames.index(v)
314 315 except ValueError:
315 316 try:
316 317 return int(v)
317 318 except ValueError:
318 319 msg = _("phases.new-commit: not a valid phase name ('%s')")
319 320 raise error.ConfigError(msg % v)
320 321
General Comments 0
You need to be logged in to leave comments. Login now