##// END OF EJS Templates
phase: improve retractboundary perf...
Durham Goode -
r26909:e3611881 default
parent child Browse files
Show More
@@ -1,478 +1,488
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 from __future__ import absolute_import
104 104
105 105 import errno
106 106 import os
107 107
108 108 from .i18n import _
109 109 from .node import (
110 110 bin,
111 111 hex,
112 112 nullid,
113 113 nullrev,
114 114 short,
115 115 )
116 116 from . import (
117 117 error,
118 118 )
119 119
120 120 allphases = public, draft, secret = range(3)
121 121 trackedphases = allphases[1:]
122 122 phasenames = ['public', 'draft', 'secret']
123 123
124 124 def _readroots(repo, phasedefaults=None):
125 125 """Read phase roots from disk
126 126
127 127 phasedefaults is a list of fn(repo, roots) callable, which are
128 128 executed if the phase roots file does not exist. When phases are
129 129 being initialized on an existing repository, this could be used to
130 130 set selected changesets phase to something else than public.
131 131
132 132 Return (roots, dirty) where dirty is true if roots differ from
133 133 what is being stored.
134 134 """
135 135 repo = repo.unfiltered()
136 136 dirty = False
137 137 roots = [set() for i in allphases]
138 138 try:
139 139 f = None
140 140 if 'HG_PENDING' in os.environ:
141 141 try:
142 142 f = repo.svfs('phaseroots.pending')
143 143 except IOError as inst:
144 144 if inst.errno != errno.ENOENT:
145 145 raise
146 146 if f is None:
147 147 f = repo.svfs('phaseroots')
148 148 try:
149 149 for line in f:
150 150 phase, nh = line.split()
151 151 roots[int(phase)].add(bin(nh))
152 152 finally:
153 153 f.close()
154 154 except IOError as inst:
155 155 if inst.errno != errno.ENOENT:
156 156 raise
157 157 if phasedefaults:
158 158 for f in phasedefaults:
159 159 roots = f(repo, roots)
160 160 dirty = True
161 161 return roots, dirty
162 162
163 163 class phasecache(object):
164 164 def __init__(self, repo, phasedefaults, _load=True):
165 165 if _load:
166 166 # Cheap trick to allow shallow-copy without copy module
167 167 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
168 168 self._phaserevs = None
169 169 self._phasesets = None
170 170 self.filterunknown(repo)
171 171 self.opener = repo.svfs
172 172
173 173 def copy(self):
174 174 # Shallow copy meant to ensure isolation in
175 175 # advance/retractboundary(), nothing more.
176 176 ph = self.__class__(None, None, _load=False)
177 177 ph.phaseroots = self.phaseroots[:]
178 178 ph.dirty = self.dirty
179 179 ph.opener = self.opener
180 180 ph._phaserevs = self._phaserevs
181 181 ph._phasesets = self._phasesets
182 182 return ph
183 183
184 184 def replace(self, phcache):
185 185 """replace all values in 'self' with content of phcache"""
186 186 for a in ('phaseroots', 'dirty', 'opener', '_phaserevs', '_phasesets'):
187 187 setattr(self, a, getattr(phcache, a))
188 188
189 189 def _getphaserevsnative(self, repo):
190 190 repo = repo.unfiltered()
191 191 nativeroots = []
192 192 for phase in trackedphases:
193 193 nativeroots.append(map(repo.changelog.rev, self.phaseroots[phase]))
194 194 return repo.changelog.computephases(nativeroots)
195 195
196 196 def _computephaserevspure(self, repo):
197 197 repo = repo.unfiltered()
198 198 revs = [public] * len(repo.changelog)
199 199 self._phaserevs = revs
200 200 self._populatephaseroots(repo)
201 201 for phase in trackedphases:
202 202 roots = map(repo.changelog.rev, self.phaseroots[phase])
203 203 if roots:
204 204 for rev in roots:
205 205 revs[rev] = phase
206 206 for rev in repo.changelog.descendants(roots):
207 207 revs[rev] = phase
208 208
209 209 def loadphaserevs(self, repo):
210 210 """ensure phase information is loaded in the object"""
211 211 if self._phaserevs is None:
212 212 try:
213 213 if repo.ui.configbool('experimental',
214 214 'nativephaseskillswitch'):
215 215 self._computephaserevspure(repo)
216 216 else:
217 217 res = self._getphaserevsnative(repo)
218 218 self._phaserevs, self._phasesets = res
219 219 except AttributeError:
220 220 self._computephaserevspure(repo)
221 221
222 222 def invalidate(self):
223 223 self._phaserevs = None
224 224 self._phasesets = None
225 225
226 226 def _populatephaseroots(self, repo):
227 227 """Fills the _phaserevs cache with phases for the roots.
228 228 """
229 229 cl = repo.changelog
230 230 phaserevs = self._phaserevs
231 231 for phase in trackedphases:
232 232 roots = map(cl.rev, self.phaseroots[phase])
233 233 for root in roots:
234 234 phaserevs[root] = phase
235 235
236 236 def phase(self, repo, rev):
237 237 # We need a repo argument here to be able to build _phaserevs
238 238 # if necessary. The repository instance is not stored in
239 239 # phasecache to avoid reference cycles. The changelog instance
240 240 # is not stored because it is a filecache() property and can
241 241 # be replaced without us being notified.
242 242 if rev == nullrev:
243 243 return public
244 244 if rev < nullrev:
245 245 raise ValueError(_('cannot lookup negative revision'))
246 246 if self._phaserevs is None or rev >= len(self._phaserevs):
247 247 self.invalidate()
248 248 self.loadphaserevs(repo)
249 249 return self._phaserevs[rev]
250 250
251 251 def write(self):
252 252 if not self.dirty:
253 253 return
254 254 f = self.opener('phaseroots', 'w', atomictemp=True)
255 255 try:
256 256 self._write(f)
257 257 finally:
258 258 f.close()
259 259
260 260 def _write(self, fp):
261 261 for phase, roots in enumerate(self.phaseroots):
262 262 for h in roots:
263 263 fp.write('%i %s\n' % (phase, hex(h)))
264 264 self.dirty = False
265 265
266 266 def _updateroots(self, phase, newroots, tr):
267 267 self.phaseroots[phase] = newroots
268 268 self.invalidate()
269 269 self.dirty = True
270 270
271 271 tr.addfilegenerator('phase', ('phaseroots',), self._write)
272 272 tr.hookargs['phases_moved'] = '1'
273 273
274 274 def advanceboundary(self, repo, tr, targetphase, nodes):
275 275 # Be careful to preserve shallow-copied values: do not update
276 276 # phaseroots values, replace them.
277 277
278 278 repo = repo.unfiltered()
279 279 delroots = [] # set of root deleted by this path
280 280 for phase in xrange(targetphase + 1, len(allphases)):
281 281 # filter nodes that are not in a compatible phase already
282 282 nodes = [n for n in nodes
283 283 if self.phase(repo, repo[n].rev()) >= phase]
284 284 if not nodes:
285 285 break # no roots to move anymore
286 286 olds = self.phaseroots[phase]
287 287 roots = set(ctx.node() for ctx in repo.set(
288 288 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
289 289 if olds != roots:
290 290 self._updateroots(phase, roots, tr)
291 291 # some roots may need to be declared for lower phases
292 292 delroots.extend(olds - roots)
293 293 # declare deleted root in the target phase
294 294 if targetphase != 0:
295 295 self.retractboundary(repo, tr, targetphase, delroots)
296 296 repo.invalidatevolatilesets()
297 297
298 298 def retractboundary(self, repo, tr, targetphase, nodes):
299 299 # Be careful to preserve shallow-copied values: do not update
300 300 # phaseroots values, replace them.
301 301
302 302 repo = repo.unfiltered()
303 303 currentroots = self.phaseroots[targetphase]
304 304 newroots = [n for n in nodes
305 305 if self.phase(repo, repo[n].rev()) < targetphase]
306 306 if newroots:
307 307 if nullid in newroots:
308 308 raise error.Abort(_('cannot change null revision phase'))
309 309 currentroots = currentroots.copy()
310 310 currentroots.update(newroots)
311 ctxs = repo.set('roots(%ln::)', currentroots)
312 currentroots.intersection_update(ctx.node() for ctx in ctxs)
313 self._updateroots(targetphase, currentroots, tr)
311
312 # Only compute new roots for revs above the roots that are being
313 # retracted.
314 minnewroot = min(repo[n].rev() for n in newroots)
315 aboveroots = [n for n in currentroots
316 if repo[n].rev() >= minnewroot]
317 updatedroots = repo.set('roots(%ln::)', aboveroots)
318
319 finalroots = set(n for n in currentroots if repo[n].rev() <
320 minnewroot)
321 finalroots.update(ctx.node() for ctx in updatedroots)
322
323 self._updateroots(targetphase, finalroots, tr)
314 324 repo.invalidatevolatilesets()
315 325
316 326 def filterunknown(self, repo):
317 327 """remove unknown nodes from the phase boundary
318 328
319 329 Nothing is lost as unknown nodes only hold data for their descendants.
320 330 """
321 331 filtered = False
322 332 nodemap = repo.changelog.nodemap # to filter unknown nodes
323 333 for phase, nodes in enumerate(self.phaseroots):
324 334 missing = sorted(node for node in nodes if node not in nodemap)
325 335 if missing:
326 336 for mnode in missing:
327 337 repo.ui.debug(
328 338 'removing unknown node %s from %i-phase boundary\n'
329 339 % (short(mnode), phase))
330 340 nodes.symmetric_difference_update(missing)
331 341 filtered = True
332 342 if filtered:
333 343 self.dirty = True
334 344 # filterunknown is called by repo.destroyed, we may have no changes in
335 345 # root but phaserevs contents is certainly invalid (or at least we
336 346 # have not proper way to check that). related to issue 3858.
337 347 #
338 348 # The other caller is __init__ that have no _phaserevs initialized
339 349 # anyway. If this change we should consider adding a dedicated
340 350 # "destroyed" function to phasecache or a proper cache key mechanism
341 351 # (see branchmap one)
342 352 self.invalidate()
343 353
344 354 def advanceboundary(repo, tr, targetphase, nodes):
345 355 """Add nodes to a phase changing other nodes phases if necessary.
346 356
347 357 This function move boundary *forward* this means that all nodes
348 358 are set in the target phase or kept in a *lower* phase.
349 359
350 360 Simplify boundary to contains phase roots only."""
351 361 phcache = repo._phasecache.copy()
352 362 phcache.advanceboundary(repo, tr, targetphase, nodes)
353 363 repo._phasecache.replace(phcache)
354 364
355 365 def retractboundary(repo, tr, targetphase, nodes):
356 366 """Set nodes back to a phase changing other nodes phases if
357 367 necessary.
358 368
359 369 This function move boundary *backward* this means that all nodes
360 370 are set in the target phase or kept in a *higher* phase.
361 371
362 372 Simplify boundary to contains phase roots only."""
363 373 phcache = repo._phasecache.copy()
364 374 phcache.retractboundary(repo, tr, targetphase, nodes)
365 375 repo._phasecache.replace(phcache)
366 376
367 377 def listphases(repo):
368 378 """List phases root for serialization over pushkey"""
369 379 keys = {}
370 380 value = '%i' % draft
371 381 for root in repo._phasecache.phaseroots[draft]:
372 382 keys[hex(root)] = value
373 383
374 384 if repo.publishing():
375 385 # Add an extra data to let remote know we are a publishing
376 386 # repo. Publishing repo can't just pretend they are old repo.
377 387 # When pushing to a publishing repo, the client still need to
378 388 # push phase boundary
379 389 #
380 390 # Push do not only push changeset. It also push phase data.
381 391 # New phase data may apply to common changeset which won't be
382 392 # push (as they are common). Here is a very simple example:
383 393 #
384 394 # 1) repo A push changeset X as draft to repo B
385 395 # 2) repo B make changeset X public
386 396 # 3) repo B push to repo A. X is not pushed but the data that
387 397 # X as now public should
388 398 #
389 399 # The server can't handle it on it's own as it has no idea of
390 400 # client phase data.
391 401 keys['publishing'] = 'True'
392 402 return keys
393 403
394 404 def pushphase(repo, nhex, oldphasestr, newphasestr):
395 405 """List phases root for serialization over pushkey"""
396 406 repo = repo.unfiltered()
397 407 tr = None
398 408 lock = repo.lock()
399 409 try:
400 410 currentphase = repo[nhex].phase()
401 411 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
402 412 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
403 413 if currentphase == oldphase and newphase < oldphase:
404 414 tr = repo.transaction('pushkey-phase')
405 415 advanceboundary(repo, tr, newphase, [bin(nhex)])
406 416 tr.close()
407 417 return 1
408 418 elif currentphase == newphase:
409 419 # raced, but got correct result
410 420 return 1
411 421 else:
412 422 return 0
413 423 finally:
414 424 if tr:
415 425 tr.release()
416 426 lock.release()
417 427
418 428 def analyzeremotephases(repo, subset, roots):
419 429 """Compute phases heads and root in a subset of node from root dict
420 430
421 431 * subset is heads of the subset
422 432 * roots is {<nodeid> => phase} mapping. key and value are string.
423 433
424 434 Accept unknown element input
425 435 """
426 436 repo = repo.unfiltered()
427 437 # build list from dictionary
428 438 draftroots = []
429 439 nodemap = repo.changelog.nodemap # to filter unknown nodes
430 440 for nhex, phase in roots.iteritems():
431 441 if nhex == 'publishing': # ignore data related to publish option
432 442 continue
433 443 node = bin(nhex)
434 444 phase = int(phase)
435 445 if phase == 0:
436 446 if node != nullid:
437 447 repo.ui.warn(_('ignoring inconsistent public root'
438 448 ' from remote: %s\n') % nhex)
439 449 elif phase == 1:
440 450 if node in nodemap:
441 451 draftroots.append(node)
442 452 else:
443 453 repo.ui.warn(_('ignoring unexpected root from remote: %i %s\n')
444 454 % (phase, nhex))
445 455 # compute heads
446 456 publicheads = newheads(repo, subset, draftroots)
447 457 return publicheads, draftroots
448 458
449 459 def newheads(repo, heads, roots):
450 460 """compute new head of a subset minus another
451 461
452 462 * `heads`: define the first subset
453 463 * `roots`: define the second we subtract from the first"""
454 464 repo = repo.unfiltered()
455 465 revset = repo.set('heads((%ln + parents(%ln)) - (%ln::%ln))',
456 466 heads, roots, roots, heads)
457 467 return [c.node() for c in revset]
458 468
459 469
460 470 def newcommitphase(ui):
461 471 """helper to get the target phase of new commit
462 472
463 473 Handle all possible values for the phases.new-commit options.
464 474
465 475 """
466 476 v = ui.config('phases', 'new-commit', draft)
467 477 try:
468 478 return phasenames.index(v)
469 479 except ValueError:
470 480 try:
471 481 return int(v)
472 482 except ValueError:
473 483 msg = _("phases.new-commit: not a valid phase name ('%s')")
474 484 raise error.ConfigError(msg % v)
475 485
476 486 def hassecret(repo):
477 487 """utility function that check if a repo have any secret changeset."""
478 488 return bool(repo._phasecache.phaseroots[2])
General Comments 0
You need to be logged in to leave comments. Login now