##// END OF EJS Templates
phases: make the working directory consistently a draft...
Rodrigo Damazio Bovendorp -
r44456:9c1fd975 default
parent child Browse files
Show More
@@ -1,791 +1,811 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 wdirrev,
115 116 )
116 117 from .pycompat import (
117 118 getattr,
118 119 setattr,
119 120 )
120 121 from . import (
121 122 error,
122 123 pycompat,
123 124 smartset,
124 125 txnutil,
125 126 util,
126 127 )
127 128
128 129 _fphasesentry = struct.Struct(b'>i20s')
129 130
130 131 INTERNAL_FLAG = 64 # Phases for mercurial internal usage only
131 132 HIDEABLE_FLAG = 32 # Phases that are hideable
132 133
133 134 # record phase index
134 135 public, draft, secret = range(3)
135 136 internal = INTERNAL_FLAG | HIDEABLE_FLAG
136 137 archived = HIDEABLE_FLAG
137 138 allphases = list(range(internal + 1))
138 139 trackedphases = allphases[1:]
139 140 # record phase names
140 141 cmdphasenames = [b'public', b'draft', b'secret'] # known to `hg phase` command
141 142 phasenames = [None] * len(allphases)
142 143 phasenames[: len(cmdphasenames)] = cmdphasenames
143 144 phasenames[archived] = b'archived'
144 145 phasenames[internal] = b'internal'
145 146 # record phase property
146 147 mutablephases = tuple(allphases[1:])
147 148 remotehiddenphases = tuple(allphases[2:])
148 149 localhiddenphases = tuple(p for p in allphases if p & HIDEABLE_FLAG)
149 150
150 151
151 152 def supportinternal(repo):
152 153 """True if the internal phase can be used on a repository"""
153 154 return b'internal-phase' in repo.requirements
154 155
155 156
156 157 def _readroots(repo, phasedefaults=None):
157 158 """Read phase roots from disk
158 159
159 160 phasedefaults is a list of fn(repo, roots) callable, which are
160 161 executed if the phase roots file does not exist. When phases are
161 162 being initialized on an existing repository, this could be used to
162 163 set selected changesets phase to something else than public.
163 164
164 165 Return (roots, dirty) where dirty is true if roots differ from
165 166 what is being stored.
166 167 """
167 168 repo = repo.unfiltered()
168 169 dirty = False
169 170 roots = [set() for i in allphases]
170 171 try:
171 172 f, pending = txnutil.trypending(repo.root, repo.svfs, b'phaseroots')
172 173 try:
173 174 for line in f:
174 175 phase, nh = line.split()
175 176 roots[int(phase)].add(bin(nh))
176 177 finally:
177 178 f.close()
178 179 except IOError as inst:
179 180 if inst.errno != errno.ENOENT:
180 181 raise
181 182 if phasedefaults:
182 183 for f in phasedefaults:
183 184 roots = f(repo, roots)
184 185 dirty = True
185 186 return roots, dirty
186 187
187 188
188 189 def binaryencode(phasemapping):
189 190 """encode a 'phase -> nodes' mapping into a binary stream
190 191
191 192 Since phases are integer the mapping is actually a python list:
192 193 [[PUBLIC_HEADS], [DRAFTS_HEADS], [SECRET_HEADS]]
193 194 """
194 195 binarydata = []
195 196 for phase, nodes in enumerate(phasemapping):
196 197 for head in nodes:
197 198 binarydata.append(_fphasesentry.pack(phase, head))
198 199 return b''.join(binarydata)
199 200
200 201
201 202 def binarydecode(stream):
202 203 """decode a binary stream into a 'phase -> nodes' mapping
203 204
204 205 Since phases are integer the mapping is actually a python list."""
205 206 headsbyphase = [[] for i in allphases]
206 207 entrysize = _fphasesentry.size
207 208 while True:
208 209 entry = stream.read(entrysize)
209 210 if len(entry) < entrysize:
210 211 if entry:
211 212 raise error.Abort(_(b'bad phase-heads stream'))
212 213 break
213 214 phase, node = _fphasesentry.unpack(entry)
214 215 headsbyphase[phase].append(node)
215 216 return headsbyphase
216 217
217 218
218 219 def _trackphasechange(data, rev, old, new):
219 220 """add a phase move the <data> dictionnary
220 221
221 222 If data is None, nothing happens.
222 223 """
223 224 if data is None:
224 225 return
225 226 existing = data.get(rev)
226 227 if existing is not None:
227 228 old = existing[0]
228 229 data[rev] = (old, new)
229 230
230 231
231 232 class phasecache(object):
232 233 def __init__(self, repo, phasedefaults, _load=True):
233 234 if _load:
234 235 # Cheap trick to allow shallow-copy without copy module
235 236 self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
236 237 self._loadedrevslen = 0
237 238 self._phasesets = None
238 239 self.filterunknown(repo)
239 240 self.opener = repo.svfs
240 241
241 242 def getrevset(self, repo, phases, subset=None):
242 243 """return a smartset for the given phases"""
243 244 self.loadphaserevs(repo) # ensure phase's sets are loaded
244 245 phases = set(phases)
246
245 247 if public not in phases:
246 248 # fast path: _phasesets contains the interesting sets,
247 249 # might only need a union and post-filtering.
248 250 if len(phases) == 1:
249 251 [p] = phases
250 252 revs = self._phasesets[p]
251 253 else:
252 254 revs = set.union(*[self._phasesets[p] for p in phases])
253 255 if repo.changelog.filteredrevs:
254 256 revs = revs - repo.changelog.filteredrevs
257
255 258 if subset is None:
256 259 return smartset.baseset(revs)
257 260 else:
261 if wdirrev in subset and repo[None].phase() in phases:
262 # The working dir would never be in the cache, but it was
263 # in the subset being filtered for its phase, so add it to
264 # the output.
265 revs.add(wdirrev)
266
258 267 return subset & smartset.baseset(revs)
259 268 else:
269 # phases keeps all the *other* phases.
260 270 phases = set(allphases).difference(phases)
261 271 if not phases:
262 272 return smartset.fullreposet(repo)
273
274 # revs has the revisions in all *other* phases.
263 275 if len(phases) == 1:
264 276 [p] = phases
265 277 revs = self._phasesets[p]
266 278 else:
267 279 revs = set.union(*[self._phasesets[p] for p in phases])
280
268 281 if subset is None:
269 282 subset = smartset.fullreposet(repo)
283
284 if wdirrev in subset and repo[None].phase() in phases:
285 # The working dir is in the subset being filtered, and its
286 # phase is in the phases *not* being returned, so add it to the
287 # set of revisions to filter out.
288 revs.add(wdirrev)
289
270 290 if not revs:
271 291 return subset
272 292 return subset.filter(lambda r: r not in revs)
273 293
274 294 def copy(self):
275 295 # Shallow copy meant to ensure isolation in
276 296 # advance/retractboundary(), nothing more.
277 297 ph = self.__class__(None, None, _load=False)
278 298 ph.phaseroots = self.phaseroots[:]
279 299 ph.dirty = self.dirty
280 300 ph.opener = self.opener
281 301 ph._loadedrevslen = self._loadedrevslen
282 302 ph._phasesets = self._phasesets
283 303 return ph
284 304
285 305 def replace(self, phcache):
286 306 """replace all values in 'self' with content of phcache"""
287 307 for a in (
288 308 b'phaseroots',
289 309 b'dirty',
290 310 b'opener',
291 311 b'_loadedrevslen',
292 312 b'_phasesets',
293 313 ):
294 314 setattr(self, a, getattr(phcache, a))
295 315
296 316 def _getphaserevsnative(self, repo):
297 317 repo = repo.unfiltered()
298 318 nativeroots = []
299 319 for phase in trackedphases:
300 320 nativeroots.append(
301 321 pycompat.maplist(repo.changelog.rev, self.phaseroots[phase])
302 322 )
303 323 return repo.changelog.computephases(nativeroots)
304 324
305 325 def _computephaserevspure(self, repo):
306 326 repo = repo.unfiltered()
307 327 cl = repo.changelog
308 328 self._phasesets = [set() for phase in allphases]
309 329 lowerroots = set()
310 330 for phase in reversed(trackedphases):
311 331 roots = pycompat.maplist(cl.rev, self.phaseroots[phase])
312 332 if roots:
313 333 ps = set(cl.descendants(roots))
314 334 for root in roots:
315 335 ps.add(root)
316 336 ps.difference_update(lowerroots)
317 337 lowerroots.update(ps)
318 338 self._phasesets[phase] = ps
319 339 self._loadedrevslen = len(cl)
320 340
321 341 def loadphaserevs(self, repo):
322 342 """ensure phase information is loaded in the object"""
323 343 if self._phasesets is None:
324 344 try:
325 345 res = self._getphaserevsnative(repo)
326 346 self._loadedrevslen, self._phasesets = res
327 347 except AttributeError:
328 348 self._computephaserevspure(repo)
329 349
330 350 def invalidate(self):
331 351 self._loadedrevslen = 0
332 352 self._phasesets = None
333 353
334 354 def phase(self, repo, rev):
335 355 # We need a repo argument here to be able to build _phasesets
336 356 # if necessary. The repository instance is not stored in
337 357 # phasecache to avoid reference cycles. The changelog instance
338 358 # is not stored because it is a filecache() property and can
339 359 # be replaced without us being notified.
340 360 if rev == nullrev:
341 361 return public
342 362 if rev < nullrev:
343 363 raise ValueError(_(b'cannot lookup negative revision'))
344 364 if rev >= self._loadedrevslen:
345 365 self.invalidate()
346 366 self.loadphaserevs(repo)
347 367 for phase in trackedphases:
348 368 if rev in self._phasesets[phase]:
349 369 return phase
350 370 return public
351 371
352 372 def write(self):
353 373 if not self.dirty:
354 374 return
355 375 f = self.opener(b'phaseroots', b'w', atomictemp=True, checkambig=True)
356 376 try:
357 377 self._write(f)
358 378 finally:
359 379 f.close()
360 380
361 381 def _write(self, fp):
362 382 for phase, roots in enumerate(self.phaseroots):
363 383 for h in sorted(roots):
364 384 fp.write(b'%i %s\n' % (phase, hex(h)))
365 385 self.dirty = False
366 386
367 387 def _updateroots(self, phase, newroots, tr):
368 388 self.phaseroots[phase] = newroots
369 389 self.invalidate()
370 390 self.dirty = True
371 391
372 392 tr.addfilegenerator(b'phase', (b'phaseroots',), self._write)
373 393 tr.hookargs[b'phases_moved'] = b'1'
374 394
375 395 def registernew(self, repo, tr, targetphase, nodes):
376 396 repo = repo.unfiltered()
377 397 self._retractboundary(repo, tr, targetphase, nodes)
378 398 if tr is not None and b'phases' in tr.changes:
379 399 phasetracking = tr.changes[b'phases']
380 400 torev = repo.changelog.rev
381 401 phase = self.phase
382 402 for n in nodes:
383 403 rev = torev(n)
384 404 revphase = phase(repo, rev)
385 405 _trackphasechange(phasetracking, rev, None, revphase)
386 406 repo.invalidatevolatilesets()
387 407
388 408 def advanceboundary(self, repo, tr, targetphase, nodes, dryrun=None):
389 409 """Set all 'nodes' to phase 'targetphase'
390 410
391 411 Nodes with a phase lower than 'targetphase' are not affected.
392 412
393 413 If dryrun is True, no actions will be performed
394 414
395 415 Returns a set of revs whose phase is changed or should be changed
396 416 """
397 417 # Be careful to preserve shallow-copied values: do not update
398 418 # phaseroots values, replace them.
399 419 if tr is None:
400 420 phasetracking = None
401 421 else:
402 422 phasetracking = tr.changes.get(b'phases')
403 423
404 424 repo = repo.unfiltered()
405 425
406 426 changes = set() # set of revisions to be changed
407 427 delroots = [] # set of root deleted by this path
408 428 for phase in pycompat.xrange(targetphase + 1, len(allphases)):
409 429 # filter nodes that are not in a compatible phase already
410 430 nodes = [
411 431 n for n in nodes if self.phase(repo, repo[n].rev()) >= phase
412 432 ]
413 433 if not nodes:
414 434 break # no roots to move anymore
415 435
416 436 olds = self.phaseroots[phase]
417 437
418 438 affected = repo.revs(b'%ln::%ln', olds, nodes)
419 439 changes.update(affected)
420 440 if dryrun:
421 441 continue
422 442 for r in affected:
423 443 _trackphasechange(
424 444 phasetracking, r, self.phase(repo, r), targetphase
425 445 )
426 446
427 447 roots = set(
428 448 ctx.node()
429 449 for ctx in repo.set(b'roots((%ln::) - %ld)', olds, affected)
430 450 )
431 451 if olds != roots:
432 452 self._updateroots(phase, roots, tr)
433 453 # some roots may need to be declared for lower phases
434 454 delroots.extend(olds - roots)
435 455 if not dryrun:
436 456 # declare deleted root in the target phase
437 457 if targetphase != 0:
438 458 self._retractboundary(repo, tr, targetphase, delroots)
439 459 repo.invalidatevolatilesets()
440 460 return changes
441 461
442 462 def retractboundary(self, repo, tr, targetphase, nodes):
443 463 oldroots = self.phaseroots[: targetphase + 1]
444 464 if tr is None:
445 465 phasetracking = None
446 466 else:
447 467 phasetracking = tr.changes.get(b'phases')
448 468 repo = repo.unfiltered()
449 469 if (
450 470 self._retractboundary(repo, tr, targetphase, nodes)
451 471 and phasetracking is not None
452 472 ):
453 473
454 474 # find the affected revisions
455 475 new = self.phaseroots[targetphase]
456 476 old = oldroots[targetphase]
457 477 affected = set(repo.revs(b'(%ln::) - (%ln::)', new, old))
458 478
459 479 # find the phase of the affected revision
460 480 for phase in pycompat.xrange(targetphase, -1, -1):
461 481 if phase:
462 482 roots = oldroots[phase]
463 483 revs = set(repo.revs(b'%ln::%ld', roots, affected))
464 484 affected -= revs
465 485 else: # public phase
466 486 revs = affected
467 487 for r in revs:
468 488 _trackphasechange(phasetracking, r, phase, targetphase)
469 489 repo.invalidatevolatilesets()
470 490
471 491 def _retractboundary(self, repo, tr, targetphase, nodes):
472 492 # Be careful to preserve shallow-copied values: do not update
473 493 # phaseroots values, replace them.
474 494 if targetphase in (archived, internal) and not supportinternal(repo):
475 495 name = phasenames[targetphase]
476 496 msg = b'this repository does not support the %s phase' % name
477 497 raise error.ProgrammingError(msg)
478 498
479 499 repo = repo.unfiltered()
480 500 currentroots = self.phaseroots[targetphase]
481 501 finalroots = oldroots = set(currentroots)
482 502 newroots = [
483 503 n for n in nodes if self.phase(repo, repo[n].rev()) < targetphase
484 504 ]
485 505 if newroots:
486 506
487 507 if nullid in newroots:
488 508 raise error.Abort(_(b'cannot change null revision phase'))
489 509 currentroots = currentroots.copy()
490 510 currentroots.update(newroots)
491 511
492 512 # Only compute new roots for revs above the roots that are being
493 513 # retracted.
494 514 minnewroot = min(repo[n].rev() for n in newroots)
495 515 aboveroots = [
496 516 n for n in currentroots if repo[n].rev() >= minnewroot
497 517 ]
498 518 updatedroots = repo.set(b'roots(%ln::)', aboveroots)
499 519
500 520 finalroots = set(
501 521 n for n in currentroots if repo[n].rev() < minnewroot
502 522 )
503 523 finalroots.update(ctx.node() for ctx in updatedroots)
504 524 if finalroots != oldroots:
505 525 self._updateroots(targetphase, finalroots, tr)
506 526 return True
507 527 return False
508 528
509 529 def filterunknown(self, repo):
510 530 """remove unknown nodes from the phase boundary
511 531
512 532 Nothing is lost as unknown nodes only hold data for their descendants.
513 533 """
514 534 filtered = False
515 535 has_node = repo.changelog.index.has_node # to filter unknown nodes
516 536 for phase, nodes in enumerate(self.phaseroots):
517 537 missing = sorted(node for node in nodes if not has_node(node))
518 538 if missing:
519 539 for mnode in missing:
520 540 repo.ui.debug(
521 541 b'removing unknown node %s from %i-phase boundary\n'
522 542 % (short(mnode), phase)
523 543 )
524 544 nodes.symmetric_difference_update(missing)
525 545 filtered = True
526 546 if filtered:
527 547 self.dirty = True
528 548 # filterunknown is called by repo.destroyed, we may have no changes in
529 549 # root but _phasesets contents is certainly invalid (or at least we
530 550 # have not proper way to check that). related to issue 3858.
531 551 #
532 552 # The other caller is __init__ that have no _phasesets initialized
533 553 # anyway. If this change we should consider adding a dedicated
534 554 # "destroyed" function to phasecache or a proper cache key mechanism
535 555 # (see branchmap one)
536 556 self.invalidate()
537 557
538 558
539 559 def advanceboundary(repo, tr, targetphase, nodes, dryrun=None):
540 560 """Add nodes to a phase changing other nodes phases if necessary.
541 561
542 562 This function move boundary *forward* this means that all nodes
543 563 are set in the target phase or kept in a *lower* phase.
544 564
545 565 Simplify boundary to contains phase roots only.
546 566
547 567 If dryrun is True, no actions will be performed
548 568
549 569 Returns a set of revs whose phase is changed or should be changed
550 570 """
551 571 phcache = repo._phasecache.copy()
552 572 changes = phcache.advanceboundary(
553 573 repo, tr, targetphase, nodes, dryrun=dryrun
554 574 )
555 575 if not dryrun:
556 576 repo._phasecache.replace(phcache)
557 577 return changes
558 578
559 579
560 580 def retractboundary(repo, tr, targetphase, nodes):
561 581 """Set nodes back to a phase changing other nodes phases if
562 582 necessary.
563 583
564 584 This function move boundary *backward* this means that all nodes
565 585 are set in the target phase or kept in a *higher* phase.
566 586
567 587 Simplify boundary to contains phase roots only."""
568 588 phcache = repo._phasecache.copy()
569 589 phcache.retractboundary(repo, tr, targetphase, nodes)
570 590 repo._phasecache.replace(phcache)
571 591
572 592
573 593 def registernew(repo, tr, targetphase, nodes):
574 594 """register a new revision and its phase
575 595
576 596 Code adding revisions to the repository should use this function to
577 597 set new changeset in their target phase (or higher).
578 598 """
579 599 phcache = repo._phasecache.copy()
580 600 phcache.registernew(repo, tr, targetphase, nodes)
581 601 repo._phasecache.replace(phcache)
582 602
583 603
584 604 def listphases(repo):
585 605 """List phases root for serialization over pushkey"""
586 606 # Use ordered dictionary so behavior is deterministic.
587 607 keys = util.sortdict()
588 608 value = b'%i' % draft
589 609 cl = repo.unfiltered().changelog
590 610 for root in repo._phasecache.phaseroots[draft]:
591 611 if repo._phasecache.phase(repo, cl.rev(root)) <= draft:
592 612 keys[hex(root)] = value
593 613
594 614 if repo.publishing():
595 615 # Add an extra data to let remote know we are a publishing
596 616 # repo. Publishing repo can't just pretend they are old repo.
597 617 # When pushing to a publishing repo, the client still need to
598 618 # push phase boundary
599 619 #
600 620 # Push do not only push changeset. It also push phase data.
601 621 # New phase data may apply to common changeset which won't be
602 622 # push (as they are common). Here is a very simple example:
603 623 #
604 624 # 1) repo A push changeset X as draft to repo B
605 625 # 2) repo B make changeset X public
606 626 # 3) repo B push to repo A. X is not pushed but the data that
607 627 # X as now public should
608 628 #
609 629 # The server can't handle it on it's own as it has no idea of
610 630 # client phase data.
611 631 keys[b'publishing'] = b'True'
612 632 return keys
613 633
614 634
615 635 def pushphase(repo, nhex, oldphasestr, newphasestr):
616 636 """List phases root for serialization over pushkey"""
617 637 repo = repo.unfiltered()
618 638 with repo.lock():
619 639 currentphase = repo[nhex].phase()
620 640 newphase = abs(int(newphasestr)) # let's avoid negative index surprise
621 641 oldphase = abs(int(oldphasestr)) # let's avoid negative index surprise
622 642 if currentphase == oldphase and newphase < oldphase:
623 643 with repo.transaction(b'pushkey-phase') as tr:
624 644 advanceboundary(repo, tr, newphase, [bin(nhex)])
625 645 return True
626 646 elif currentphase == newphase:
627 647 # raced, but got correct result
628 648 return True
629 649 else:
630 650 return False
631 651
632 652
633 653 def subsetphaseheads(repo, subset):
634 654 """Finds the phase heads for a subset of a history
635 655
636 656 Returns a list indexed by phase number where each item is a list of phase
637 657 head nodes.
638 658 """
639 659 cl = repo.changelog
640 660
641 661 headsbyphase = [[] for i in allphases]
642 662 # No need to keep track of secret phase; any heads in the subset that
643 663 # are not mentioned are implicitly secret.
644 664 for phase in allphases[:secret]:
645 665 revset = b"heads(%%ln & %s())" % phasenames[phase]
646 666 headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
647 667 return headsbyphase
648 668
649 669
650 670 def updatephases(repo, trgetter, headsbyphase):
651 671 """Updates the repo with the given phase heads"""
652 672 # Now advance phase boundaries of all but secret phase
653 673 #
654 674 # run the update (and fetch transaction) only if there are actually things
655 675 # to update. This avoid creating empty transaction during no-op operation.
656 676
657 677 for phase in allphases[:-1]:
658 678 revset = b'%ln - _phase(%s)'
659 679 heads = [c.node() for c in repo.set(revset, headsbyphase[phase], phase)]
660 680 if heads:
661 681 advanceboundary(repo, trgetter(), phase, heads)
662 682
663 683
664 684 def analyzeremotephases(repo, subset, roots):
665 685 """Compute phases heads and root in a subset of node from root dict
666 686
667 687 * subset is heads of the subset
668 688 * roots is {<nodeid> => phase} mapping. key and value are string.
669 689
670 690 Accept unknown element input
671 691 """
672 692 repo = repo.unfiltered()
673 693 # build list from dictionary
674 694 draftroots = []
675 695 has_node = repo.changelog.index.has_node # to filter unknown nodes
676 696 for nhex, phase in pycompat.iteritems(roots):
677 697 if nhex == b'publishing': # ignore data related to publish option
678 698 continue
679 699 node = bin(nhex)
680 700 phase = int(phase)
681 701 if phase == public:
682 702 if node != nullid:
683 703 repo.ui.warn(
684 704 _(
685 705 b'ignoring inconsistent public root'
686 706 b' from remote: %s\n'
687 707 )
688 708 % nhex
689 709 )
690 710 elif phase == draft:
691 711 if has_node(node):
692 712 draftroots.append(node)
693 713 else:
694 714 repo.ui.warn(
695 715 _(b'ignoring unexpected root from remote: %i %s\n')
696 716 % (phase, nhex)
697 717 )
698 718 # compute heads
699 719 publicheads = newheads(repo, subset, draftroots)
700 720 return publicheads, draftroots
701 721
702 722
703 723 class remotephasessummary(object):
704 724 """summarize phase information on the remote side
705 725
706 726 :publishing: True is the remote is publishing
707 727 :publicheads: list of remote public phase heads (nodes)
708 728 :draftheads: list of remote draft phase heads (nodes)
709 729 :draftroots: list of remote draft phase root (nodes)
710 730 """
711 731
712 732 def __init__(self, repo, remotesubset, remoteroots):
713 733 unfi = repo.unfiltered()
714 734 self._allremoteroots = remoteroots
715 735
716 736 self.publishing = remoteroots.get(b'publishing', False)
717 737
718 738 ana = analyzeremotephases(repo, remotesubset, remoteroots)
719 739 self.publicheads, self.draftroots = ana
720 740 # Get the list of all "heads" revs draft on remote
721 741 dheads = unfi.set(b'heads(%ln::%ln)', self.draftroots, remotesubset)
722 742 self.draftheads = [c.node() for c in dheads]
723 743
724 744
725 745 def newheads(repo, heads, roots):
726 746 """compute new head of a subset minus another
727 747
728 748 * `heads`: define the first subset
729 749 * `roots`: define the second we subtract from the first"""
730 750 # prevent an import cycle
731 751 # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
732 752 from . import dagop
733 753
734 754 repo = repo.unfiltered()
735 755 cl = repo.changelog
736 756 rev = cl.index.get_rev
737 757 if not roots:
738 758 return heads
739 759 if not heads or heads == [nullid]:
740 760 return []
741 761 # The logic operated on revisions, convert arguments early for convenience
742 762 new_heads = set(rev(n) for n in heads if n != nullid)
743 763 roots = [rev(n) for n in roots]
744 764 # compute the area we need to remove
745 765 affected_zone = repo.revs(b"(%ld::%ld)", roots, new_heads)
746 766 # heads in the area are no longer heads
747 767 new_heads.difference_update(affected_zone)
748 768 # revisions in the area have children outside of it,
749 769 # They might be new heads
750 770 candidates = repo.revs(
751 771 b"parents(%ld + (%ld and merge())) and not null", roots, affected_zone
752 772 )
753 773 candidates -= affected_zone
754 774 if new_heads or candidates:
755 775 # remove candidate that are ancestors of other heads
756 776 new_heads.update(candidates)
757 777 prunestart = repo.revs(b"parents(%ld) and not null", new_heads)
758 778 pruned = dagop.reachableroots(repo, candidates, prunestart)
759 779 new_heads.difference_update(pruned)
760 780
761 781 return pycompat.maplist(cl.node, sorted(new_heads))
762 782
763 783
764 784 def newcommitphase(ui):
765 785 """helper to get the target phase of new commit
766 786
767 787 Handle all possible values for the phases.new-commit options.
768 788
769 789 """
770 790 v = ui.config(b'phases', b'new-commit')
771 791 try:
772 792 return phasenames.index(v)
773 793 except ValueError:
774 794 try:
775 795 return int(v)
776 796 except ValueError:
777 797 msg = _(b"phases.new-commit: not a valid phase name ('%s')")
778 798 raise error.ConfigError(msg % v)
779 799
780 800
781 801 def hassecret(repo):
782 802 """utility function that check if a repo have any secret changeset."""
783 803 return bool(repo._phasecache.phaseroots[2])
784 804
785 805
786 806 def preparehookargs(node, old, new):
787 807 if old is None:
788 808 old = b''
789 809 else:
790 810 old = phasenames[old]
791 811 return {b'node': node, b'oldphase': old, b'phase': phasenames[new]}
@@ -1,956 +1,1001 b''
1 1 $ cat > $TESTTMP/hook.sh << 'EOF'
2 2 > echo "test-hook-close-phase: $HG_NODE: $HG_OLDPHASE -> $HG_PHASE"
3 3 > EOF
4 4
5 5 $ cat >> $HGRCPATH << EOF
6 6 > [extensions]
7 7 > phasereport=$TESTDIR/testlib/ext-phase-report.py
8 8 > [hooks]
9 9 > txnclose-phase.test = sh $TESTTMP/hook.sh
10 10 > EOF
11 11
12 12 $ hglog() { hg log --template "{rev} {phaseidx} {desc}\n" $*; }
13 13 $ mkcommit() {
14 14 > echo "$1" > "$1"
15 15 > hg add "$1"
16 16 > message="$1"
17 17 > shift
18 18 > hg ci -m "$message" $*
19 19 > }
20 20
21 21 $ hg init initialrepo
22 22 $ cd initialrepo
23 23
24 24 Cannot change null revision phase
25 25
26 26 $ hg phase --force --secret null
27 27 abort: cannot change null revision phase
28 28 [255]
29 29 $ hg phase null
30 30 -1: public
31 31
32 32 $ mkcommit A
33 33 test-debug-phase: new rev 0: x -> 1
34 34 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> draft
35 35
36 36 New commit are draft by default
37 37
38 38 $ hglog
39 39 0 1 A
40 40
41 41 Following commit are draft too
42 42
43 43 $ mkcommit B
44 44 test-debug-phase: new rev 1: x -> 1
45 45 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: -> draft
46 46
47 47 $ hglog
48 48 1 1 B
49 49 0 1 A
50 50
51 Draft commit are properly created over public one:
51 Working directory phase is secret when its parent is secret.
52
53 $ hg phase --force --secret .
54 test-debug-phase: move rev 0: 1 -> 2
55 test-debug-phase: move rev 1: 1 -> 2
56 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: draft -> secret
57 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: draft -> secret
58 $ hg log -r 'wdir()' -T '{phase}\n'
59 secret
60 $ hg log -r 'wdir() and public()' -T '{phase}\n'
61 $ hg log -r 'wdir() and draft()' -T '{phase}\n'
62 $ hg log -r 'wdir() and secret()' -T '{phase}\n'
63 secret
64
65 Working directory phase is draft when its parent is draft.
66
67 $ hg phase --draft .
68 test-debug-phase: move rev 1: 2 -> 1
69 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: secret -> draft
70 $ hg log -r 'wdir()' -T '{phase}\n'
71 draft
72 $ hg log -r 'wdir() and public()' -T '{phase}\n'
73 $ hg log -r 'wdir() and draft()' -T '{phase}\n'
74 draft
75 $ hg log -r 'wdir() and secret()' -T '{phase}\n'
76
77 Working directory phase is secret when a new commit will be created as secret,
78 even if the parent is draft.
79
80 $ hg log -r 'wdir() and secret()' -T '{phase}\n' \
81 > --config phases.new-commit='secret'
82 secret
83
84 Working directory phase is draft when its parent is public.
52 85
53 86 $ hg phase --public .
54 87 test-debug-phase: move rev 0: 1 -> 0
55 88 test-debug-phase: move rev 1: 1 -> 0
56 89 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: draft -> public
57 90 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: draft -> public
91 $ hg log -r 'wdir()' -T '{phase}\n'
92 draft
93 $ hg log -r 'wdir() and public()' -T '{phase}\n'
94 $ hg log -r 'wdir() and draft()' -T '{phase}\n'
95 draft
96 $ hg log -r 'wdir() and secret()' -T '{phase}\n'
97 $ hg log -r 'wdir() and secret()' -T '{phase}\n' \
98 > --config phases.new-commit='secret'
99 secret
100
101 Draft commit are properly created over public one:
102
58 103 $ hg phase
59 104 1: public
60 105 $ hglog
61 106 1 0 B
62 107 0 0 A
63 108
64 109 $ mkcommit C
65 110 test-debug-phase: new rev 2: x -> 1
66 111 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: -> draft
67 112 $ mkcommit D
68 113 test-debug-phase: new rev 3: x -> 1
69 114 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: -> draft
70 115
71 116 $ hglog
72 117 3 1 D
73 118 2 1 C
74 119 1 0 B
75 120 0 0 A
76 121
77 122 Test creating changeset as secret
78 123
79 124 $ mkcommit E --config phases.new-commit='secret'
80 125 test-debug-phase: new rev 4: x -> 2
81 126 test-hook-close-phase: a603bfb5a83e312131cebcd05353c217d4d21dde: -> secret
82 127 $ hglog
83 128 4 2 E
84 129 3 1 D
85 130 2 1 C
86 131 1 0 B
87 132 0 0 A
88 133
89 134 Test the secret property is inherited
90 135
91 136 $ mkcommit H
92 137 test-debug-phase: new rev 5: x -> 2
93 138 test-hook-close-phase: a030c6be5127abc010fcbff1851536552e6951a8: -> secret
94 139 $ hglog
95 140 5 2 H
96 141 4 2 E
97 142 3 1 D
98 143 2 1 C
99 144 1 0 B
100 145 0 0 A
101 146
102 147 Even on merge
103 148
104 149 $ hg up -q 1
105 150 $ mkcommit "B'"
106 151 test-debug-phase: new rev 6: x -> 1
107 152 created new head
108 153 test-hook-close-phase: cf9fe039dfd67e829edf6522a45de057b5c86519: -> draft
109 154 $ hglog
110 155 6 1 B'
111 156 5 2 H
112 157 4 2 E
113 158 3 1 D
114 159 2 1 C
115 160 1 0 B
116 161 0 0 A
117 162 $ hg merge 4 # E
118 163 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
119 164 (branch merge, don't forget to commit)
120 165 $ hg phase
121 166 6: draft
122 167 4: secret
123 168 $ hg ci -m "merge B' and E"
124 169 test-debug-phase: new rev 7: x -> 2
125 170 test-hook-close-phase: 17a481b3bccb796c0521ae97903d81c52bfee4af: -> secret
126 171
127 172 $ hglog
128 173 7 2 merge B' and E
129 174 6 1 B'
130 175 5 2 H
131 176 4 2 E
132 177 3 1 D
133 178 2 1 C
134 179 1 0 B
135 180 0 0 A
136 181
137 182 Test secret changeset are not pushed
138 183
139 184 $ hg init ../push-dest
140 185 $ cat > ../push-dest/.hg/hgrc << EOF
141 186 > [phases]
142 187 > publish=False
143 188 > EOF
144 189 $ hg outgoing ../push-dest --template='{rev} {phase} {desc|firstline}\n'
145 190 comparing with ../push-dest
146 191 searching for changes
147 192 0 public A
148 193 1 public B
149 194 2 draft C
150 195 3 draft D
151 196 6 draft B'
152 197 $ hg outgoing -r 'branch(default)' ../push-dest --template='{rev} {phase} {desc|firstline}\n'
153 198 comparing with ../push-dest
154 199 searching for changes
155 200 0 public A
156 201 1 public B
157 202 2 draft C
158 203 3 draft D
159 204 6 draft B'
160 205
161 206 $ hg push ../push-dest -f # force because we push multiple heads
162 207 pushing to ../push-dest
163 208 searching for changes
164 209 adding changesets
165 210 adding manifests
166 211 adding file changes
167 212 added 5 changesets with 5 changes to 5 files (+1 heads)
168 213 test-debug-phase: new rev 0: x -> 0
169 214 test-debug-phase: new rev 1: x -> 0
170 215 test-debug-phase: new rev 2: x -> 1
171 216 test-debug-phase: new rev 3: x -> 1
172 217 test-debug-phase: new rev 4: x -> 1
173 218 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> public
174 219 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: -> public
175 220 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: -> draft
176 221 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: -> draft
177 222 test-hook-close-phase: cf9fe039dfd67e829edf6522a45de057b5c86519: -> draft
178 223 $ hglog
179 224 7 2 merge B' and E
180 225 6 1 B'
181 226 5 2 H
182 227 4 2 E
183 228 3 1 D
184 229 2 1 C
185 230 1 0 B
186 231 0 0 A
187 232 $ cd ../push-dest
188 233 $ hglog
189 234 4 1 B'
190 235 3 1 D
191 236 2 1 C
192 237 1 0 B
193 238 0 0 A
194 239
195 240 (Issue3303)
196 241 Check that remote secret changeset are ignore when checking creation of remote heads
197 242
198 243 We add a secret head into the push destination. This secret head shadows a
199 244 visible shared between the initial repo and the push destination.
200 245
201 246 $ hg up -q 4 # B'
202 247 $ mkcommit Z --config phases.new-commit=secret
203 248 test-debug-phase: new rev 5: x -> 2
204 249 test-hook-close-phase: 2713879da13d6eea1ff22b442a5a87cb31a7ce6a: -> secret
205 250 $ hg phase .
206 251 5: secret
207 252
208 253 We now try to push a new public changeset that descend from the common public
209 254 head shadowed by the remote secret head.
210 255
211 256 $ cd ../initialrepo
212 257 $ hg up -q 6 #B'
213 258 $ mkcommit I
214 259 test-debug-phase: new rev 8: x -> 1
215 260 created new head
216 261 test-hook-close-phase: 6d6770faffce199f1fddd1cf87f6f026138cf061: -> draft
217 262 $ hg push ../push-dest
218 263 pushing to ../push-dest
219 264 searching for changes
220 265 adding changesets
221 266 adding manifests
222 267 adding file changes
223 268 added 1 changesets with 1 changes to 1 files (+1 heads)
224 269 test-debug-phase: new rev 6: x -> 1
225 270 test-hook-close-phase: 6d6770faffce199f1fddd1cf87f6f026138cf061: -> draft
226 271
227 272 :note: The "(+1 heads)" is wrong as we do not had any visible head
228 273
229 274 check that branch cache with "served" filter are properly computed and stored
230 275
231 276 $ ls ../push-dest/.hg/cache/branch2*
232 277 ../push-dest/.hg/cache/branch2-base
233 278 ../push-dest/.hg/cache/branch2-served
234 279 $ cat ../push-dest/.hg/cache/branch2-served
235 280 6d6770faffce199f1fddd1cf87f6f026138cf061 6 465891ffab3c47a3c23792f7dc84156e19a90722
236 281 b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e o default
237 282 6d6770faffce199f1fddd1cf87f6f026138cf061 o default
238 283 $ hg heads -R ../push-dest --template '{rev}:{node} {phase}\n' #update visible cache too
239 284 6:6d6770faffce199f1fddd1cf87f6f026138cf061 draft
240 285 5:2713879da13d6eea1ff22b442a5a87cb31a7ce6a secret
241 286 3:b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e draft
242 287 $ ls ../push-dest/.hg/cache/branch2*
243 288 ../push-dest/.hg/cache/branch2-base
244 289 ../push-dest/.hg/cache/branch2-served
245 290 ../push-dest/.hg/cache/branch2-visible
246 291 $ cat ../push-dest/.hg/cache/branch2-served
247 292 6d6770faffce199f1fddd1cf87f6f026138cf061 6 465891ffab3c47a3c23792f7dc84156e19a90722
248 293 b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e o default
249 294 6d6770faffce199f1fddd1cf87f6f026138cf061 o default
250 295 $ cat ../push-dest/.hg/cache/branch2-visible
251 296 6d6770faffce199f1fddd1cf87f6f026138cf061 6
252 297 b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e o default
253 298 2713879da13d6eea1ff22b442a5a87cb31a7ce6a o default
254 299 6d6770faffce199f1fddd1cf87f6f026138cf061 o default
255 300
256 301
257 302 Restore condition prior extra insertion.
258 303 $ hg -q --config extensions.mq= strip .
259 304 $ hg up -q 7
260 305 $ cd ..
261 306
262 307 Test secret changeset are not pull
263 308
264 309 $ hg init pull-dest
265 310 $ cd pull-dest
266 311 $ hg pull ../initialrepo
267 312 pulling from ../initialrepo
268 313 requesting all changes
269 314 adding changesets
270 315 adding manifests
271 316 adding file changes
272 317 added 5 changesets with 5 changes to 5 files (+1 heads)
273 318 new changesets 4a2df7238c3b:cf9fe039dfd6
274 319 test-debug-phase: new rev 0: x -> 0
275 320 test-debug-phase: new rev 1: x -> 0
276 321 test-debug-phase: new rev 2: x -> 0
277 322 test-debug-phase: new rev 3: x -> 0
278 323 test-debug-phase: new rev 4: x -> 0
279 324 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> public
280 325 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: -> public
281 326 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: -> public
282 327 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: -> public
283 328 test-hook-close-phase: cf9fe039dfd67e829edf6522a45de057b5c86519: -> public
284 329 (run 'hg heads' to see heads, 'hg merge' to merge)
285 330 $ hglog
286 331 4 0 B'
287 332 3 0 D
288 333 2 0 C
289 334 1 0 B
290 335 0 0 A
291 336 $ cd ..
292 337
293 338 But secret can still be bundled explicitly
294 339
295 340 $ cd initialrepo
296 341 $ hg bundle --base '4^' -r 'children(4)' ../secret-bundle.hg
297 342 4 changesets found
298 343 $ cd ..
299 344
300 345 Test secret changeset are not cloned
301 346 (during local clone)
302 347
303 348 $ hg clone -qU initialrepo clone-dest
304 349 test-debug-phase: new rev 0: x -> 0
305 350 test-debug-phase: new rev 1: x -> 0
306 351 test-debug-phase: new rev 2: x -> 0
307 352 test-debug-phase: new rev 3: x -> 0
308 353 test-debug-phase: new rev 4: x -> 0
309 354 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> public
310 355 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: -> public
311 356 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: -> public
312 357 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: -> public
313 358 test-hook-close-phase: cf9fe039dfd67e829edf6522a45de057b5c86519: -> public
314 359 $ hglog -R clone-dest
315 360 4 0 B'
316 361 3 0 D
317 362 2 0 C
318 363 1 0 B
319 364 0 0 A
320 365
321 366 Test summary
322 367
323 368 $ hg summary -R clone-dest --verbose
324 369 parent: -1:000000000000 (no revision checked out)
325 370 branch: default
326 371 commit: (clean)
327 372 update: 5 new changesets (update)
328 373 $ hg summary -R initialrepo
329 374 parent: 7:17a481b3bccb tip
330 375 merge B' and E
331 376 branch: default
332 377 commit: (clean) (secret)
333 378 update: 1 new changesets, 2 branch heads (merge)
334 379 phases: 3 draft, 3 secret
335 380 $ hg summary -R initialrepo --quiet
336 381 parent: 7:17a481b3bccb tip
337 382 update: 1 new changesets, 2 branch heads (merge)
338 383
339 384 Test revset
340 385
341 386 $ cd initialrepo
342 387 $ hglog -r 'public()'
343 388 0 0 A
344 389 1 0 B
345 390 $ hglog -r 'draft()'
346 391 2 1 C
347 392 3 1 D
348 393 6 1 B'
349 394 $ hglog -r 'secret()'
350 395 4 2 E
351 396 5 2 H
352 397 7 2 merge B' and E
353 398
354 399 test that phase are displayed in log at debug level
355 400
356 401 $ hg log --debug
357 402 changeset: 7:17a481b3bccb796c0521ae97903d81c52bfee4af
358 403 tag: tip
359 404 phase: secret
360 405 parent: 6:cf9fe039dfd67e829edf6522a45de057b5c86519
361 406 parent: 4:a603bfb5a83e312131cebcd05353c217d4d21dde
362 407 manifest: 7:5e724ffacba267b2ab726c91fc8b650710deaaa8
363 408 user: test
364 409 date: Thu Jan 01 00:00:00 1970 +0000
365 410 files+: C D E
366 411 extra: branch=default
367 412 description:
368 413 merge B' and E
369 414
370 415
371 416 changeset: 6:cf9fe039dfd67e829edf6522a45de057b5c86519
372 417 phase: draft
373 418 parent: 1:27547f69f25460a52fff66ad004e58da7ad3fb56
374 419 parent: -1:0000000000000000000000000000000000000000
375 420 manifest: 6:ab8bfef2392903058bf4ebb9e7746e8d7026b27a
376 421 user: test
377 422 date: Thu Jan 01 00:00:00 1970 +0000
378 423 files+: B'
379 424 extra: branch=default
380 425 description:
381 426 B'
382 427
383 428
384 429 changeset: 5:a030c6be5127abc010fcbff1851536552e6951a8
385 430 phase: secret
386 431 parent: 4:a603bfb5a83e312131cebcd05353c217d4d21dde
387 432 parent: -1:0000000000000000000000000000000000000000
388 433 manifest: 5:5c710aa854874fe3d5fa7192e77bdb314cc08b5a
389 434 user: test
390 435 date: Thu Jan 01 00:00:00 1970 +0000
391 436 files+: H
392 437 extra: branch=default
393 438 description:
394 439 H
395 440
396 441
397 442 changeset: 4:a603bfb5a83e312131cebcd05353c217d4d21dde
398 443 phase: secret
399 444 parent: 3:b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e
400 445 parent: -1:0000000000000000000000000000000000000000
401 446 manifest: 4:7173fd1c27119750b959e3a0f47ed78abe75d6dc
402 447 user: test
403 448 date: Thu Jan 01 00:00:00 1970 +0000
404 449 files+: E
405 450 extra: branch=default
406 451 description:
407 452 E
408 453
409 454
410 455 changeset: 3:b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e
411 456 phase: draft
412 457 parent: 2:f838bfaca5c7226600ebcfd84f3c3c13a28d3757
413 458 parent: -1:0000000000000000000000000000000000000000
414 459 manifest: 3:6e1f4c47ecb533ffd0c8e52cdc88afb6cd39e20c
415 460 user: test
416 461 date: Thu Jan 01 00:00:00 1970 +0000
417 462 files+: D
418 463 extra: branch=default
419 464 description:
420 465 D
421 466
422 467
423 468 changeset: 2:f838bfaca5c7226600ebcfd84f3c3c13a28d3757
424 469 phase: draft
425 470 parent: 1:27547f69f25460a52fff66ad004e58da7ad3fb56
426 471 parent: -1:0000000000000000000000000000000000000000
427 472 manifest: 2:66a5a01817fdf5239c273802b5b7618d051c89e4
428 473 user: test
429 474 date: Thu Jan 01 00:00:00 1970 +0000
430 475 files+: C
431 476 extra: branch=default
432 477 description:
433 478 C
434 479
435 480
436 481 changeset: 1:27547f69f25460a52fff66ad004e58da7ad3fb56
437 482 phase: public
438 483 parent: 0:4a2df7238c3b48766b5e22fafbb8a2f506ec8256
439 484 parent: -1:0000000000000000000000000000000000000000
440 485 manifest: 1:cb5cbbc1bfbf24cc34b9e8c16914e9caa2d2a7fd
441 486 user: test
442 487 date: Thu Jan 01 00:00:00 1970 +0000
443 488 files+: B
444 489 extra: branch=default
445 490 description:
446 491 B
447 492
448 493
449 494 changeset: 0:4a2df7238c3b48766b5e22fafbb8a2f506ec8256
450 495 phase: public
451 496 parent: -1:0000000000000000000000000000000000000000
452 497 parent: -1:0000000000000000000000000000000000000000
453 498 manifest: 0:007d8c9d88841325f5c6b06371b35b4e8a2b1a83
454 499 user: test
455 500 date: Thu Jan 01 00:00:00 1970 +0000
456 501 files+: A
457 502 extra: branch=default
458 503 description:
459 504 A
460 505
461 506
462 507
463 508
464 509 (Issue3707)
465 510 test invalid phase name
466 511
467 512 $ mkcommit I --config phases.new-commit='babar'
468 513 transaction abort!
469 514 rollback completed
470 515 abort: phases.new-commit: not a valid phase name ('babar')
471 516 [255]
472 517 Test phase command
473 518 ===================
474 519
475 520 initial picture
476 521
477 522 $ hg log -G --template "{rev} {phase} {desc}\n"
478 523 @ 7 secret merge B' and E
479 524 |\
480 525 | o 6 draft B'
481 526 | |
482 527 +---o 5 secret H
483 528 | |
484 529 o | 4 secret E
485 530 | |
486 531 o | 3 draft D
487 532 | |
488 533 o | 2 draft C
489 534 |/
490 535 o 1 public B
491 536 |
492 537 o 0 public A
493 538
494 539
495 540 display changesets phase
496 541
497 542 (mixing -r and plain rev specification)
498 543
499 544 $ hg phase 1::4 -r 7
500 545 1: public
501 546 2: draft
502 547 3: draft
503 548 4: secret
504 549 7: secret
505 550
506 551
507 552 move changeset forward
508 553
509 554 (with -r option)
510 555
511 556 $ hg phase --public -r 2
512 557 test-debug-phase: move rev 2: 1 -> 0
513 558 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: draft -> public
514 559 $ hg log -G --template "{rev} {phase} {desc}\n"
515 560 @ 7 secret merge B' and E
516 561 |\
517 562 | o 6 draft B'
518 563 | |
519 564 +---o 5 secret H
520 565 | |
521 566 o | 4 secret E
522 567 | |
523 568 o | 3 draft D
524 569 | |
525 570 o | 2 public C
526 571 |/
527 572 o 1 public B
528 573 |
529 574 o 0 public A
530 575
531 576
532 577 move changeset backward
533 578
534 579 (without -r option)
535 580
536 581 $ hg phase --draft --force 2
537 582 test-debug-phase: move rev 2: 0 -> 1
538 583 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: public -> draft
539 584 $ hg log -G --template "{rev} {phase} {desc}\n"
540 585 @ 7 secret merge B' and E
541 586 |\
542 587 | o 6 draft B'
543 588 | |
544 589 +---o 5 secret H
545 590 | |
546 591 o | 4 secret E
547 592 | |
548 593 o | 3 draft D
549 594 | |
550 595 o | 2 draft C
551 596 |/
552 597 o 1 public B
553 598 |
554 599 o 0 public A
555 600
556 601
557 602 move changeset forward and backward
558 603
559 604 $ hg phase --draft --force 1::4
560 605 test-debug-phase: move rev 1: 0 -> 1
561 606 test-debug-phase: move rev 4: 2 -> 1
562 607 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: public -> draft
563 608 test-hook-close-phase: a603bfb5a83e312131cebcd05353c217d4d21dde: secret -> draft
564 609 $ hg log -G --template "{rev} {phase} {desc}\n"
565 610 @ 7 secret merge B' and E
566 611 |\
567 612 | o 6 draft B'
568 613 | |
569 614 +---o 5 secret H
570 615 | |
571 616 o | 4 draft E
572 617 | |
573 618 o | 3 draft D
574 619 | |
575 620 o | 2 draft C
576 621 |/
577 622 o 1 draft B
578 623 |
579 624 o 0 public A
580 625
581 626 test partial failure
582 627
583 628 $ hg phase --public 7
584 629 test-debug-phase: move rev 1: 1 -> 0
585 630 test-debug-phase: move rev 2: 1 -> 0
586 631 test-debug-phase: move rev 3: 1 -> 0
587 632 test-debug-phase: move rev 4: 1 -> 0
588 633 test-debug-phase: move rev 6: 1 -> 0
589 634 test-debug-phase: move rev 7: 2 -> 0
590 635 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: draft -> public
591 636 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: draft -> public
592 637 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: draft -> public
593 638 test-hook-close-phase: a603bfb5a83e312131cebcd05353c217d4d21dde: draft -> public
594 639 test-hook-close-phase: cf9fe039dfd67e829edf6522a45de057b5c86519: draft -> public
595 640 test-hook-close-phase: 17a481b3bccb796c0521ae97903d81c52bfee4af: secret -> public
596 641 $ hg phase --draft '5 or 7'
597 642 test-debug-phase: move rev 5: 2 -> 1
598 643 test-hook-close-phase: a030c6be5127abc010fcbff1851536552e6951a8: secret -> draft
599 644 cannot move 1 changesets to a higher phase, use --force
600 645 phase changed for 1 changesets
601 646 [1]
602 647 $ hg log -G --template "{rev} {phase} {desc}\n"
603 648 @ 7 public merge B' and E
604 649 |\
605 650 | o 6 public B'
606 651 | |
607 652 +---o 5 draft H
608 653 | |
609 654 o | 4 public E
610 655 | |
611 656 o | 3 public D
612 657 | |
613 658 o | 2 public C
614 659 |/
615 660 o 1 public B
616 661 |
617 662 o 0 public A
618 663
619 664
620 665 test complete failure
621 666
622 667 $ hg phase --draft 7
623 668 cannot move 1 changesets to a higher phase, use --force
624 669 no phases changed
625 670 [1]
626 671
627 672 $ cd ..
628 673
629 674 test hidden changeset are not cloned as public (issue3935)
630 675
631 676 $ cd initialrepo
632 677
633 678 (enabling evolution)
634 679 $ cat >> $HGRCPATH << EOF
635 680 > [experimental]
636 681 > evolution.createmarkers=True
637 682 > EOF
638 683
639 684 (making a changeset hidden; H in that case)
640 685 $ hg debugobsolete `hg id --debug -r 5`
641 686 1 new obsolescence markers
642 687 obsoleted 1 changesets
643 688
644 689 $ cd ..
645 690 $ hg clone initialrepo clonewithobs
646 691 requesting all changes
647 692 adding changesets
648 693 adding manifests
649 694 adding file changes
650 695 added 7 changesets with 6 changes to 6 files
651 696 new changesets 4a2df7238c3b:17a481b3bccb
652 697 test-debug-phase: new rev 0: x -> 0
653 698 test-debug-phase: new rev 1: x -> 0
654 699 test-debug-phase: new rev 2: x -> 0
655 700 test-debug-phase: new rev 3: x -> 0
656 701 test-debug-phase: new rev 4: x -> 0
657 702 test-debug-phase: new rev 5: x -> 0
658 703 test-debug-phase: new rev 6: x -> 0
659 704 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> public
660 705 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: -> public
661 706 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: -> public
662 707 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: -> public
663 708 test-hook-close-phase: a603bfb5a83e312131cebcd05353c217d4d21dde: -> public
664 709 test-hook-close-phase: cf9fe039dfd67e829edf6522a45de057b5c86519: -> public
665 710 test-hook-close-phase: 17a481b3bccb796c0521ae97903d81c52bfee4af: -> public
666 711 updating to branch default
667 712 6 files updated, 0 files merged, 0 files removed, 0 files unresolved
668 713 $ cd clonewithobs
669 714 $ hg log -G --template "{rev} {phase} {desc}\n"
670 715 @ 6 public merge B' and E
671 716 |\
672 717 | o 5 public B'
673 718 | |
674 719 o | 4 public E
675 720 | |
676 721 o | 3 public D
677 722 | |
678 723 o | 2 public C
679 724 |/
680 725 o 1 public B
681 726 |
682 727 o 0 public A
683 728
684 729
685 730 test verify repo containing hidden changesets, which should not abort just
686 731 because repo.cancopy() is False
687 732
688 733 $ cd ../initialrepo
689 734 $ hg verify
690 735 checking changesets
691 736 checking manifests
692 737 crosschecking files in changesets and manifests
693 738 checking files
694 739 checked 8 changesets with 7 changes to 7 files
695 740
696 741 $ cd ..
697 742
698 743 check whether HG_PENDING makes pending changes only in related
699 744 repositories visible to an external hook.
700 745
701 746 (emulate a transaction running concurrently by copied
702 747 .hg/phaseroots.pending in subsequent test)
703 748
704 749 $ cat > $TESTTMP/savepending.sh <<EOF
705 750 > cp .hg/store/phaseroots.pending .hg/store/phaseroots.pending.saved
706 751 > exit 1 # to avoid changing phase for subsequent tests
707 752 > EOF
708 753 $ cd push-dest
709 754 $ hg phase 6
710 755 6: draft
711 756 $ hg --config hooks.pretxnclose="sh $TESTTMP/savepending.sh" phase -f -s 6
712 757 transaction abort!
713 758 rollback completed
714 759 abort: pretxnclose hook exited with status 1
715 760 [255]
716 761 $ cp .hg/store/phaseroots.pending.saved .hg/store/phaseroots.pending
717 762
718 763 (check (in)visibility of phaseroot while transaction running in repo)
719 764
720 765 $ cat > $TESTTMP/checkpending.sh <<EOF
721 766 > echo '@initialrepo'
722 767 > hg -R "$TESTTMP/initialrepo" phase 7
723 768 > echo '@push-dest'
724 769 > hg -R "$TESTTMP/push-dest" phase 6
725 770 > exit 1 # to avoid changing phase for subsequent tests
726 771 > EOF
727 772 $ cd ../initialrepo
728 773 $ hg phase 7
729 774 7: public
730 775 $ hg --config hooks.pretxnclose="sh $TESTTMP/checkpending.sh" phase -f -s 7
731 776 @initialrepo
732 777 7: secret
733 778 @push-dest
734 779 6: draft
735 780 transaction abort!
736 781 rollback completed
737 782 abort: pretxnclose hook exited with status 1
738 783 [255]
739 784
740 785 Check that pretxnclose-phase hook can control phase movement
741 786
742 787 $ hg phase --force b3325c91a4d9 --secret
743 788 test-debug-phase: move rev 3: 0 -> 2
744 789 test-debug-phase: move rev 4: 0 -> 2
745 790 test-debug-phase: move rev 5: 1 -> 2
746 791 test-debug-phase: move rev 7: 0 -> 2
747 792 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: public -> secret
748 793 test-hook-close-phase: a603bfb5a83e312131cebcd05353c217d4d21dde: public -> secret
749 794 test-hook-close-phase: a030c6be5127abc010fcbff1851536552e6951a8: draft -> secret
750 795 test-hook-close-phase: 17a481b3bccb796c0521ae97903d81c52bfee4af: public -> secret
751 796 $ hg log -G -T phases
752 797 @ changeset: 7:17a481b3bccb
753 798 |\ tag: tip
754 799 | | phase: secret
755 800 | | parent: 6:cf9fe039dfd6
756 801 | | parent: 4:a603bfb5a83e
757 802 | | user: test
758 803 | | date: Thu Jan 01 00:00:00 1970 +0000
759 804 | | summary: merge B' and E
760 805 | |
761 806 | o changeset: 6:cf9fe039dfd6
762 807 | | phase: public
763 808 | | parent: 1:27547f69f254
764 809 | | user: test
765 810 | | date: Thu Jan 01 00:00:00 1970 +0000
766 811 | | summary: B'
767 812 | |
768 813 o | changeset: 4:a603bfb5a83e
769 814 | | phase: secret
770 815 | | user: test
771 816 | | date: Thu Jan 01 00:00:00 1970 +0000
772 817 | | summary: E
773 818 | |
774 819 o | changeset: 3:b3325c91a4d9
775 820 | | phase: secret
776 821 | | user: test
777 822 | | date: Thu Jan 01 00:00:00 1970 +0000
778 823 | | summary: D
779 824 | |
780 825 o | changeset: 2:f838bfaca5c7
781 826 |/ phase: public
782 827 | user: test
783 828 | date: Thu Jan 01 00:00:00 1970 +0000
784 829 | summary: C
785 830 |
786 831 o changeset: 1:27547f69f254
787 832 | phase: public
788 833 | user: test
789 834 | date: Thu Jan 01 00:00:00 1970 +0000
790 835 | summary: B
791 836 |
792 837 o changeset: 0:4a2df7238c3b
793 838 phase: public
794 839 user: test
795 840 date: Thu Jan 01 00:00:00 1970 +0000
796 841 summary: A
797 842
798 843
799 844 Install a hook that prevent b3325c91a4d9 to become public
800 845
801 846 $ cat >> .hg/hgrc << EOF
802 847 > [hooks]
803 848 > pretxnclose-phase.nopublish_D = sh -c "(echo \$HG_NODE| grep -v b3325c91a4d9>/dev/null) || [ 'public' != \$HG_PHASE ]"
804 849 > EOF
805 850
806 851 Try various actions. only the draft move should succeed
807 852
808 853 $ hg phase --public b3325c91a4d9
809 854 transaction abort!
810 855 rollback completed
811 856 abort: pretxnclose-phase.nopublish_D hook exited with status 1
812 857 [255]
813 858 $ hg phase --public a603bfb5a83e
814 859 transaction abort!
815 860 rollback completed
816 861 abort: pretxnclose-phase.nopublish_D hook exited with status 1
817 862 [255]
818 863 $ hg phase --draft 17a481b3bccb
819 864 test-debug-phase: move rev 3: 2 -> 1
820 865 test-debug-phase: move rev 4: 2 -> 1
821 866 test-debug-phase: move rev 7: 2 -> 1
822 867 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: secret -> draft
823 868 test-hook-close-phase: a603bfb5a83e312131cebcd05353c217d4d21dde: secret -> draft
824 869 test-hook-close-phase: 17a481b3bccb796c0521ae97903d81c52bfee4af: secret -> draft
825 870 $ hg phase --public 17a481b3bccb
826 871 transaction abort!
827 872 rollback completed
828 873 abort: pretxnclose-phase.nopublish_D hook exited with status 1
829 874 [255]
830 875
831 876 $ cd ..
832 877
833 878 Test for the "internal" phase
834 879 =============================
835 880
836 881 Check we deny its usage on older repository
837 882
838 883 $ hg init no-internal-phase --config format.internal-phase=no
839 884 $ cd no-internal-phase
840 885 $ cat .hg/requires
841 886 dotencode
842 887 fncache
843 888 generaldelta
844 889 revlogv1
845 890 sparserevlog
846 891 store
847 892 $ echo X > X
848 893 $ hg add X
849 894 $ hg status
850 895 A X
851 896 $ hg --config "phases.new-commit=internal" commit -m "my test internal commit" 2>&1 | grep ProgrammingError
852 897 ** ProgrammingError: this repository does not support the internal phase
853 898 raise error.ProgrammingError(msg)
854 899 mercurial.error.ProgrammingError: this repository does not support the internal phase
855 900 $ hg --config "phases.new-commit=archived" commit -m "my test archived commit" 2>&1 | grep ProgrammingError
856 901 ** ProgrammingError: this repository does not support the archived phase
857 902 raise error.ProgrammingError(msg)
858 903 mercurial.error.ProgrammingError: this repository does not support the archived phase
859 904
860 905 $ cd ..
861 906
862 907 Check it works fine with repository that supports it.
863 908
864 909 $ hg init internal-phase --config format.internal-phase=yes
865 910 $ cd internal-phase
866 911 $ cat .hg/requires
867 912 dotencode
868 913 fncache
869 914 generaldelta
870 915 internal-phase
871 916 revlogv1
872 917 sparserevlog
873 918 store
874 919 $ mkcommit A
875 920 test-debug-phase: new rev 0: x -> 1
876 921 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> draft
877 922
878 923 Commit an internal changesets
879 924
880 925 $ echo B > B
881 926 $ hg add B
882 927 $ hg status
883 928 A B
884 929 $ hg --config "phases.new-commit=internal" commit -m "my test internal commit"
885 930 test-debug-phase: new rev 1: x -> 96
886 931 test-hook-close-phase: c01c42dffc7f81223397e99652a0703f83e1c5ea: -> internal
887 932
888 933 The changeset is a working parent descendant.
889 934 Per the usual visibility rules, it is made visible.
890 935
891 936 $ hg log -G -l 3
892 937 @ changeset: 1:c01c42dffc7f
893 938 | tag: tip
894 939 | user: test
895 940 | date: Thu Jan 01 00:00:00 1970 +0000
896 941 | summary: my test internal commit
897 942 |
898 943 o changeset: 0:4a2df7238c3b
899 944 user: test
900 945 date: Thu Jan 01 00:00:00 1970 +0000
901 946 summary: A
902 947
903 948
904 949 Commit is hidden as expected
905 950
906 951 $ hg up 0
907 952 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
908 953 $ hg log -G
909 954 @ changeset: 0:4a2df7238c3b
910 955 tag: tip
911 956 user: test
912 957 date: Thu Jan 01 00:00:00 1970 +0000
913 958 summary: A
914 959
915 960
916 961 Test for archived phase
917 962 -----------------------
918 963
919 964 Commit an archived changesets
920 965
921 966 $ echo B > B
922 967 $ hg add B
923 968 $ hg status
924 969 A B
925 970 $ hg --config "phases.new-commit=archived" commit -m "my test archived commit"
926 971 test-debug-phase: new rev 2: x -> 32
927 972 test-hook-close-phase: 8df5997c3361518f733d1ae67cd3adb9b0eaf125: -> archived
928 973
929 974 The changeset is a working parent descendant.
930 975 Per the usual visibility rules, it is made visible.
931 976
932 977 $ hg log -G -l 3
933 978 @ changeset: 2:8df5997c3361
934 979 | tag: tip
935 980 | parent: 0:4a2df7238c3b
936 981 | user: test
937 982 | date: Thu Jan 01 00:00:00 1970 +0000
938 983 | summary: my test archived commit
939 984 |
940 985 o changeset: 0:4a2df7238c3b
941 986 user: test
942 987 date: Thu Jan 01 00:00:00 1970 +0000
943 988 summary: A
944 989
945 990
946 991 Commit is hidden as expected
947 992
948 993 $ hg up 0
949 994 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
950 995 $ hg log -G
951 996 @ changeset: 0:4a2df7238c3b
952 997 tag: tip
953 998 user: test
954 999 date: Thu Jan 01 00:00:00 1970 +0000
955 1000 summary: A
956 1001
General Comments 0
You need to be logged in to leave comments. Login now