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