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