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