##// END OF EJS Templates
phases: update doc to mention secret phase
Pierre-Yves David -
r15705:e34f4d1f default
parent child Browse files
Show More
@@ -1,279 +1,282 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
38
38 local commits are draft by default
39 local commits are draft by default
39
40
40 Phase movement and exchange
41 Phase movement and exchange
41 ============================
42 ============================
42
43
43 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
44 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
45 draft changeset publish.
46 draft changeset publish.
46
47
47 A small list of fact/rules define the exchange of phase:
48 A small list of fact/rules define the exchange of phase:
48
49
49 * old client never changes server states
50 * old client never changes server states
50 * pull never changes server states
51 * pull never changes server states
51 * publish and old server csets are seen as public by client
52 * publish and old server csets are seen as public by client
52
53
54 * Any secret changeset seens in another repository is lowered to at least draft
55
53
56
54 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:
55
58
56 server
59 server
57 old publish non-publish
60 old publish non-publish
58 N X N D P N D P
61 N X N D P N D P
59 old client
62 old client
60 pull
63 pull
61 N - X/X - X/D X/P - X/D X/P
64 N - X/X - X/D X/P - X/D X/P
62 X - X/X - X/D X/P - X/D X/P
65 X - X/X - X/D X/P - X/D X/P
63 push
66 push
64 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
65 new client
68 new client
66 pull
69 pull
67 N - P/X - P/D P/P - D/D P/P
70 N - P/X - P/D P/P - D/D P/P
68 D - P/X - P/D P/P - D/D P/P
71 D - P/X - P/D P/P - D/D P/P
69 P - P/X - P/D P/P - P/D P/P
72 P - P/X - P/D P/P - P/D P/P
70 push
73 push
71 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
72 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
73
76
74 Legend:
77 Legend:
75
78
76 A/B = final state on client / state on server
79 A/B = final state on client / state on server
77
80
78 * N = new/not present,
81 * N = new/not present,
79 * P = public,
82 * P = public,
80 * D = draft,
83 * D = draft,
81 * 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
82 recording the phase.)
85 recording the phase.)
83
86
84 passive = only pushes
87 passive = only pushes
85
88
86
89
87 A cell here can be read like this:
90 A cell here can be read like this:
88
91
89 "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
90 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)."
91
94
92 Note: old client behave as publish server with Draft only content
95 Note: old client behave as publish server with Draft only content
93 - other people see it as public
96 - other people see it as public
94 - content is pushed as draft
97 - content is pushed as draft
95
98
96 """
99 """
97
100
98 import errno
101 import errno
99 from node import nullid, bin, hex, short
102 from node import nullid, bin, hex, short
100 from i18n import _
103 from i18n import _
101
104
102 allphases = range(3)
105 allphases = range(3)
103 trackedphases = allphases[1:]
106 trackedphases = allphases[1:]
104
107
105 def readroots(repo):
108 def readroots(repo):
106 """Read phase roots from disk"""
109 """Read phase roots from disk"""
107 roots = [set() for i in allphases]
110 roots = [set() for i in allphases]
108 roots[0].add(nullid)
111 roots[0].add(nullid)
109 try:
112 try:
110 f = repo.sopener('phaseroots')
113 f = repo.sopener('phaseroots')
111 try:
114 try:
112 for line in f:
115 for line in f:
113 phase, nh = line.strip().split()
116 phase, nh = line.strip().split()
114 roots[int(phase)].add(bin(nh))
117 roots[int(phase)].add(bin(nh))
115 finally:
118 finally:
116 f.close()
119 f.close()
117 except IOError, inst:
120 except IOError, inst:
118 if inst.errno != errno.ENOENT:
121 if inst.errno != errno.ENOENT:
119 raise
122 raise
120 return roots
123 return roots
121
124
122 def writeroots(repo):
125 def writeroots(repo):
123 """Write phase roots from disk"""
126 """Write phase roots from disk"""
124 f = repo.sopener('phaseroots', 'w', atomictemp=True)
127 f = repo.sopener('phaseroots', 'w', atomictemp=True)
125 try:
128 try:
126 for phase, roots in enumerate(repo._phaseroots):
129 for phase, roots in enumerate(repo._phaseroots):
127 for h in roots:
130 for h in roots:
128 f.write('%i %s\n' % (phase, hex(h)))
131 f.write('%i %s\n' % (phase, hex(h)))
129 repo._dirtyphases = False
132 repo._dirtyphases = False
130 finally:
133 finally:
131 f.close()
134 f.close()
132
135
133 def filterunknown(repo, phaseroots=None):
136 def filterunknown(repo, phaseroots=None):
134 """remove unknown nodes from the phase boundary
137 """remove unknown nodes from the phase boundary
135
138
136 no data is lost as unknown node only old data for their descentants
139 no data is lost as unknown node only old data for their descentants
137 """
140 """
138 if phaseroots is None:
141 if phaseroots is None:
139 phaseroots = repo._phaseroots
142 phaseroots = repo._phaseroots
140 for phase, nodes in enumerate(phaseroots):
143 for phase, nodes in enumerate(phaseroots):
141 missing = [node for node in nodes if node not in repo]
144 missing = [node for node in nodes if node not in repo]
142 if missing:
145 if missing:
143 for mnode in missing:
146 for mnode in missing:
144 msg = _('Removing unknown node %(n)s from %(p)i-phase boundary')
147 msg = _('Removing unknown node %(n)s from %(p)i-phase boundary')
145 repo.ui.debug(msg, {'n': short(mnode), 'p': phase})
148 repo.ui.debug(msg, {'n': short(mnode), 'p': phase})
146 nodes.symmetric_difference_update(missing)
149 nodes.symmetric_difference_update(missing)
147 repo._dirtyphases = True
150 repo._dirtyphases = True
148
151
149 def advanceboundary(repo, targetphase, nodes):
152 def advanceboundary(repo, targetphase, nodes):
150 """Add nodes to a phase changing other nodes phases if necessary.
153 """Add nodes to a phase changing other nodes phases if necessary.
151
154
152 This function move boundary *forward* this means that all nodes are set
155 This function move boundary *forward* this means that all nodes are set
153 in the target phase or kept in a *lower* phase.
156 in the target phase or kept in a *lower* phase.
154
157
155 Simplify boundary to contains phase roots only."""
158 Simplify boundary to contains phase roots only."""
156 delroots = [] # set of root deleted by this path
159 delroots = [] # set of root deleted by this path
157 for phase in xrange(targetphase + 1, len(allphases)):
160 for phase in xrange(targetphase + 1, len(allphases)):
158 # filter nodes that are not in a compatible phase already
161 # filter nodes that are not in a compatible phase already
159 # XXX rev phase cache might have been invalidated by a previous loop
162 # XXX rev phase cache might have been invalidated by a previous loop
160 # XXX we need to be smarter here
163 # XXX we need to be smarter here
161 nodes = [n for n in nodes if repo[n].phase() >= phase]
164 nodes = [n for n in nodes if repo[n].phase() >= phase]
162 if not nodes:
165 if not nodes:
163 break # no roots to move anymore
166 break # no roots to move anymore
164 roots = repo._phaseroots[phase]
167 roots = repo._phaseroots[phase]
165 olds = roots.copy()
168 olds = roots.copy()
166 ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
169 ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
167 roots.clear()
170 roots.clear()
168 roots.update(ctx.node() for ctx in ctxs)
171 roots.update(ctx.node() for ctx in ctxs)
169 if olds != roots:
172 if olds != roots:
170 # invalidate cache (we probably could be smarter here
173 # invalidate cache (we probably could be smarter here
171 if '_phaserev' in vars(repo):
174 if '_phaserev' in vars(repo):
172 del repo._phaserev
175 del repo._phaserev
173 repo._dirtyphases = True
176 repo._dirtyphases = True
174 # some roots may need to be declared for lower phases
177 # some roots may need to be declared for lower phases
175 delroots.extend(olds - roots)
178 delroots.extend(olds - roots)
176 # declare deleted root in the target phase
179 # declare deleted root in the target phase
177 if targetphase != 0:
180 if targetphase != 0:
178 retractboundary(repo, targetphase, delroots)
181 retractboundary(repo, targetphase, delroots)
179
182
180
183
181 def retractboundary(repo, targetphase, nodes):
184 def retractboundary(repo, targetphase, nodes):
182 """Set nodes back to a phase changing other nodes phases if necessary.
185 """Set nodes back to a phase changing other nodes phases if necessary.
183
186
184 This function move boundary *backward* this means that all nodes are set
187 This function move boundary *backward* this means that all nodes are set
185 in the target phase or kept in a *higher* phase.
188 in the target phase or kept in a *higher* phase.
186
189
187 Simplify boundary to contains phase roots only."""
190 Simplify boundary to contains phase roots only."""
188 currentroots = repo._phaseroots[targetphase]
191 currentroots = repo._phaseroots[targetphase]
189 newroots = [n for n in nodes if repo[n].phase() < targetphase]
192 newroots = [n for n in nodes if repo[n].phase() < targetphase]
190 if newroots:
193 if newroots:
191 currentroots.update(newroots)
194 currentroots.update(newroots)
192 ctxs = repo.set('roots(%ln::)', currentroots)
195 ctxs = repo.set('roots(%ln::)', currentroots)
193 currentroots.intersection_update(ctx.node() for ctx in ctxs)
196 currentroots.intersection_update(ctx.node() for ctx in ctxs)
194 if '_phaserev' in vars(repo):
197 if '_phaserev' in vars(repo):
195 del repo._phaserev
198 del repo._phaserev
196 repo._dirtyphases = True
199 repo._dirtyphases = True
197
200
198
201
199 def listphases(repo):
202 def listphases(repo):
200 """List phases root for serialisation over pushkey"""
203 """List phases root for serialisation over pushkey"""
201 keys = {}
204 keys = {}
202 for phase in trackedphases:
205 for phase in trackedphases:
203 for root in repo._phaseroots[phase]:
206 for root in repo._phaseroots[phase]:
204 keys[hex(root)] = '%i' % phase
207 keys[hex(root)] = '%i' % phase
205 if repo.ui.configbool('phases', 'publish', True):
208 if repo.ui.configbool('phases', 'publish', True):
206 # Add an extra data to let remote know we are a publishing repo.
209 # Add an extra data to let remote know we are a publishing repo.
207 # Publishing repo can't just pretend they are old repo. When pushing to
210 # Publishing repo can't just pretend they are old repo. When pushing to
208 # a publishing repo, the client still need to push phase boundary
211 # a publishing repo, the client still need to push phase boundary
209 #
212 #
210 # Push do not only push changeset. It also push phase data. New
213 # Push do not only push changeset. It also push phase data. New
211 # phase data may apply to common changeset which won't be push (as they
214 # phase data may apply to common changeset which won't be push (as they
212 # are common). Here is a very simple example:
215 # are common). Here is a very simple example:
213 #
216 #
214 # 1) repo A push changeset X as draft to repo B
217 # 1) repo A push changeset X as draft to repo B
215 # 2) repo B make changeset X public
218 # 2) repo B make changeset X public
216 # 3) repo B push to repo A. X is not pushed but the data that X as now
219 # 3) repo B push to repo A. X is not pushed but the data that X as now
217 # public should
220 # public should
218 #
221 #
219 # The server can't handle it on it's own as it has no idea of client
222 # The server can't handle it on it's own as it has no idea of client
220 # phase data.
223 # phase data.
221 keys['publishing'] = 'True'
224 keys['publishing'] = 'True'
222 return keys
225 return keys
223
226
224 def pushphase(repo, nhex, oldphasestr, newphasestr):
227 def pushphase(repo, nhex, oldphasestr, newphasestr):
225 """List phases root for serialisation over pushkey"""
228 """List phases root for serialisation over pushkey"""
226 lock = repo.lock()
229 lock = repo.lock()
227 try:
230 try:
228 currentphase = repo[nhex].phase()
231 currentphase = repo[nhex].phase()
229 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
232 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
230 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
233 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
231 if currentphase == oldphase and newphase < oldphase:
234 if currentphase == oldphase and newphase < oldphase:
232 advanceboundary(repo, newphase, [bin(nhex)])
235 advanceboundary(repo, newphase, [bin(nhex)])
233 return 1
236 return 1
234 else:
237 else:
235 return 0
238 return 0
236 finally:
239 finally:
237 lock.release()
240 lock.release()
238
241
239 def visibleheads(repo):
242 def visibleheads(repo):
240 """return the set of visible head of this repo"""
243 """return the set of visible head of this repo"""
241 # XXX we want a cache on this
244 # XXX we want a cache on this
242 sroots = repo._phaseroots[2]
245 sroots = repo._phaseroots[2]
243 if sroots:
246 if sroots:
244 # XXX very slow revset. storing heads or secret "boundary" would help.
247 # XXX very slow revset. storing heads or secret "boundary" would help.
245 revset = repo.set('heads(not (%ln::))', sroots)
248 revset = repo.set('heads(not (%ln::))', sroots)
246
249
247 vheads = [ctx.node() for ctx in revset]
250 vheads = [ctx.node() for ctx in revset]
248 if not vheads:
251 if not vheads:
249 vheads.append(nullid)
252 vheads.append(nullid)
250 else:
253 else:
251 vheads = repo.heads()
254 vheads = repo.heads()
252 return vheads
255 return vheads
253
256
254 def analyzeremotephases(repo, subset, roots):
257 def analyzeremotephases(repo, subset, roots):
255 """Compute phases heads and root in a subset of node from root dict
258 """Compute phases heads and root in a subset of node from root dict
256
259
257 * subset is heads of the subset
260 * subset is heads of the subset
258 * roots is {<nodeid> => phase} mapping. key and value are string.
261 * roots is {<nodeid> => phase} mapping. key and value are string.
259
262
260 Accept unknown element input
263 Accept unknown element input
261 """
264 """
262 # build list from dictionary
265 # build list from dictionary
263 phaseroots = [[] for p in allphases]
266 phaseroots = [[] for p in allphases]
264 for nhex, phase in roots.iteritems():
267 for nhex, phase in roots.iteritems():
265 if nhex == 'publishing': # ignore data related to publish option
268 if nhex == 'publishing': # ignore data related to publish option
266 continue
269 continue
267 node = bin(nhex)
270 node = bin(nhex)
268 phase = int(phase)
271 phase = int(phase)
269 if node in repo:
272 if node in repo:
270 phaseroots[phase].append(node)
273 phaseroots[phase].append(node)
271 # compute heads
274 # compute heads
272 phaseheads = [[] for p in allphases]
275 phaseheads = [[] for p in allphases]
273 for phase in allphases[:-1]:
276 for phase in allphases[:-1]:
274 toproof = phaseroots[phase + 1]
277 toproof = phaseroots[phase + 1]
275 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
278 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
276 subset, toproof, toproof, subset)
279 subset, toproof, toproof, subset)
277 phaseheads[phase].extend(c.node() for c in revset)
280 phaseheads[phase].extend(c.node() for c in revset)
278 return phaseheads, phaseroots
281 return phaseheads, phaseroots
279
282
General Comments 0
You need to be logged in to leave comments. Login now