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