##// END OF EJS Templates
phases: fix performance regression with Python 2...
Joerg Sonnenberger -
r46127:29a259be default
parent child Browse files
Show More
@@ -1,930 +1,932 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 from __future__ import absolute_import
104 104
105 105 import errno
106 106 import struct
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 wdirrev,
116 116 )
117 117 from .pycompat import (
118 118 getattr,
119 119 setattr,
120 120 )
121 121 from . import (
122 122 error,
123 123 pycompat,
124 124 requirements,
125 125 smartset,
126 126 txnutil,
127 127 util,
128 128 )
129 129
130 130 _fphasesentry = struct.Struct(b'>i20s')
131 131
132 132 # record phase index
133 133 public, draft, secret = range(3)
134 134 archived = 32 # non-continuous for compatibility
135 135 internal = 96 # non-continuous for compatibility
136 136 allphases = (public, draft, secret, archived, internal)
137 137 trackedphases = (draft, secret, archived, internal)
138 138 # record phase names
139 139 cmdphasenames = [b'public', b'draft', b'secret'] # known to `hg phase` command
140 140 phasenames = dict(enumerate(cmdphasenames))
141 141 phasenames[archived] = b'archived'
142 142 phasenames[internal] = b'internal'
143 143 # map phase name to phase number
144 144 phasenumber = {name: phase for phase, name in phasenames.items()}
145 145 # like phasenumber, but also include maps for the numeric and binary
146 146 # phase number to the phase number
147 147 phasenumber2 = phasenumber.copy()
148 148 phasenumber2.update({phase: phase for phase in phasenames})
149 149 phasenumber2.update({b'%i' % phase: phase for phase in phasenames})
150 150 # record phase property
151 151 mutablephases = (draft, secret, archived, internal)
152 152 remotehiddenphases = (secret, archived, internal)
153 153 localhiddenphases = (internal, archived)
154 154
155 155
156 156 def supportinternal(repo):
157 157 """True if the internal phase can be used on a repository"""
158 158 return requirements.INTERNAL_PHASE_REQUIREMENT in repo.requirements
159 159
160 160
161 161 def _readroots(repo, phasedefaults=None):
162 162 """Read phase roots from disk
163 163
164 164 phasedefaults is a list of fn(repo, roots) callable, which are
165 165 executed if the phase roots file does not exist. When phases are
166 166 being initialized on an existing repository, this could be used to
167 167 set selected changesets phase to something else than public.
168 168
169 169 Return (roots, dirty) where dirty is true if roots differ from
170 170 what is being stored.
171 171 """
172 172 repo = repo.unfiltered()
173 173 dirty = False
174 174 roots = {i: set() for i in allphases}
175 175 try:
176 176 f, pending = txnutil.trypending(repo.root, repo.svfs, b'phaseroots')
177 177 try:
178 178 for line in f:
179 179 phase, nh = line.split()
180 180 roots[int(phase)].add(bin(nh))
181 181 finally:
182 182 f.close()
183 183 except IOError as inst:
184 184 if inst.errno != errno.ENOENT:
185 185 raise
186 186 if phasedefaults:
187 187 for f in phasedefaults:
188 188 roots = f(repo, roots)
189 189 dirty = True
190 190 return roots, dirty
191 191
192 192
193 193 def binaryencode(phasemapping):
194 194 """encode a 'phase -> nodes' mapping into a binary stream
195 195
196 196 The revision lists are encoded as (phase, root) pairs.
197 197 """
198 198 binarydata = []
199 199 for phase, nodes in pycompat.iteritems(phasemapping):
200 200 for head in nodes:
201 201 binarydata.append(_fphasesentry.pack(phase, head))
202 202 return b''.join(binarydata)
203 203
204 204
205 205 def binarydecode(stream):
206 206 """decode a binary stream into a 'phase -> nodes' mapping
207 207
208 208 The (phase, root) pairs are turned back into a dictionary with
209 209 the phase as index and the aggregated roots of that phase as value."""
210 210 headsbyphase = {i: [] for i in allphases}
211 211 entrysize = _fphasesentry.size
212 212 while True:
213 213 entry = stream.read(entrysize)
214 214 if len(entry) < entrysize:
215 215 if entry:
216 216 raise error.Abort(_(b'bad phase-heads stream'))
217 217 break
218 218 phase, node = _fphasesentry.unpack(entry)
219 219 headsbyphase[phase].append(node)
220 220 return headsbyphase
221 221
222 222
223 223 def _sortedrange_insert(data, idx, rev, t):
224 224 merge_before = False
225 225 if idx:
226 226 r1, t1 = data[idx - 1]
227 227 merge_before = r1[-1] + 1 == rev and t1 == t
228 228 merge_after = False
229 229 if idx < len(data):
230 230 r2, t2 = data[idx]
231 231 merge_after = r2[0] == rev + 1 and t2 == t
232 232
233 233 if merge_before and merge_after:
234 234 data[idx - 1] = (pycompat.xrange(r1[0], r2[-1] + 1), t)
235 235 data.pop(idx)
236 236 elif merge_before:
237 237 data[idx - 1] = (pycompat.xrange(r1[0], rev + 1), t)
238 238 elif merge_after:
239 239 data[idx] = (pycompat.xrange(rev, r2[-1] + 1), t)
240 240 else:
241 241 data.insert(idx, (pycompat.xrange(rev, rev + 1), t))
242 242
243 243
244 244 def _sortedrange_split(data, idx, rev, t):
245 245 r1, t1 = data[idx]
246 246 if t == t1:
247 247 return
248 248 t = (t1[0], t[1])
249 249 if len(r1) == 1:
250 250 data.pop(idx)
251 251 _sortedrange_insert(data, idx, rev, t)
252 252 elif r1[0] == rev:
253 253 data[idx] = (pycompat.xrange(rev + 1, r1[-1] + 1), t1)
254 254 _sortedrange_insert(data, idx, rev, t)
255 255 elif r1[-1] == rev:
256 256 data[idx] = (pycompat.xrange(r1[0], rev), t1)
257 257 _sortedrange_insert(data, idx + 1, rev, t)
258 258 else:
259 259 data[idx : idx + 1] = [
260 260 (pycompat.xrange(r1[0], rev), t1),
261 261 (pycompat.xrange(rev, rev + 1), t),
262 262 (pycompat.xrange(rev + 1, r1[-1] + 1), t1),
263 263 ]
264 264
265 265
266 266 def _trackphasechange(data, rev, old, new):
267 267 """add a phase move to the <data> list of ranges
268 268
269 269 If data is None, nothing happens.
270 270 """
271 271 if data is None:
272 272 return
273 273
274 274 # If data is empty, create a one-revision range and done
275 275 if not data:
276 276 data.insert(0, (pycompat.xrange(rev, rev + 1), (old, new)))
277 277 return
278 278
279 279 low = 0
280 280 high = len(data)
281 281 t = (old, new)
282 282 while low < high:
283 283 mid = (low + high) // 2
284 284 revs = data[mid][0]
285 revs_low = revs[0]
286 revs_high = revs[-1]
285 287
286 if rev in revs:
288 if rev >= revs_low and rev <= revs_high:
287 289 _sortedrange_split(data, mid, rev, t)
288 290 return
289 291
290 if revs[0] == rev + 1:
292 if revs_low == rev + 1:
291 293 if mid and data[mid - 1][0][-1] == rev:
292 294 _sortedrange_split(data, mid - 1, rev, t)
293 295 else:
294 296 _sortedrange_insert(data, mid, rev, t)
295 297 return
296 298
297 if revs[-1] == rev - 1:
299 if revs_high == rev - 1:
298 300 if mid + 1 < len(data) and data[mid + 1][0][0] == rev:
299 301 _sortedrange_split(data, mid + 1, rev, t)
300 302 else:
301 303 _sortedrange_insert(data, mid + 1, rev, t)
302 304 return
303 305
304 if revs[0] > rev:
306 if revs_low > rev:
305 307 high = mid
306 308 else:
307 309 low = mid + 1
308 310
309 311 if low == len(data):
310 312 data.append((pycompat.xrange(rev, rev + 1), t))
311 313 return
312 314
313 315 r1, t1 = data[low]
314 316 if r1[0] > rev:
315 317 data.insert(low, (pycompat.xrange(rev, rev + 1), t))
316 318 else:
317 319 data.insert(low + 1, (pycompat.xrange(rev, rev + 1), t))
318 320
319 321
320 322 class phasecache(object):
321 323 def __init__(self, repo, phasedefaults, _load=True):
322 324 if _load:
323 325 # Cheap trick to allow shallow-copy without copy module
324 326 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
325 327 self._loadedrevslen = 0
326 328 self._phasesets = None
327 329 self.filterunknown(repo)
328 330 self.opener = repo.svfs
329 331
330 332 def hasnonpublicphases(self, repo):
331 333 """detect if there are revisions with non-public phase"""
332 334 repo = repo.unfiltered()
333 335 cl = repo.changelog
334 336 if len(cl) >= self._loadedrevslen:
335 337 self.invalidate()
336 338 self.loadphaserevs(repo)
337 339 return any(
338 340 revs
339 341 for phase, revs in pycompat.iteritems(self.phaseroots)
340 342 if phase != public
341 343 )
342 344
343 345 def nonpublicphaseroots(self, repo):
344 346 """returns the roots of all non-public phases
345 347
346 348 The roots are not minimized, so if the secret revisions are
347 349 descendants of draft revisions, their roots will still be present.
348 350 """
349 351 repo = repo.unfiltered()
350 352 cl = repo.changelog
351 353 if len(cl) >= self._loadedrevslen:
352 354 self.invalidate()
353 355 self.loadphaserevs(repo)
354 356 return set().union(
355 357 *[
356 358 revs
357 359 for phase, revs in pycompat.iteritems(self.phaseroots)
358 360 if phase != public
359 361 ]
360 362 )
361 363
362 364 def getrevset(self, repo, phases, subset=None):
363 365 """return a smartset for the given phases"""
364 366 self.loadphaserevs(repo) # ensure phase's sets are loaded
365 367 phases = set(phases)
366 368 publicphase = public in phases
367 369
368 370 if publicphase:
369 371 # In this case, phases keeps all the *other* phases.
370 372 phases = set(allphases).difference(phases)
371 373 if not phases:
372 374 return smartset.fullreposet(repo)
373 375
374 376 # fast path: _phasesets contains the interesting sets,
375 377 # might only need a union and post-filtering.
376 378 revsneedscopy = False
377 379 if len(phases) == 1:
378 380 [p] = phases
379 381 revs = self._phasesets[p]
380 382 revsneedscopy = True # Don't modify _phasesets
381 383 else:
382 384 # revs has the revisions in all *other* phases.
383 385 revs = set.union(*[self._phasesets[p] for p in phases])
384 386
385 387 def _addwdir(wdirsubset, wdirrevs):
386 388 if wdirrev in wdirsubset and repo[None].phase() in phases:
387 389 if revsneedscopy:
388 390 wdirrevs = wdirrevs.copy()
389 391 # The working dir would never be in the # cache, but it was in
390 392 # the subset being filtered for its phase (or filtered out,
391 393 # depending on publicphase), so add it to the output to be
392 394 # included (or filtered out).
393 395 wdirrevs.add(wdirrev)
394 396 return wdirrevs
395 397
396 398 if not publicphase:
397 399 if repo.changelog.filteredrevs:
398 400 revs = revs - repo.changelog.filteredrevs
399 401
400 402 if subset is None:
401 403 return smartset.baseset(revs)
402 404 else:
403 405 revs = _addwdir(subset, revs)
404 406 return subset & smartset.baseset(revs)
405 407 else:
406 408 if subset is None:
407 409 subset = smartset.fullreposet(repo)
408 410
409 411 revs = _addwdir(subset, revs)
410 412
411 413 if not revs:
412 414 return subset
413 415 return subset.filter(lambda r: r not in revs)
414 416
415 417 def copy(self):
416 418 # Shallow copy meant to ensure isolation in
417 419 # advance/retractboundary(), nothing more.
418 420 ph = self.__class__(None, None, _load=False)
419 421 ph.phaseroots = self.phaseroots.copy()
420 422 ph.dirty = self.dirty
421 423 ph.opener = self.opener
422 424 ph._loadedrevslen = self._loadedrevslen
423 425 ph._phasesets = self._phasesets
424 426 return ph
425 427
426 428 def replace(self, phcache):
427 429 """replace all values in 'self' with content of phcache"""
428 430 for a in (
429 431 b'phaseroots',
430 432 b'dirty',
431 433 b'opener',
432 434 b'_loadedrevslen',
433 435 b'_phasesets',
434 436 ):
435 437 setattr(self, a, getattr(phcache, a))
436 438
437 439 def _getphaserevsnative(self, repo):
438 440 repo = repo.unfiltered()
439 441 return repo.changelog.computephases(self.phaseroots)
440 442
441 443 def _computephaserevspure(self, repo):
442 444 repo = repo.unfiltered()
443 445 cl = repo.changelog
444 446 self._phasesets = {phase: set() for phase in allphases}
445 447 lowerroots = set()
446 448 for phase in reversed(trackedphases):
447 449 roots = pycompat.maplist(cl.rev, self.phaseroots[phase])
448 450 if roots:
449 451 ps = set(cl.descendants(roots))
450 452 for root in roots:
451 453 ps.add(root)
452 454 ps.difference_update(lowerroots)
453 455 lowerroots.update(ps)
454 456 self._phasesets[phase] = ps
455 457 self._loadedrevslen = len(cl)
456 458
457 459 def loadphaserevs(self, repo):
458 460 """ensure phase information is loaded in the object"""
459 461 if self._phasesets is None:
460 462 try:
461 463 res = self._getphaserevsnative(repo)
462 464 self._loadedrevslen, self._phasesets = res
463 465 except AttributeError:
464 466 self._computephaserevspure(repo)
465 467
466 468 def invalidate(self):
467 469 self._loadedrevslen = 0
468 470 self._phasesets = None
469 471
470 472 def phase(self, repo, rev):
471 473 # We need a repo argument here to be able to build _phasesets
472 474 # if necessary. The repository instance is not stored in
473 475 # phasecache to avoid reference cycles. The changelog instance
474 476 # is not stored because it is a filecache() property and can
475 477 # be replaced without us being notified.
476 478 if rev == nullrev:
477 479 return public
478 480 if rev < nullrev:
479 481 raise ValueError(_(b'cannot lookup negative revision'))
480 482 if rev >= self._loadedrevslen:
481 483 self.invalidate()
482 484 self.loadphaserevs(repo)
483 485 for phase in trackedphases:
484 486 if rev in self._phasesets[phase]:
485 487 return phase
486 488 return public
487 489
488 490 def write(self):
489 491 if not self.dirty:
490 492 return
491 493 f = self.opener(b'phaseroots', b'w', atomictemp=True, checkambig=True)
492 494 try:
493 495 self._write(f)
494 496 finally:
495 497 f.close()
496 498
497 499 def _write(self, fp):
498 500 for phase, roots in pycompat.iteritems(self.phaseroots):
499 501 for h in sorted(roots):
500 502 fp.write(b'%i %s\n' % (phase, hex(h)))
501 503 self.dirty = False
502 504
503 505 def _updateroots(self, phase, newroots, tr):
504 506 self.phaseroots[phase] = newroots
505 507 self.invalidate()
506 508 self.dirty = True
507 509
508 510 tr.addfilegenerator(b'phase', (b'phaseroots',), self._write)
509 511 tr.hookargs[b'phases_moved'] = b'1'
510 512
511 513 def registernew(self, repo, tr, targetphase, nodes):
512 514 repo = repo.unfiltered()
513 515 self._retractboundary(repo, tr, targetphase, nodes)
514 516 if tr is not None and b'phases' in tr.changes:
515 517 phasetracking = tr.changes[b'phases']
516 518 torev = repo.changelog.rev
517 519 phase = self.phase
518 520 revs = [torev(node) for node in nodes]
519 521 revs.sort()
520 522 for rev in revs:
521 523 revphase = phase(repo, rev)
522 524 _trackphasechange(phasetracking, rev, None, revphase)
523 525 repo.invalidatevolatilesets()
524 526
525 527 def advanceboundary(self, repo, tr, targetphase, nodes, dryrun=None):
526 528 """Set all 'nodes' to phase 'targetphase'
527 529
528 530 Nodes with a phase lower than 'targetphase' are not affected.
529 531
530 532 If dryrun is True, no actions will be performed
531 533
532 534 Returns a set of revs whose phase is changed or should be changed
533 535 """
534 536 # Be careful to preserve shallow-copied values: do not update
535 537 # phaseroots values, replace them.
536 538 if tr is None:
537 539 phasetracking = None
538 540 else:
539 541 phasetracking = tr.changes.get(b'phases')
540 542
541 543 repo = repo.unfiltered()
542 544
543 545 changes = set() # set of revisions to be changed
544 546 delroots = [] # set of root deleted by this path
545 547 for phase in (phase for phase in allphases if phase > targetphase):
546 548 # filter nodes that are not in a compatible phase already
547 549 nodes = [
548 550 n for n in nodes if self.phase(repo, repo[n].rev()) >= phase
549 551 ]
550 552 if not nodes:
551 553 break # no roots to move anymore
552 554
553 555 olds = self.phaseroots[phase]
554 556
555 557 affected = repo.revs(b'%ln::%ln', olds, nodes)
556 558 changes.update(affected)
557 559 if dryrun:
558 560 continue
559 561 for r in affected:
560 562 _trackphasechange(
561 563 phasetracking, r, self.phase(repo, r), targetphase
562 564 )
563 565
564 566 roots = {
565 567 ctx.node()
566 568 for ctx in repo.set(b'roots((%ln::) - %ld)', olds, affected)
567 569 }
568 570 if olds != roots:
569 571 self._updateroots(phase, roots, tr)
570 572 # some roots may need to be declared for lower phases
571 573 delroots.extend(olds - roots)
572 574 if not dryrun:
573 575 # declare deleted root in the target phase
574 576 if targetphase != 0:
575 577 self._retractboundary(repo, tr, targetphase, delroots)
576 578 repo.invalidatevolatilesets()
577 579 return changes
578 580
579 581 def retractboundary(self, repo, tr, targetphase, nodes):
580 582 oldroots = {
581 583 phase: revs
582 584 for phase, revs in pycompat.iteritems(self.phaseroots)
583 585 if phase <= targetphase
584 586 }
585 587 if tr is None:
586 588 phasetracking = None
587 589 else:
588 590 phasetracking = tr.changes.get(b'phases')
589 591 repo = repo.unfiltered()
590 592 if (
591 593 self._retractboundary(repo, tr, targetphase, nodes)
592 594 and phasetracking is not None
593 595 ):
594 596
595 597 # find the affected revisions
596 598 new = self.phaseroots[targetphase]
597 599 old = oldroots[targetphase]
598 600 affected = set(repo.revs(b'(%ln::) - (%ln::)', new, old))
599 601
600 602 # find the phase of the affected revision
601 603 for phase in pycompat.xrange(targetphase, -1, -1):
602 604 if phase:
603 605 roots = oldroots.get(phase, [])
604 606 revs = set(repo.revs(b'%ln::%ld', roots, affected))
605 607 affected -= revs
606 608 else: # public phase
607 609 revs = affected
608 610 for r in sorted(revs):
609 611 _trackphasechange(phasetracking, r, phase, targetphase)
610 612 repo.invalidatevolatilesets()
611 613
612 614 def _retractboundary(self, repo, tr, targetphase, nodes):
613 615 # Be careful to preserve shallow-copied values: do not update
614 616 # phaseroots values, replace them.
615 617 if targetphase in (archived, internal) and not supportinternal(repo):
616 618 name = phasenames[targetphase]
617 619 msg = b'this repository does not support the %s phase' % name
618 620 raise error.ProgrammingError(msg)
619 621
620 622 repo = repo.unfiltered()
621 623 torev = repo.changelog.rev
622 624 tonode = repo.changelog.node
623 625 currentroots = {torev(node) for node in self.phaseroots[targetphase]}
624 626 finalroots = oldroots = set(currentroots)
625 627 newroots = [torev(node) for node in nodes]
626 628 newroots = [
627 629 rev for rev in newroots if self.phase(repo, rev) < targetphase
628 630 ]
629 631
630 632 if newroots:
631 633 if nullrev in newroots:
632 634 raise error.Abort(_(b'cannot change null revision phase'))
633 635 currentroots.update(newroots)
634 636
635 637 # Only compute new roots for revs above the roots that are being
636 638 # retracted.
637 639 minnewroot = min(newroots)
638 640 aboveroots = [rev for rev in currentroots if rev >= minnewroot]
639 641 updatedroots = repo.revs(b'roots(%ld::)', aboveroots)
640 642
641 643 finalroots = {rev for rev in currentroots if rev < minnewroot}
642 644 finalroots.update(updatedroots)
643 645 if finalroots != oldroots:
644 646 self._updateroots(
645 647 targetphase, {tonode(rev) for rev in finalroots}, tr
646 648 )
647 649 return True
648 650 return False
649 651
650 652 def filterunknown(self, repo):
651 653 """remove unknown nodes from the phase boundary
652 654
653 655 Nothing is lost as unknown nodes only hold data for their descendants.
654 656 """
655 657 filtered = False
656 658 has_node = repo.changelog.index.has_node # to filter unknown nodes
657 659 for phase, nodes in pycompat.iteritems(self.phaseroots):
658 660 missing = sorted(node for node in nodes if not has_node(node))
659 661 if missing:
660 662 for mnode in missing:
661 663 repo.ui.debug(
662 664 b'removing unknown node %s from %i-phase boundary\n'
663 665 % (short(mnode), phase)
664 666 )
665 667 nodes.symmetric_difference_update(missing)
666 668 filtered = True
667 669 if filtered:
668 670 self.dirty = True
669 671 # filterunknown is called by repo.destroyed, we may have no changes in
670 672 # root but _phasesets contents is certainly invalid (or at least we
671 673 # have not proper way to check that). related to issue 3858.
672 674 #
673 675 # The other caller is __init__ that have no _phasesets initialized
674 676 # anyway. If this change we should consider adding a dedicated
675 677 # "destroyed" function to phasecache or a proper cache key mechanism
676 678 # (see branchmap one)
677 679 self.invalidate()
678 680
679 681
680 682 def advanceboundary(repo, tr, targetphase, nodes, dryrun=None):
681 683 """Add nodes to a phase changing other nodes phases if necessary.
682 684
683 685 This function move boundary *forward* this means that all nodes
684 686 are set in the target phase or kept in a *lower* phase.
685 687
686 688 Simplify boundary to contains phase roots only.
687 689
688 690 If dryrun is True, no actions will be performed
689 691
690 692 Returns a set of revs whose phase is changed or should be changed
691 693 """
692 694 phcache = repo._phasecache.copy()
693 695 changes = phcache.advanceboundary(
694 696 repo, tr, targetphase, nodes, dryrun=dryrun
695 697 )
696 698 if not dryrun:
697 699 repo._phasecache.replace(phcache)
698 700 return changes
699 701
700 702
701 703 def retractboundary(repo, tr, targetphase, nodes):
702 704 """Set nodes back to a phase changing other nodes phases if
703 705 necessary.
704 706
705 707 This function move boundary *backward* this means that all nodes
706 708 are set in the target phase or kept in a *higher* phase.
707 709
708 710 Simplify boundary to contains phase roots only."""
709 711 phcache = repo._phasecache.copy()
710 712 phcache.retractboundary(repo, tr, targetphase, nodes)
711 713 repo._phasecache.replace(phcache)
712 714
713 715
714 716 def registernew(repo, tr, targetphase, nodes):
715 717 """register a new revision and its phase
716 718
717 719 Code adding revisions to the repository should use this function to
718 720 set new changeset in their target phase (or higher).
719 721 """
720 722 phcache = repo._phasecache.copy()
721 723 phcache.registernew(repo, tr, targetphase, nodes)
722 724 repo._phasecache.replace(phcache)
723 725
724 726
725 727 def listphases(repo):
726 728 """List phases root for serialization over pushkey"""
727 729 # Use ordered dictionary so behavior is deterministic.
728 730 keys = util.sortdict()
729 731 value = b'%i' % draft
730 732 cl = repo.unfiltered().changelog
731 733 for root in repo._phasecache.phaseroots[draft]:
732 734 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
733 735 keys[hex(root)] = value
734 736
735 737 if repo.publishing():
736 738 # Add an extra data to let remote know we are a publishing
737 739 # repo. Publishing repo can't just pretend they are old repo.
738 740 # When pushing to a publishing repo, the client still need to
739 741 # push phase boundary
740 742 #
741 743 # Push do not only push changeset. It also push phase data.
742 744 # New phase data may apply to common changeset which won't be
743 745 # push (as they are common). Here is a very simple example:
744 746 #
745 747 # 1) repo A push changeset X as draft to repo B
746 748 # 2) repo B make changeset X public
747 749 # 3) repo B push to repo A. X is not pushed but the data that
748 750 # X as now public should
749 751 #
750 752 # The server can't handle it on it's own as it has no idea of
751 753 # client phase data.
752 754 keys[b'publishing'] = b'True'
753 755 return keys
754 756
755 757
756 758 def pushphase(repo, nhex, oldphasestr, newphasestr):
757 759 """List phases root for serialization over pushkey"""
758 760 repo = repo.unfiltered()
759 761 with repo.lock():
760 762 currentphase = repo[nhex].phase()
761 763 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
762 764 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
763 765 if currentphase == oldphase and newphase < oldphase:
764 766 with repo.transaction(b'pushkey-phase') as tr:
765 767 advanceboundary(repo, tr, newphase, [bin(nhex)])
766 768 return True
767 769 elif currentphase == newphase:
768 770 # raced, but got correct result
769 771 return True
770 772 else:
771 773 return False
772 774
773 775
774 776 def subsetphaseheads(repo, subset):
775 777 """Finds the phase heads for a subset of a history
776 778
777 779 Returns a list indexed by phase number where each item is a list of phase
778 780 head nodes.
779 781 """
780 782 cl = repo.changelog
781 783
782 784 headsbyphase = {i: [] for i in allphases}
783 785 # No need to keep track of secret phase; any heads in the subset that
784 786 # are not mentioned are implicitly secret.
785 787 for phase in allphases[:secret]:
786 788 revset = b"heads(%%ln & %s())" % phasenames[phase]
787 789 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
788 790 return headsbyphase
789 791
790 792
791 793 def updatephases(repo, trgetter, headsbyphase):
792 794 """Updates the repo with the given phase heads"""
793 795 # Now advance phase boundaries of all phases
794 796 #
795 797 # run the update (and fetch transaction) only if there are actually things
796 798 # to update. This avoid creating empty transaction during no-op operation.
797 799
798 800 for phase in allphases:
799 801 revset = b'%ln - _phase(%s)'
800 802 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
801 803 if heads:
802 804 advanceboundary(repo, trgetter(), phase, heads)
803 805
804 806
805 807 def analyzeremotephases(repo, subset, roots):
806 808 """Compute phases heads and root in a subset of node from root dict
807 809
808 810 * subset is heads of the subset
809 811 * roots is {<nodeid> => phase} mapping. key and value are string.
810 812
811 813 Accept unknown element input
812 814 """
813 815 repo = repo.unfiltered()
814 816 # build list from dictionary
815 817 draftroots = []
816 818 has_node = repo.changelog.index.has_node # to filter unknown nodes
817 819 for nhex, phase in pycompat.iteritems(roots):
818 820 if nhex == b'publishing': # ignore data related to publish option
819 821 continue
820 822 node = bin(nhex)
821 823 phase = int(phase)
822 824 if phase == public:
823 825 if node != nullid:
824 826 repo.ui.warn(
825 827 _(
826 828 b'ignoring inconsistent public root'
827 829 b' from remote: %s\n'
828 830 )
829 831 % nhex
830 832 )
831 833 elif phase == draft:
832 834 if has_node(node):
833 835 draftroots.append(node)
834 836 else:
835 837 repo.ui.warn(
836 838 _(b'ignoring unexpected root from remote: %i %s\n')
837 839 % (phase, nhex)
838 840 )
839 841 # compute heads
840 842 publicheads = newheads(repo, subset, draftroots)
841 843 return publicheads, draftroots
842 844
843 845
844 846 class remotephasessummary(object):
845 847 """summarize phase information on the remote side
846 848
847 849 :publishing: True is the remote is publishing
848 850 :publicheads: list of remote public phase heads (nodes)
849 851 :draftheads: list of remote draft phase heads (nodes)
850 852 :draftroots: list of remote draft phase root (nodes)
851 853 """
852 854
853 855 def __init__(self, repo, remotesubset, remoteroots):
854 856 unfi = repo.unfiltered()
855 857 self._allremoteroots = remoteroots
856 858
857 859 self.publishing = remoteroots.get(b'publishing', False)
858 860
859 861 ana = analyzeremotephases(repo, remotesubset, remoteroots)
860 862 self.publicheads, self.draftroots = ana
861 863 # Get the list of all "heads" revs draft on remote
862 864 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
863 865 self.draftheads = [c.node() for c in dheads]
864 866
865 867
866 868 def newheads(repo, heads, roots):
867 869 """compute new head of a subset minus another
868 870
869 871 * `heads`: define the first subset
870 872 * `roots`: define the second we subtract from the first"""
871 873 # prevent an import cycle
872 874 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
873 875 from . import dagop
874 876
875 877 repo = repo.unfiltered()
876 878 cl = repo.changelog
877 879 rev = cl.index.get_rev
878 880 if not roots:
879 881 return heads
880 882 if not heads or heads == [nullid]:
881 883 return []
882 884 # The logic operated on revisions, convert arguments early for convenience
883 885 new_heads = {rev(n) for n in heads if n != nullid}
884 886 roots = [rev(n) for n in roots]
885 887 # compute the area we need to remove
886 888 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
887 889 # heads in the area are no longer heads
888 890 new_heads.difference_update(affected_zone)
889 891 # revisions in the area have children outside of it,
890 892 # They might be new heads
891 893 candidates = repo.revs(
892 894 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
893 895 )
894 896 candidates -= affected_zone
895 897 if new_heads or candidates:
896 898 # remove candidate that are ancestors of other heads
897 899 new_heads.update(candidates)
898 900 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
899 901 pruned = dagop.reachableroots(repo, candidates, prunestart)
900 902 new_heads.difference_update(pruned)
901 903
902 904 return pycompat.maplist(cl.node, sorted(new_heads))
903 905
904 906
905 907 def newcommitphase(ui):
906 908 """helper to get the target phase of new commit
907 909
908 910 Handle all possible values for the phases.new-commit options.
909 911
910 912 """
911 913 v = ui.config(b'phases', b'new-commit')
912 914 try:
913 915 return phasenumber2[v]
914 916 except KeyError:
915 917 raise error.ConfigError(
916 918 _(b"phases.new-commit: not a valid phase name ('%s')") % v
917 919 )
918 920
919 921
920 922 def hassecret(repo):
921 923 """utility function that check if a repo have any secret changeset."""
922 924 return bool(repo._phasecache.phaseroots[secret])
923 925
924 926
925 927 def preparehookargs(node, old, new):
926 928 if old is None:
927 929 old = b''
928 930 else:
929 931 old = phasenames[old]
930 932 return {b'node': node, b'oldphase': old, b'phase': phasenames[new]}
General Comments 0
You need to be logged in to leave comments. Login now