##// END OF EJS Templates
phases: add list of string to access phase name
Pierre-Yves David -
r15821:e3ee8bf5 default
parent child Browse files
Show More
@@ -1,282 +1,283 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
108
108 def readroots(repo):
109 def readroots(repo):
109 """Read phase roots from disk"""
110 """Read phase roots from disk"""
110 roots = [set() for i in allphases]
111 roots = [set() for i in allphases]
111 roots[0].add(nullid)
112 roots[0].add(nullid)
112 try:
113 try:
113 f = repo.sopener('phaseroots')
114 f = repo.sopener('phaseroots')
114 try:
115 try:
115 for line in f:
116 for line in f:
116 phase, nh = line.strip().split()
117 phase, nh = line.strip().split()
117 roots[int(phase)].add(bin(nh))
118 roots[int(phase)].add(bin(nh))
118 finally:
119 finally:
119 f.close()
120 f.close()
120 except IOError, inst:
121 except IOError, inst:
121 if inst.errno != errno.ENOENT:
122 if inst.errno != errno.ENOENT:
122 raise
123 raise
123 return roots
124 return roots
124
125
125 def writeroots(repo):
126 def writeroots(repo):
126 """Write phase roots from disk"""
127 """Write phase roots from disk"""
127 f = repo.sopener('phaseroots', 'w', atomictemp=True)
128 f = repo.sopener('phaseroots', 'w', atomictemp=True)
128 try:
129 try:
129 for phase, roots in enumerate(repo._phaseroots):
130 for phase, roots in enumerate(repo._phaseroots):
130 for h in roots:
131 for h in roots:
131 f.write('%i %s\n' % (phase, hex(h)))
132 f.write('%i %s\n' % (phase, hex(h)))
132 repo._dirtyphases = False
133 repo._dirtyphases = False
133 finally:
134 finally:
134 f.close()
135 f.close()
135
136
136 def filterunknown(repo, phaseroots=None):
137 def filterunknown(repo, phaseroots=None):
137 """remove unknown nodes from the phase boundary
138 """remove unknown nodes from the phase boundary
138
139
139 no data is lost as unknown node only old data for their descentants
140 no data is lost as unknown node only old data for their descentants
140 """
141 """
141 if phaseroots is None:
142 if phaseroots is None:
142 phaseroots = repo._phaseroots
143 phaseroots = repo._phaseroots
143 for phase, nodes in enumerate(phaseroots):
144 for phase, nodes in enumerate(phaseroots):
144 missing = [node for node in nodes if node not in repo]
145 missing = [node for node in nodes if node not in repo]
145 if missing:
146 if missing:
146 for mnode in missing:
147 for mnode in missing:
147 msg = _('Removing unknown node %(n)s from %(p)i-phase boundary')
148 msg = _('Removing unknown node %(n)s from %(p)i-phase boundary')
148 repo.ui.debug(msg, {'n': short(mnode), 'p': phase})
149 repo.ui.debug(msg, {'n': short(mnode), 'p': phase})
149 nodes.symmetric_difference_update(missing)
150 nodes.symmetric_difference_update(missing)
150 repo._dirtyphases = True
151 repo._dirtyphases = True
151
152
152 def advanceboundary(repo, targetphase, nodes):
153 def advanceboundary(repo, targetphase, nodes):
153 """Add nodes to a phase changing other nodes phases if necessary.
154 """Add nodes to a phase changing other nodes phases if necessary.
154
155
155 This function move boundary *forward* this means that all nodes are set
156 This function move boundary *forward* this means that all nodes are set
156 in the target phase or kept in a *lower* phase.
157 in the target phase or kept in a *lower* phase.
157
158
158 Simplify boundary to contains phase roots only."""
159 Simplify boundary to contains phase roots only."""
159 delroots = [] # set of root deleted by this path
160 delroots = [] # set of root deleted by this path
160 for phase in xrange(targetphase + 1, len(allphases)):
161 for phase in xrange(targetphase + 1, len(allphases)):
161 # filter nodes that are not in a compatible phase already
162 # filter nodes that are not in a compatible phase already
162 # XXX rev phase cache might have been invalidated by a previous loop
163 # XXX rev phase cache might have been invalidated by a previous loop
163 # XXX we need to be smarter here
164 # XXX we need to be smarter here
164 nodes = [n for n in nodes if repo[n].phase() >= phase]
165 nodes = [n for n in nodes if repo[n].phase() >= phase]
165 if not nodes:
166 if not nodes:
166 break # no roots to move anymore
167 break # no roots to move anymore
167 roots = repo._phaseroots[phase]
168 roots = repo._phaseroots[phase]
168 olds = roots.copy()
169 olds = roots.copy()
169 ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
170 ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
170 roots.clear()
171 roots.clear()
171 roots.update(ctx.node() for ctx in ctxs)
172 roots.update(ctx.node() for ctx in ctxs)
172 if olds != roots:
173 if olds != roots:
173 # invalidate cache (we probably could be smarter here
174 # invalidate cache (we probably could be smarter here
174 if '_phaserev' in vars(repo):
175 if '_phaserev' in vars(repo):
175 del repo._phaserev
176 del repo._phaserev
176 repo._dirtyphases = True
177 repo._dirtyphases = True
177 # some roots may need to be declared for lower phases
178 # some roots may need to be declared for lower phases
178 delroots.extend(olds - roots)
179 delroots.extend(olds - roots)
179 # declare deleted root in the target phase
180 # declare deleted root in the target phase
180 if targetphase != 0:
181 if targetphase != 0:
181 retractboundary(repo, targetphase, delroots)
182 retractboundary(repo, targetphase, delroots)
182
183
183
184
184 def retractboundary(repo, targetphase, nodes):
185 def retractboundary(repo, targetphase, nodes):
185 """Set nodes back to a phase changing other nodes phases if necessary.
186 """Set nodes back to a phase changing other nodes phases if necessary.
186
187
187 This function move boundary *backward* this means that all nodes are set
188 This function move boundary *backward* this means that all nodes are set
188 in the target phase or kept in a *higher* phase.
189 in the target phase or kept in a *higher* phase.
189
190
190 Simplify boundary to contains phase roots only."""
191 Simplify boundary to contains phase roots only."""
191 currentroots = repo._phaseroots[targetphase]
192 currentroots = repo._phaseroots[targetphase]
192 newroots = [n for n in nodes if repo[n].phase() < targetphase]
193 newroots = [n for n in nodes if repo[n].phase() < targetphase]
193 if newroots:
194 if newroots:
194 currentroots.update(newroots)
195 currentroots.update(newroots)
195 ctxs = repo.set('roots(%ln::)', currentroots)
196 ctxs = repo.set('roots(%ln::)', currentroots)
196 currentroots.intersection_update(ctx.node() for ctx in ctxs)
197 currentroots.intersection_update(ctx.node() for ctx in ctxs)
197 if '_phaserev' in vars(repo):
198 if '_phaserev' in vars(repo):
198 del repo._phaserev
199 del repo._phaserev
199 repo._dirtyphases = True
200 repo._dirtyphases = True
200
201
201
202
202 def listphases(repo):
203 def listphases(repo):
203 """List phases root for serialisation over pushkey"""
204 """List phases root for serialisation over pushkey"""
204 keys = {}
205 keys = {}
205 for phase in trackedphases:
206 for phase in trackedphases:
206 for root in repo._phaseroots[phase]:
207 for root in repo._phaseroots[phase]:
207 keys[hex(root)] = '%i' % phase
208 keys[hex(root)] = '%i' % phase
208 if repo.ui.configbool('phases', 'publish', True):
209 if repo.ui.configbool('phases', 'publish', True):
209 # Add an extra data to let remote know we are a publishing repo.
210 # Add an extra data to let remote know we are a publishing repo.
210 # Publishing repo can't just pretend they are old repo. When pushing to
211 # Publishing repo can't just pretend they are old repo. When pushing to
211 # a publishing repo, the client still need to push phase boundary
212 # a publishing repo, the client still need to push phase boundary
212 #
213 #
213 # Push do not only push changeset. It also push phase data. New
214 # Push do not only push changeset. It also push phase data. New
214 # phase data may apply to common changeset which won't be push (as they
215 # phase data may apply to common changeset which won't be push (as they
215 # are common). Here is a very simple example:
216 # are common). Here is a very simple example:
216 #
217 #
217 # 1) repo A push changeset X as draft to repo B
218 # 1) repo A push changeset X as draft to repo B
218 # 2) repo B make changeset X public
219 # 2) repo B make changeset X public
219 # 3) repo B push to repo A. X is not pushed but the data that X as now
220 # 3) repo B push to repo A. X is not pushed but the data that X as now
220 # public should
221 # public should
221 #
222 #
222 # The server can't handle it on it's own as it has no idea of client
223 # The server can't handle it on it's own as it has no idea of client
223 # phase data.
224 # phase data.
224 keys['publishing'] = 'True'
225 keys['publishing'] = 'True'
225 return keys
226 return keys
226
227
227 def pushphase(repo, nhex, oldphasestr, newphasestr):
228 def pushphase(repo, nhex, oldphasestr, newphasestr):
228 """List phases root for serialisation over pushkey"""
229 """List phases root for serialisation over pushkey"""
229 lock = repo.lock()
230 lock = repo.lock()
230 try:
231 try:
231 currentphase = repo[nhex].phase()
232 currentphase = repo[nhex].phase()
232 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
233 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
233 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
234 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
234 if currentphase == oldphase and newphase < oldphase:
235 if currentphase == oldphase and newphase < oldphase:
235 advanceboundary(repo, newphase, [bin(nhex)])
236 advanceboundary(repo, newphase, [bin(nhex)])
236 return 1
237 return 1
237 else:
238 else:
238 return 0
239 return 0
239 finally:
240 finally:
240 lock.release()
241 lock.release()
241
242
242 def visibleheads(repo):
243 def visibleheads(repo):
243 """return the set of visible head of this repo"""
244 """return the set of visible head of this repo"""
244 # XXX we want a cache on this
245 # XXX we want a cache on this
245 sroots = repo._phaseroots[secret]
246 sroots = repo._phaseroots[secret]
246 if sroots:
247 if sroots:
247 # XXX very slow revset. storing heads or secret "boundary" would help.
248 # XXX very slow revset. storing heads or secret "boundary" would help.
248 revset = repo.set('heads(not (%ln::))', sroots)
249 revset = repo.set('heads(not (%ln::))', sroots)
249
250
250 vheads = [ctx.node() for ctx in revset]
251 vheads = [ctx.node() for ctx in revset]
251 if not vheads:
252 if not vheads:
252 vheads.append(nullid)
253 vheads.append(nullid)
253 else:
254 else:
254 vheads = repo.heads()
255 vheads = repo.heads()
255 return vheads
256 return vheads
256
257
257 def analyzeremotephases(repo, subset, roots):
258 def analyzeremotephases(repo, subset, roots):
258 """Compute phases heads and root in a subset of node from root dict
259 """Compute phases heads and root in a subset of node from root dict
259
260
260 * subset is heads of the subset
261 * subset is heads of the subset
261 * roots is {<nodeid> => phase} mapping. key and value are string.
262 * roots is {<nodeid> => phase} mapping. key and value are string.
262
263
263 Accept unknown element input
264 Accept unknown element input
264 """
265 """
265 # build list from dictionary
266 # build list from dictionary
266 phaseroots = [[] for p in allphases]
267 phaseroots = [[] for p in allphases]
267 for nhex, phase in roots.iteritems():
268 for nhex, phase in roots.iteritems():
268 if nhex == 'publishing': # ignore data related to publish option
269 if nhex == 'publishing': # ignore data related to publish option
269 continue
270 continue
270 node = bin(nhex)
271 node = bin(nhex)
271 phase = int(phase)
272 phase = int(phase)
272 if node in repo:
273 if node in repo:
273 phaseroots[phase].append(node)
274 phaseroots[phase].append(node)
274 # compute heads
275 # compute heads
275 phaseheads = [[] for p in allphases]
276 phaseheads = [[] for p in allphases]
276 for phase in allphases[:-1]:
277 for phase in allphases[:-1]:
277 toproof = phaseroots[phase + 1]
278 toproof = phaseroots[phase + 1]
278 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
279 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
279 subset, toproof, toproof, subset)
280 subset, toproof, toproof, subset)
280 phaseheads[phase].extend(c.node() for c in revset)
281 phaseheads[phase].extend(c.node() for c in revset)
281 return phaseheads, phaseroots
282 return phaseheads, phaseroots
282
283
General Comments 0
You need to be logged in to leave comments. Login now