##// END OF EJS Templates
phases: add a function to compute visible heads...
Pierre-Yves David -
r15697:21eb048e default
parent child Browse files
Show More
@@ -1,264 +1,279 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(3)
102 allphases = range(3)
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 delroots = [] # set of root deleted by this path
157 for phase in xrange(targetphase + 1, len(allphases)):
157 for phase in xrange(targetphase + 1, len(allphases)):
158 # filter nodes that are not in a compatible phase already
158 # filter nodes that are not in a compatible phase already
159 # 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
160 # XXX we need to be smarter here
160 # XXX we need to be smarter here
161 nodes = [n for n in nodes if repo[n].phase() >= phase]
161 nodes = [n for n in nodes if repo[n].phase() >= phase]
162 if not nodes:
162 if not nodes:
163 break # no roots to move anymore
163 break # no roots to move anymore
164 roots = repo._phaseroots[phase]
164 roots = repo._phaseroots[phase]
165 olds = roots.copy()
165 olds = roots.copy()
166 ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
166 ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
167 roots.clear()
167 roots.clear()
168 roots.update(ctx.node() for ctx in ctxs)
168 roots.update(ctx.node() for ctx in ctxs)
169 if olds != roots:
169 if olds != roots:
170 # invalidate cache (we probably could be smarter here
170 # invalidate cache (we probably could be smarter here
171 if '_phaserev' in vars(repo):
171 if '_phaserev' in vars(repo):
172 del repo._phaserev
172 del repo._phaserev
173 repo._dirtyphases = True
173 repo._dirtyphases = True
174 # some roots may need to be declared for lower phases
174 # some roots may need to be declared for lower phases
175 delroots.extend(olds - roots)
175 delroots.extend(olds - roots)
176 # declare deleted root in the target phase
176 # declare deleted root in the target phase
177 if targetphase != 0:
177 if targetphase != 0:
178 retractboundary(repo, targetphase, delroots)
178 retractboundary(repo, targetphase, delroots)
179
179
180
180
181 def retractboundary(repo, targetphase, nodes):
181 def retractboundary(repo, targetphase, nodes):
182 """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.
183
183
184 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
185 in the target phase or kept in a *higher* phase.
185 in the target phase or kept in a *higher* phase.
186
186
187 Simplify boundary to contains phase roots only."""
187 Simplify boundary to contains phase roots only."""
188 currentroots = repo._phaseroots[targetphase]
188 currentroots = repo._phaseroots[targetphase]
189 newroots = [n for n in nodes if repo[n].phase() < targetphase]
189 newroots = [n for n in nodes if repo[n].phase() < targetphase]
190 if newroots:
190 if newroots:
191 currentroots.update(newroots)
191 currentroots.update(newroots)
192 ctxs = repo.set('roots(%ln::)', currentroots)
192 ctxs = repo.set('roots(%ln::)', currentroots)
193 currentroots.intersection_update(ctx.node() for ctx in ctxs)
193 currentroots.intersection_update(ctx.node() for ctx in ctxs)
194 if '_phaserev' in vars(repo):
194 if '_phaserev' in vars(repo):
195 del repo._phaserev
195 del repo._phaserev
196 repo._dirtyphases = True
196 repo._dirtyphases = True
197
197
198
198
199 def listphases(repo):
199 def listphases(repo):
200 """List phases root for serialisation over pushkey"""
200 """List phases root for serialisation over pushkey"""
201 keys = {}
201 keys = {}
202 for phase in trackedphases:
202 for phase in trackedphases:
203 for root in repo._phaseroots[phase]:
203 for root in repo._phaseroots[phase]:
204 keys[hex(root)] = '%i' % phase
204 keys[hex(root)] = '%i' % phase
205 if repo.ui.configbool('phases', 'publish', True):
205 if repo.ui.configbool('phases', 'publish', True):
206 # 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.
207 # 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
208 # a publishing repo, the client still need to push phase boundary
208 # a publishing repo, the client still need to push phase boundary
209 #
209 #
210 # 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
211 # 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
212 # are common). Here is a very simple example:
212 # are common). Here is a very simple example:
213 #
213 #
214 # 1) repo A push changeset X as draft to repo B
214 # 1) repo A push changeset X as draft to repo B
215 # 2) repo B make changeset X public
215 # 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
216 # 3) repo B push to repo A. X is not pushed but the data that X as now
217 # public should
217 # public should
218 #
218 #
219 # 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
220 # phase data.
220 # phase data.
221 keys['publishing'] = 'True'
221 keys['publishing'] = 'True'
222 return keys
222 return keys
223
223
224 def pushphase(repo, nhex, oldphasestr, newphasestr):
224 def pushphase(repo, nhex, oldphasestr, newphasestr):
225 """List phases root for serialisation over pushkey"""
225 """List phases root for serialisation over pushkey"""
226 lock = repo.lock()
226 lock = repo.lock()
227 try:
227 try:
228 currentphase = repo[nhex].phase()
228 currentphase = repo[nhex].phase()
229 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
229 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
230 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
230 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
231 if currentphase == oldphase and newphase < oldphase:
231 if currentphase == oldphase and newphase < oldphase:
232 advanceboundary(repo, newphase, [bin(nhex)])
232 advanceboundary(repo, newphase, [bin(nhex)])
233 return 1
233 return 1
234 else:
234 else:
235 return 0
235 return 0
236 finally:
236 finally:
237 lock.release()
237 lock.release()
238
238
239 def visibleheads(repo):
240 """return the set of visible head of this repo"""
241 # XXX we want a cache on this
242 sroots = repo._phaseroots[2]
243 if sroots:
244 # XXX very slow revset. storing heads or secret "boundary" would help.
245 revset = repo.set('heads(not (%ln::))', sroots)
246
247 vheads = [ctx.node() for ctx in revset]
248 if not vheads:
249 vheads.append(nullid)
250 else:
251 vheads = repo.heads()
252 return vheads
253
239 def analyzeremotephases(repo, subset, roots):
254 def analyzeremotephases(repo, subset, roots):
240 """Compute phases heads and root in a subset of node from root dict
255 """Compute phases heads and root in a subset of node from root dict
241
256
242 * subset is heads of the subset
257 * subset is heads of the subset
243 * roots is {<nodeid> => phase} mapping. key and value are string.
258 * roots is {<nodeid> => phase} mapping. key and value are string.
244
259
245 Accept unknown element input
260 Accept unknown element input
246 """
261 """
247 # build list from dictionary
262 # build list from dictionary
248 phaseroots = [[] for p in allphases]
263 phaseroots = [[] for p in allphases]
249 for nhex, phase in roots.iteritems():
264 for nhex, phase in roots.iteritems():
250 if nhex == 'publishing': # ignore data related to publish option
265 if nhex == 'publishing': # ignore data related to publish option
251 continue
266 continue
252 node = bin(nhex)
267 node = bin(nhex)
253 phase = int(phase)
268 phase = int(phase)
254 if node in repo:
269 if node in repo:
255 phaseroots[phase].append(node)
270 phaseroots[phase].append(node)
256 # compute heads
271 # compute heads
257 phaseheads = [[] for p in allphases]
272 phaseheads = [[] for p in allphases]
258 for phase in allphases[:-1]:
273 for phase in allphases[:-1]:
259 toproof = phaseroots[phase + 1]
274 toproof = phaseroots[phase + 1]
260 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
275 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
261 subset, toproof, toproof, subset)
276 subset, toproof, toproof, subset)
262 phaseheads[phase].extend(c.node() for c in revset)
277 phaseheads[phase].extend(c.node() for c in revset)
263 return phaseheads, phaseroots
278 return phaseheads, phaseroots
264
279
General Comments 0
You need to be logged in to leave comments. Login now