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