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