##// END OF EJS Templates
phase: extract the phaseroots serialization in a dedicated method...
Pierre-Yves David -
r22079:5dcc5864 default
parent child Browse files
Show More
@@ -1,415 +1,418 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
10 10 of the 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 phase' is an indicator that tells us how a changeset is
21 21 manipulated and communicated. The details of each phase is described
22 22 below, 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
25 25 permanent and leave no audit trail.
26 26
27 27 First, no changeset can be in two phases at once. Phases are ordered,
28 28 so they can be considered from lowest to highest. The default, lowest
29 29 phase is 'public' - this is the normal phase of existing changesets. A
30 30 child changeset can not be 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 is exchanged by pushkey on pull and push. Some servers have
45 45 a publish option set, we call such a server a "publishing server".
46 46 Pushing a draft changeset to a publishing server changes the phase to
47 47 public.
48 48
49 49 A small list of fact/rules define the exchange of phase:
50 50
51 51 * old client never changes server states
52 52 * pull never changes server states
53 53 * publish and old server changesets are seen as public by client
54 54 * any secret changeset seen in another repository is lowered to at
55 55 least draft
56 56
57 57 Here is the final table summing up the 49 possible use cases of phase
58 58 exchange:
59 59
60 60 server
61 61 old publish non-publish
62 62 N X N D P N D P
63 63 old client
64 64 pull
65 65 N - X/X - X/D X/P - X/D X/P
66 66 X - X/X - X/D X/P - X/D X/P
67 67 push
68 68 X X/X X/X X/P X/P X/P X/D X/D X/P
69 69 new client
70 70 pull
71 71 N - P/X - P/D P/P - D/D P/P
72 72 D - P/X - P/D P/P - D/D P/P
73 73 P - P/X - P/D P/P - P/D P/P
74 74 push
75 75 D P/X P/X P/P P/P P/P D/D D/D P/P
76 76 P P/X P/X P/P P/P P/P P/P P/P P/P
77 77
78 78 Legend:
79 79
80 80 A/B = final state on client / state on server
81 81
82 82 * N = new/not present,
83 83 * P = public,
84 84 * D = draft,
85 85 * X = not tracked (i.e., the old client or server has no internal
86 86 way of recording the phase.)
87 87
88 88 passive = only pushes
89 89
90 90
91 91 A cell here can be read like this:
92 92
93 93 "When a new client pushes a draft changeset (D) to a publishing
94 94 server where it's not present (N), it's marked public on both
95 95 sides (P/P)."
96 96
97 97 Note: old client behave as a publishing server with draft only content
98 98 - other people see it as public
99 99 - content is pushed as draft
100 100
101 101 """
102 102
103 103 import errno
104 104 from node import nullid, nullrev, bin, hex, short
105 105 from i18n import _
106 106 import util, error
107 107
108 108 allphases = public, draft, secret = range(3)
109 109 trackedphases = allphases[1:]
110 110 phasenames = ['public', 'draft', 'secret']
111 111
112 112 def _readroots(repo, phasedefaults=None):
113 113 """Read phase roots from disk
114 114
115 115 phasedefaults is a list of fn(repo, roots) callable, which are
116 116 executed if the phase roots file does not exist. When phases are
117 117 being initialized on an existing repository, this could be used to
118 118 set selected changesets phase to something else than public.
119 119
120 120 Return (roots, dirty) where dirty is true if roots differ from
121 121 what is being stored.
122 122 """
123 123 repo = repo.unfiltered()
124 124 dirty = False
125 125 roots = [set() for i in allphases]
126 126 try:
127 127 f = repo.sopener('phaseroots')
128 128 try:
129 129 for line in f:
130 130 phase, nh = line.split()
131 131 roots[int(phase)].add(bin(nh))
132 132 finally:
133 133 f.close()
134 134 except IOError, inst:
135 135 if inst.errno != errno.ENOENT:
136 136 raise
137 137 if phasedefaults:
138 138 for f in phasedefaults:
139 139 roots = f(repo, roots)
140 140 dirty = True
141 141 return roots, dirty
142 142
143 143 class phasecache(object):
144 144 def __init__(self, repo, phasedefaults, _load=True):
145 145 if _load:
146 146 # Cheap trick to allow shallow-copy without copy module
147 147 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
148 148 self._phaserevs = None
149 149 self.filterunknown(repo)
150 150 self.opener = repo.sopener
151 151
152 152 def copy(self):
153 153 # Shallow copy meant to ensure isolation in
154 154 # advance/retractboundary(), nothing more.
155 155 ph = phasecache(None, None, _load=False)
156 156 ph.phaseroots = self.phaseroots[:]
157 157 ph.dirty = self.dirty
158 158 ph.opener = self.opener
159 159 ph._phaserevs = self._phaserevs
160 160 return ph
161 161
162 162 def replace(self, phcache):
163 163 for a in 'phaseroots dirty opener _phaserevs'.split():
164 164 setattr(self, a, getattr(phcache, a))
165 165
166 166 def getphaserevs(self, repo, rebuild=False):
167 167 if rebuild or self._phaserevs is None:
168 168 repo = repo.unfiltered()
169 169 revs = [public] * len(repo.changelog)
170 170 for phase in trackedphases:
171 171 roots = map(repo.changelog.rev, self.phaseroots[phase])
172 172 if roots:
173 173 for rev in roots:
174 174 revs[rev] = phase
175 175 for rev in repo.changelog.descendants(roots):
176 176 revs[rev] = phase
177 177 self._phaserevs = revs
178 178 return self._phaserevs
179 179
180 180 def phase(self, repo, rev):
181 181 # We need a repo argument here to be able to build _phaserevs
182 182 # if necessary. The repository instance is not stored in
183 183 # phasecache to avoid reference cycles. The changelog instance
184 184 # is not stored because it is a filecache() property and can
185 185 # be replaced without us being notified.
186 186 if rev == nullrev:
187 187 return public
188 188 if rev < nullrev:
189 189 raise ValueError(_('cannot lookup negative revision'))
190 190 if self._phaserevs is None or rev >= len(self._phaserevs):
191 191 self._phaserevs = self.getphaserevs(repo, rebuild=True)
192 192 return self._phaserevs[rev]
193 193
194 194 def write(self):
195 195 if not self.dirty:
196 196 return
197 197 f = self.opener('phaseroots', 'w', atomictemp=True)
198 198 try:
199 for phase, roots in enumerate(self.phaseroots):
200 for h in roots:
201 f.write('%i %s\n' % (phase, hex(h)))
199 self._write(f)
202 200 finally:
203 201 f.close()
202
203 def _write(self, fp):
204 for phase, roots in enumerate(self.phaseroots):
205 for h in roots:
206 fp.write('%i %s\n' % (phase, hex(h)))
204 207 self.dirty = False
205 208
206 209 def _updateroots(self, phase, newroots):
207 210 self.phaseroots[phase] = newroots
208 211 self._phaserevs = None
209 212 self.dirty = True
210 213
211 214 def advanceboundary(self, repo, tr, targetphase, nodes):
212 215 # Be careful to preserve shallow-copied values: do not update
213 216 # phaseroots values, replace them.
214 217
215 218 repo = repo.unfiltered()
216 219 delroots = [] # set of root deleted by this path
217 220 for phase in xrange(targetphase + 1, len(allphases)):
218 221 # filter nodes that are not in a compatible phase already
219 222 nodes = [n for n in nodes
220 223 if self.phase(repo, repo[n].rev()) >= phase]
221 224 if not nodes:
222 225 break # no roots to move anymore
223 226 olds = self.phaseroots[phase]
224 227 roots = set(ctx.node() for ctx in repo.set(
225 228 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
226 229 if olds != roots:
227 230 self._updateroots(phase, roots)
228 231 # some roots may need to be declared for lower phases
229 232 delroots.extend(olds - roots)
230 233 # declare deleted root in the target phase
231 234 if targetphase != 0:
232 235 self.retractboundary(repo, tr, targetphase, delroots)
233 236 repo.invalidatevolatilesets()
234 237
235 238 def retractboundary(self, repo, tr, targetphase, nodes):
236 239 # Be careful to preserve shallow-copied values: do not update
237 240 # phaseroots values, replace them.
238 241
239 242 repo = repo.unfiltered()
240 243 currentroots = self.phaseroots[targetphase]
241 244 newroots = [n for n in nodes
242 245 if self.phase(repo, repo[n].rev()) < targetphase]
243 246 if newroots:
244 247 if nullid in newroots:
245 248 raise util.Abort(_('cannot change null revision phase'))
246 249 currentroots = currentroots.copy()
247 250 currentroots.update(newroots)
248 251 ctxs = repo.set('roots(%ln::)', currentroots)
249 252 currentroots.intersection_update(ctx.node() for ctx in ctxs)
250 253 self._updateroots(targetphase, currentroots)
251 254 repo.invalidatevolatilesets()
252 255
253 256 def filterunknown(self, repo):
254 257 """remove unknown nodes from the phase boundary
255 258
256 259 Nothing is lost as unknown nodes only hold data for their descendants.
257 260 """
258 261 filtered = False
259 262 nodemap = repo.changelog.nodemap # to filter unknown nodes
260 263 for phase, nodes in enumerate(self.phaseroots):
261 264 missing = sorted(node for node in nodes if node not in nodemap)
262 265 if missing:
263 266 for mnode in missing:
264 267 repo.ui.debug(
265 268 'removing unknown node %s from %i-phase boundary\n'
266 269 % (short(mnode), phase))
267 270 nodes.symmetric_difference_update(missing)
268 271 filtered = True
269 272 if filtered:
270 273 self.dirty = True
271 274 # filterunknown is called by repo.destroyed, we may have no changes in
272 275 # root but phaserevs contents is certainly invalid (or at least we
273 276 # have not proper way to check that). related to issue 3858.
274 277 #
275 278 # The other caller is __init__ that have no _phaserevs initialized
276 279 # anyway. If this change we should consider adding a dedicated
277 280 # "destroyed" function to phasecache or a proper cache key mechanism
278 281 # (see branchmap one)
279 282 self._phaserevs = None
280 283
281 284 def advanceboundary(repo, tr, targetphase, nodes):
282 285 """Add nodes to a phase changing other nodes phases if necessary.
283 286
284 287 This function move boundary *forward* this means that all nodes
285 288 are set in the target phase or kept in a *lower* phase.
286 289
287 290 Simplify boundary to contains phase roots only."""
288 291 phcache = repo._phasecache.copy()
289 292 phcache.advanceboundary(repo, tr, targetphase, nodes)
290 293 repo._phasecache.replace(phcache)
291 294
292 295 def retractboundary(repo, tr, targetphase, nodes):
293 296 """Set nodes back to a phase changing other nodes phases if
294 297 necessary.
295 298
296 299 This function move boundary *backward* this means that all nodes
297 300 are set in the target phase or kept in a *higher* phase.
298 301
299 302 Simplify boundary to contains phase roots only."""
300 303 phcache = repo._phasecache.copy()
301 304 phcache.retractboundary(repo, tr, targetphase, nodes)
302 305 repo._phasecache.replace(phcache)
303 306
304 307 def listphases(repo):
305 308 """List phases root for serialization over pushkey"""
306 309 keys = {}
307 310 value = '%i' % draft
308 311 for root in repo._phasecache.phaseroots[draft]:
309 312 keys[hex(root)] = value
310 313
311 314 if repo.ui.configbool('phases', 'publish', True):
312 315 # Add an extra data to let remote know we are a publishing
313 316 # repo. Publishing repo can't just pretend they are old repo.
314 317 # When pushing to a publishing repo, the client still need to
315 318 # push phase boundary
316 319 #
317 320 # Push do not only push changeset. It also push phase data.
318 321 # New phase data may apply to common changeset which won't be
319 322 # push (as they are common). Here is a very simple example:
320 323 #
321 324 # 1) repo A push changeset X as draft to repo B
322 325 # 2) repo B make changeset X public
323 326 # 3) repo B push to repo A. X is not pushed but the data that
324 327 # X as now public should
325 328 #
326 329 # The server can't handle it on it's own as it has no idea of
327 330 # client phase data.
328 331 keys['publishing'] = 'True'
329 332 return keys
330 333
331 334 def pushphase(repo, nhex, oldphasestr, newphasestr):
332 335 """List phases root for serialization over pushkey"""
333 336 repo = repo.unfiltered()
334 337 tr = None
335 338 lock = repo.lock()
336 339 try:
337 340 currentphase = repo[nhex].phase()
338 341 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
339 342 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
340 343 if currentphase == oldphase and newphase < oldphase:
341 344 tr = repo.transaction('pushkey-phase')
342 345 advanceboundary(repo, tr, newphase, [bin(nhex)])
343 346 tr.close()
344 347 return 1
345 348 elif currentphase == newphase:
346 349 # raced, but got correct result
347 350 return 1
348 351 else:
349 352 return 0
350 353 finally:
351 354 if tr:
352 355 tr.release()
353 356 lock.release()
354 357
355 358 def analyzeremotephases(repo, subset, roots):
356 359 """Compute phases heads and root in a subset of node from root dict
357 360
358 361 * subset is heads of the subset
359 362 * roots is {<nodeid> => phase} mapping. key and value are string.
360 363
361 364 Accept unknown element input
362 365 """
363 366 repo = repo.unfiltered()
364 367 # build list from dictionary
365 368 draftroots = []
366 369 nodemap = repo.changelog.nodemap # to filter unknown nodes
367 370 for nhex, phase in roots.iteritems():
368 371 if nhex == 'publishing': # ignore data related to publish option
369 372 continue
370 373 node = bin(nhex)
371 374 phase = int(phase)
372 375 if phase == 0:
373 376 if node != nullid:
374 377 repo.ui.warn(_('ignoring inconsistent public root'
375 378 ' from remote: %s\n') % nhex)
376 379 elif phase == 1:
377 380 if node in nodemap:
378 381 draftroots.append(node)
379 382 else:
380 383 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
381 384 % (phase, nhex))
382 385 # compute heads
383 386 publicheads = newheads(repo, subset, draftroots)
384 387 return publicheads, draftroots
385 388
386 389 def newheads(repo, heads, roots):
387 390 """compute new head of a subset minus another
388 391
389 392 * `heads`: define the first subset
390 393 * `roots`: define the second we subtract from the first"""
391 394 repo = repo.unfiltered()
392 395 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
393 396 heads, roots, roots, heads)
394 397 return [c.node() for c in revset]
395 398
396 399
397 400 def newcommitphase(ui):
398 401 """helper to get the target phase of new commit
399 402
400 403 Handle all possible values for the phases.new-commit options.
401 404
402 405 """
403 406 v = ui.config('phases', 'new-commit', draft)
404 407 try:
405 408 return phasenames.index(v)
406 409 except ValueError:
407 410 try:
408 411 return int(v)
409 412 except ValueError:
410 413 msg = _("phases.new-commit: not a valid phase name ('%s')")
411 414 raise error.ConfigError(msg % v)
412 415
413 416 def hassecret(repo):
414 417 """utility function that check if a repo have any secret changeset."""
415 418 return bool(repo._phasecache.phaseroots[2])
General Comments 0
You need to be logged in to leave comments. Login now