##// 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 """ Mercurial phases support code
1 """ Mercurial phases support code
2
2
3 ---
3 ---
4
4
5 Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
5 Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
6 Logilab SA <contact@logilab.fr>
6 Logilab SA <contact@logilab.fr>
7 Augie Fackler <durin42@gmail.com>
7 Augie Fackler <durin42@gmail.com>
8
8
9 This software may be used and distributed according to the terms of the
9 This software may be used and distributed according to the terms of the
10 GNU General Public License version 2 or any later version.
10 GNU General Public License version 2 or any later version.
11
11
12 ---
12 ---
13
13
14 This module implements most phase logic in mercurial.
14 This module implements most phase logic in mercurial.
15
15
16
16
17 Basic Concept
17 Basic Concept
18 =============
18 =============
19
19
20 A 'changeset phases' is an indicator that tells us how a changeset is
20 A 'changeset phases' is an indicator that tells us how a changeset is
21 manipulated and communicated. The details of each phase is described below,
21 manipulated and communicated. The details of each phase is described below,
22 here we describe the properties they have in common.
22 here we describe the properties they have in common.
23
23
24 Like bookmarks, phases are not stored in history and thus are not permanent and
24 Like bookmarks, phases are not stored in history and thus are not permanent and
25 leave no audit trail.
25 leave no audit trail.
26
26
27 First, no changeset can be in two phases at once. Phases are ordered, so they
27 First, no changeset can be in two phases at once. Phases are ordered, so they
28 can be considered from lowest to highest. The default, lowest phase is 'public'
28 can be considered from lowest to highest. The default, lowest phase is 'public'
29 - this is the normal phase of existing changesets. A child changeset can not be
29 - this is the normal phase of existing changesets. A child changeset can not be
30 in a lower phase than its parents.
30 in a lower phase than its parents.
31
31
32 These phases share a hierarchy of traits:
32 These phases share a hierarchy of traits:
33
33
34 immutable shared
34 immutable shared
35 public: X X
35 public: X X
36 draft: X
36 draft: X
37 secret:
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
41 Phase movement and exchange
42 ============================
42 ============================
43
43
44 Phase data are exchanged by pushkey on pull and push. Some server have a
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
45 publish option set, we call them publishing server. Pushing to such server make
46 draft changeset publish.
46 draft changeset publish.
47
47
48 A small list of fact/rules define the exchange of phase:
48 A small list of fact/rules define the exchange of phase:
49
49
50 * old client never changes server states
50 * old client never changes server states
51 * pull never changes server states
51 * pull never changes server states
52 * publish and old server csets are seen as public by client
52 * publish and old server csets are seen as public by client
53
53
54 * Any secret changeset seens in another repository is lowered to at least draft
54 * Any secret changeset seens in another repository is lowered to at least draft
55
55
56
56
57 Here is the final table summing up the 49 possible usecase of phase exchange:
57 Here is the final table summing up the 49 possible usecase of phase exchange:
58
58
59 server
59 server
60 old publish non-publish
60 old publish non-publish
61 N X N D P N D P
61 N X N D P N D P
62 old client
62 old client
63 pull
63 pull
64 N - X/X - X/D X/P - X/D X/P
64 N - X/X - X/D X/P - X/D X/P
65 X - X/X - X/D X/P - X/D X/P
65 X - X/X - X/D X/P - X/D X/P
66 push
66 push
67 X X/X X/X X/P X/P X/P X/D X/D X/P
67 X X/X X/X X/P X/P X/P X/D X/D X/P
68 new client
68 new client
69 pull
69 pull
70 N - P/X - P/D P/P - D/D P/P
70 N - P/X - P/D P/P - D/D P/P
71 D - P/X - P/D P/P - D/D P/P
71 D - P/X - P/D P/P - D/D P/P
72 P - P/X - P/D P/P - P/D P/P
72 P - P/X - P/D P/P - P/D P/P
73 push
73 push
74 D P/X P/X P/P P/P P/P D/D D/D P/P
74 D P/X P/X P/P P/P P/P D/D D/D P/P
75 P P/X P/X P/P P/P P/P P/P P/P P/P
75 P P/X P/X P/P P/P P/P P/P P/P P/P
76
76
77 Legend:
77 Legend:
78
78
79 A/B = final state on client / state on server
79 A/B = final state on client / state on server
80
80
81 * N = new/not present,
81 * N = new/not present,
82 * P = public,
82 * P = public,
83 * D = draft,
83 * D = draft,
84 * X = not tracked (ie: the old client or server has no internal way of
84 * X = not tracked (ie: the old client or server has no internal way of
85 recording the phase.)
85 recording the phase.)
86
86
87 passive = only pushes
87 passive = only pushes
88
88
89
89
90 A cell here can be read like this:
90 A cell here can be read like this:
91
91
92 "When a new client pushes a draft changeset (D) to a publishing server
92 "When a new client pushes a draft changeset (D) to a publishing server
93 where it's not present (N), it's marked public on both sides (P/P)."
93 where it's not present (N), it's marked public on both sides (P/P)."
94
94
95 Note: old client behave as publish server with Draft only content
95 Note: old client behave as publish server with Draft only content
96 - other people see it as public
96 - other people see it as public
97 - content is pushed as draft
97 - content is pushed as draft
98
98
99 """
99 """
100
100
101 import errno
101 import errno
102 from node import nullid, bin, hex, short
102 from node import nullid, bin, hex, short
103 from i18n import _
103 from i18n import _
104
104
105 allphases = public, draft, secret = range(3)
105 allphases = public, draft, secret = range(3)
106 trackedphases = allphases[1:]
106 trackedphases = allphases[1:]
107 phasenames = ['public', 'draft', 'secret']
107 phasenames = ['public', 'draft', 'secret']
108
108
109 def readroots(repo):
109 def readroots(repo):
110 """Read phase roots from disk"""
110 """Read phase roots from disk"""
111 roots = [set() for i in allphases]
111 roots = [set() for i in allphases]
112 try:
112 try:
113 f = repo.sopener('phaseroots')
113 f = repo.sopener('phaseroots')
114 try:
114 try:
115 for line in f:
115 for line in f:
116 phase, nh = line.strip().split()
116 phase, nh = line.strip().split()
117 roots[int(phase)].add(bin(nh))
117 roots[int(phase)].add(bin(nh))
118 finally:
118 finally:
119 f.close()
119 f.close()
120 except IOError, inst:
120 except IOError, inst:
121 if inst.errno != errno.ENOENT:
121 if inst.errno != errno.ENOENT:
122 raise
122 raise
123 for f in repo._phasedefaults:
123 for f in repo._phasedefaults:
124 roots = f(repo, roots)
124 roots = f(repo, roots)
125 repo._dirtyphases = True
125 repo._dirtyphases = True
126 return roots
126 return roots
127
127
128 def writeroots(repo):
128 def writeroots(repo):
129 """Write phase roots from disk"""
129 """Write phase roots from disk"""
130 f = repo.sopener('phaseroots', 'w', atomictemp=True)
130 f = repo.sopener('phaseroots', 'w', atomictemp=True)
131 try:
131 try:
132 for phase, roots in enumerate(repo._phaseroots):
132 for phase, roots in enumerate(repo._phaseroots):
133 for h in roots:
133 for h in roots:
134 f.write('%i %s\n' % (phase, hex(h)))
134 f.write('%i %s\n' % (phase, hex(h)))
135 repo._dirtyphases = False
135 repo._dirtyphases = False
136 finally:
136 finally:
137 f.close()
137 f.close()
138
138
139 def filterunknown(repo, phaseroots=None):
139 def filterunknown(repo, phaseroots=None):
140 """remove unknown nodes from the phase boundary
140 """remove unknown nodes from the phase boundary
141
141
142 no data is lost as unknown node only old data for their descentants
142 no data is lost as unknown node only old data for their descentants
143 """
143 """
144 if phaseroots is None:
144 if phaseroots is None:
145 phaseroots = repo._phaseroots
145 phaseroots = repo._phaseroots
146 nodemap = repo.changelog.nodemap # to filter unknown nodes
146 nodemap = repo.changelog.nodemap # to filter unknown nodes
147 for phase, nodes in enumerate(phaseroots):
147 for phase, nodes in enumerate(phaseroots):
148 missing = [node for node in nodes if node not in nodemap]
148 missing = [node for node in nodes if node not in nodemap]
149 if missing:
149 if missing:
150 for mnode in missing:
150 for mnode in missing:
151 msg = 'Removing unknown node %(n)s from %(p)i-phase boundary'
151 repo.ui.debug(
152 repo.ui.debug(msg, {'n': short(mnode), 'p': phase})
152 'removing unknown node %s from %i-phase boundary\n'
153 % (short(mnode), phase))
153 nodes.symmetric_difference_update(missing)
154 nodes.symmetric_difference_update(missing)
154 repo._dirtyphases = True
155 repo._dirtyphases = True
155
156
156 def advanceboundary(repo, targetphase, nodes):
157 def advanceboundary(repo, targetphase, nodes):
157 """Add nodes to a phase changing other nodes phases if necessary.
158 """Add nodes to a phase changing other nodes phases if necessary.
158
159
159 This function move boundary *forward* this means that all nodes are set
160 This function move boundary *forward* this means that all nodes are set
160 in the target phase or kept in a *lower* phase.
161 in the target phase or kept in a *lower* phase.
161
162
162 Simplify boundary to contains phase roots only."""
163 Simplify boundary to contains phase roots only."""
163 delroots = [] # set of root deleted by this path
164 delroots = [] # set of root deleted by this path
164 for phase in xrange(targetphase + 1, len(allphases)):
165 for phase in xrange(targetphase + 1, len(allphases)):
165 # filter nodes that are not in a compatible phase already
166 # filter nodes that are not in a compatible phase already
166 # XXX rev phase cache might have been invalidated by a previous loop
167 # XXX rev phase cache might have been invalidated by a previous loop
167 # XXX we need to be smarter here
168 # XXX we need to be smarter here
168 nodes = [n for n in nodes if repo[n].phase() >= phase]
169 nodes = [n for n in nodes if repo[n].phase() >= phase]
169 if not nodes:
170 if not nodes:
170 break # no roots to move anymore
171 break # no roots to move anymore
171 roots = repo._phaseroots[phase]
172 roots = repo._phaseroots[phase]
172 olds = roots.copy()
173 olds = roots.copy()
173 ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
174 ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
174 roots.clear()
175 roots.clear()
175 roots.update(ctx.node() for ctx in ctxs)
176 roots.update(ctx.node() for ctx in ctxs)
176 if olds != roots:
177 if olds != roots:
177 # invalidate cache (we probably could be smarter here
178 # invalidate cache (we probably could be smarter here
178 if '_phaserev' in vars(repo):
179 if '_phaserev' in vars(repo):
179 del repo._phaserev
180 del repo._phaserev
180 repo._dirtyphases = True
181 repo._dirtyphases = True
181 # some roots may need to be declared for lower phases
182 # some roots may need to be declared for lower phases
182 delroots.extend(olds - roots)
183 delroots.extend(olds - roots)
183 # declare deleted root in the target phase
184 # declare deleted root in the target phase
184 if targetphase != 0:
185 if targetphase != 0:
185 retractboundary(repo, targetphase, delroots)
186 retractboundary(repo, targetphase, delroots)
186
187
187
188
188 def retractboundary(repo, targetphase, nodes):
189 def retractboundary(repo, targetphase, nodes):
189 """Set nodes back to a phase changing other nodes phases if necessary.
190 """Set nodes back to a phase changing other nodes phases if necessary.
190
191
191 This function move boundary *backward* this means that all nodes are set
192 This function move boundary *backward* this means that all nodes are set
192 in the target phase or kept in a *higher* phase.
193 in the target phase or kept in a *higher* phase.
193
194
194 Simplify boundary to contains phase roots only."""
195 Simplify boundary to contains phase roots only."""
195 currentroots = repo._phaseroots[targetphase]
196 currentroots = repo._phaseroots[targetphase]
196 newroots = [n for n in nodes if repo[n].phase() < targetphase]
197 newroots = [n for n in nodes if repo[n].phase() < targetphase]
197 if newroots:
198 if newroots:
198 currentroots.update(newroots)
199 currentroots.update(newroots)
199 ctxs = repo.set('roots(%ln::)', currentroots)
200 ctxs = repo.set('roots(%ln::)', currentroots)
200 currentroots.intersection_update(ctx.node() for ctx in ctxs)
201 currentroots.intersection_update(ctx.node() for ctx in ctxs)
201 if '_phaserev' in vars(repo):
202 if '_phaserev' in vars(repo):
202 del repo._phaserev
203 del repo._phaserev
203 repo._dirtyphases = True
204 repo._dirtyphases = True
204
205
205
206
206 def listphases(repo):
207 def listphases(repo):
207 """List phases root for serialisation over pushkey"""
208 """List phases root for serialisation over pushkey"""
208 keys = {}
209 keys = {}
209 value = '%i' % draft
210 value = '%i' % draft
210 for root in repo._phaseroots[draft]:
211 for root in repo._phaseroots[draft]:
211 keys[hex(root)] = value
212 keys[hex(root)] = value
212
213
213 if repo.ui.configbool('phases', 'publish', True):
214 if repo.ui.configbool('phases', 'publish', True):
214 # Add an extra data to let remote know we are a publishing repo.
215 # Add an extra data to let remote know we are a publishing repo.
215 # Publishing repo can't just pretend they are old repo. When pushing to
216 # Publishing repo can't just pretend they are old repo. When pushing to
216 # a publishing repo, the client still need to push phase boundary
217 # a publishing repo, the client still need to push phase boundary
217 #
218 #
218 # Push do not only push changeset. It also push phase data. New
219 # Push do not only push changeset. It also push phase data. New
219 # phase data may apply to common changeset which won't be push (as they
220 # phase data may apply to common changeset which won't be push (as they
220 # are common). Here is a very simple example:
221 # are common). Here is a very simple example:
221 #
222 #
222 # 1) repo A push changeset X as draft to repo B
223 # 1) repo A push changeset X as draft to repo B
223 # 2) repo B make changeset X public
224 # 2) repo B make changeset X public
224 # 3) repo B push to repo A. X is not pushed but the data that X as now
225 # 3) repo B push to repo A. X is not pushed but the data that X as now
225 # public should
226 # public should
226 #
227 #
227 # The server can't handle it on it's own as it has no idea of client
228 # The server can't handle it on it's own as it has no idea of client
228 # phase data.
229 # phase data.
229 keys['publishing'] = 'True'
230 keys['publishing'] = 'True'
230 return keys
231 return keys
231
232
232 def pushphase(repo, nhex, oldphasestr, newphasestr):
233 def pushphase(repo, nhex, oldphasestr, newphasestr):
233 """List phases root for serialisation over pushkey"""
234 """List phases root for serialisation over pushkey"""
234 lock = repo.lock()
235 lock = repo.lock()
235 try:
236 try:
236 currentphase = repo[nhex].phase()
237 currentphase = repo[nhex].phase()
237 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
238 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
238 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
239 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
239 if currentphase == oldphase and newphase < oldphase:
240 if currentphase == oldphase and newphase < oldphase:
240 advanceboundary(repo, newphase, [bin(nhex)])
241 advanceboundary(repo, newphase, [bin(nhex)])
241 return 1
242 return 1
242 elif currentphase == newphase:
243 elif currentphase == newphase:
243 # raced, but got correct result
244 # raced, but got correct result
244 return 1
245 return 1
245 else:
246 else:
246 return 0
247 return 0
247 finally:
248 finally:
248 lock.release()
249 lock.release()
249
250
250 def visibleheads(repo):
251 def visibleheads(repo):
251 """return the set of visible head of this repo"""
252 """return the set of visible head of this repo"""
252 # XXX we want a cache on this
253 # XXX we want a cache on this
253 sroots = repo._phaseroots[secret]
254 sroots = repo._phaseroots[secret]
254 if sroots:
255 if sroots:
255 # XXX very slow revset. storing heads or secret "boundary" would help.
256 # XXX very slow revset. storing heads or secret "boundary" would help.
256 revset = repo.set('heads(not (%ln::))', sroots)
257 revset = repo.set('heads(not (%ln::))', sroots)
257
258
258 vheads = [ctx.node() for ctx in revset]
259 vheads = [ctx.node() for ctx in revset]
259 if not vheads:
260 if not vheads:
260 vheads.append(nullid)
261 vheads.append(nullid)
261 else:
262 else:
262 vheads = repo.heads()
263 vheads = repo.heads()
263 return vheads
264 return vheads
264
265
265 def analyzeremotephases(repo, subset, roots):
266 def analyzeremotephases(repo, subset, roots):
266 """Compute phases heads and root in a subset of node from root dict
267 """Compute phases heads and root in a subset of node from root dict
267
268
268 * subset is heads of the subset
269 * subset is heads of the subset
269 * roots is {<nodeid> => phase} mapping. key and value are string.
270 * roots is {<nodeid> => phase} mapping. key and value are string.
270
271
271 Accept unknown element input
272 Accept unknown element input
272 """
273 """
273 # build list from dictionary
274 # build list from dictionary
274 draftroots = []
275 draftroots = []
275 nodemap = repo.changelog.nodemap # to filter unknown nodes
276 nodemap = repo.changelog.nodemap # to filter unknown nodes
276 for nhex, phase in roots.iteritems():
277 for nhex, phase in roots.iteritems():
277 if nhex == 'publishing': # ignore data related to publish option
278 if nhex == 'publishing': # ignore data related to publish option
278 continue
279 continue
279 node = bin(nhex)
280 node = bin(nhex)
280 phase = int(phase)
281 phase = int(phase)
281 if phase == 0:
282 if phase == 0:
282 if node != nullid:
283 if node != nullid:
283 repo.ui.warn(_('ignoring inconsistent public root'
284 repo.ui.warn(_('ignoring inconsistent public root'
284 ' from remote: %s\n') % nhex)
285 ' from remote: %s\n') % nhex)
285 elif phase == 1:
286 elif phase == 1:
286 if node in nodemap:
287 if node in nodemap:
287 draftroots.append(node)
288 draftroots.append(node)
288 else:
289 else:
289 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
290 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
290 % (phase, nhex))
291 % (phase, nhex))
291 # compute heads
292 # compute heads
292 publicheads = newheads(repo, subset, draftroots)
293 publicheads = newheads(repo, subset, draftroots)
293 return publicheads, draftroots
294 return publicheads, draftroots
294
295
295 def newheads(repo, heads, roots):
296 def newheads(repo, heads, roots):
296 """compute new head of a subset minus another
297 """compute new head of a subset minus another
297
298
298 * `heads`: define the first subset
299 * `heads`: define the first subset
299 * `rroots`: define the second we substract to the first"""
300 * `rroots`: define the second we substract to the first"""
300 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
301 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
301 heads, roots, roots, heads)
302 heads, roots, roots, heads)
302 return [c.node() for c in revset]
303 return [c.node() for c in revset]
303
304
304
305
305 def newcommitphase(ui):
306 def newcommitphase(ui):
306 """helper to get the target phase of new commit
307 """helper to get the target phase of new commit
307
308
308 Handle all possible values for the phases.new-commit options.
309 Handle all possible values for the phases.new-commit options.
309
310
310 """
311 """
311 v = ui.config('phases', 'new-commit', draft)
312 v = ui.config('phases', 'new-commit', draft)
312 try:
313 try:
313 return phasenames.index(v)
314 return phasenames.index(v)
314 except ValueError:
315 except ValueError:
315 try:
316 try:
316 return int(v)
317 return int(v)
317 except ValueError:
318 except ValueError:
318 msg = _("phases.new-commit: not a valid phase name ('%s')")
319 msg = _("phases.new-commit: not a valid phase name ('%s')")
319 raise error.ConfigError(msg % v)
320 raise error.ConfigError(msg % v)
320
321
General Comments 0
You need to be logged in to leave comments. Login now