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