@@ -158,6 +158,7 @@
changegroup,
error,
obsolete,
+ phases,
pushkey,
pycompat,
tags,
@@ -178,6 +179,8 @@
_fpayloadsize = '>i'
_fpartparamcount = '>BB'
+_fphasesentry = '>i20s'
+
preferedchunksize = 4096
_parttypeforbidden = re.compile('[^a-zA-Z0-9_:-]')
@@ -1387,6 +1390,14 @@
obsmarkers = repo.obsstore.relevantmarkers(outgoing.missing)
buildobsmarkerspart(bundler, obsmarkers)
+ if opts.get('phases', False):
+ headsbyphase = phases.subsetphaseheads(repo, outgoing.missing)
+ phasedata = []
+ for phase in phases.allphases:
+ for head in headsbyphase[phase]:
+ phasedata.append(_pack(_fphasesentry, phase, head))
+ bundler.newpart('phase-heads', data=''.join(phasedata))
+
def addparttagsfnodescache(repo, bundler, outgoing):
# we include the tags fnode cache for the bundle changeset
# (as an optional parts)
@@ -1721,6 +1732,29 @@
kwargs[key] = inpart.params[key]
raise error.PushkeyFailed(partid=str(inpart.id), **kwargs)
+def _readphaseheads(inpart):
+ headsbyphase = [[] for i in phases.allphases]
+ entrysize = struct.calcsize(_fphasesentry)
+ while True:
+ entry = inpart.read(entrysize)
+ if len(entry) < entrysize:
+ if entry:
+ raise error.Abort(_('bad phase-heads bundle part'))
+ break
+ phase, node = struct.unpack(_fphasesentry, entry)
+ headsbyphase[phase].append(node)
+ return headsbyphase
+
+@parthandler('phase-heads')
+def handlephases(op, inpart):
+ """apply phases from bundle part to repo"""
+ headsbyphase = _readphaseheads(inpart)
+ addednodes = []
+ for entry in op.records['changegroup']:
+ addednodes.extend(entry['addednodes'])
+ phases.updatephases(op.repo.unfiltered(), op.gettransaction(), headsbyphase,
+ addednodes)
+
@parthandler('reply:pushkey', ('return', 'in-reply-to'))
def handlepushkeyreply(op, inpart):
"""retrieve the result of a pushkey request"""
@@ -1230,6 +1230,8 @@
contentopts = {'cg.version': cgversion}
if repo.ui.configbool('experimental', 'evolution.bundle-obsmarker', False):
contentopts['obsolescence'] = True
+ if repo.ui.configbool('experimental', 'bundle-phases', False):
+ contentopts['phases'] = True
bundle2.writenewbundle(ui, repo, 'bundle', fname, bversion, outgoing,
contentopts, compression=bcompression,
compopts=compopts)
@@ -311,6 +311,15 @@
cmdutil.showmarker(fm, m)
fm.end()
+def _debugphaseheads(ui, data, indent=0):
+ """display version and markers contained in 'data'"""
+ indent_string = ' ' * indent
+ headsbyphase = bundle2._readphaseheads(data)
+ for phase in phases.allphases:
+ for head in headsbyphase[phase]:
+ ui.write(indent_string)
+ ui.write('%s %s\n' % (hex(head), phases.phasenames[phase]))
+
def _debugbundle2(ui, gen, all=None, **opts):
"""lists the contents of a bundle2"""
if not isinstance(gen, bundle2.unbundle20):
@@ -327,6 +336,8 @@
_debugchangegroup(ui, cg, all=all, indent=4, **opts)
if part.type == 'obsmarkers':
_debugobsmarkers(ui, part, indent=4, **opts)
+ if part.type == 'phase-heads':
+ _debugphaseheads(ui, part, indent=4)
@command('debugbundle',
[('a', 'all', None, _('show all details')),
@@ -430,6 +430,32 @@
else:
return False
+def subsetphaseheads(repo, subset):
+ """Finds the phase heads for a subset of a history
+
+ Returns a list indexed by phase number where each item is a list of phase
+ head nodes.
+ """
+ cl = repo.changelog
+
+ headsbyphase = [[] for i in allphases]
+ # No need to keep track of secret phase; any heads in the subset that
+ # are not mentioned are implicitly secret.
+ for phase in allphases[:-1]:
+ revset = "heads(%%ln & %s())" % phasenames[phase]
+ headsbyphase[phase] = [cl.node(r) for r in repo.revs(revset, subset)]
+ return headsbyphase
+
+def updatephases(repo, tr, headsbyphase, addednodes):
+ """Updates the repo with the given phase heads"""
+ # First make all the added revisions secret because changegroup.apply()
+ # currently sets the phase to draft.
+ retractboundary(repo, tr, secret, addednodes)
+
+ # Now advance phase boundaries of all but secret phase
+ for phase in allphases[:-1]:
+ advanceboundary(repo, tr, phase, headsbyphase[phase])
+
def analyzeremotephases(repo, subset, roots):
"""Compute phases heads and root in a subset of node from root dict
new file mode 100644
@@ -0,0 +1,259 @@
+ $ cat >> $HGRCPATH <<EOF
+ > [experimental]
+ > bundle-phases=yes
+ > [extensions]
+ > strip=
+ > drawdag=$TESTDIR/drawdag.py
+ > EOF
+
+Set up repo with linear history
+ $ hg init linear
+ $ cd linear
+ $ hg debugdrawdag <<'EOF'
+ > E
+ > |
+ > D
+ > |
+ > C
+ > |
+ > B
+ > |
+ > A
+ > EOF
+ $ hg phase --public A
+ $ hg phase --force --secret D
+ $ hg log -G -T '{desc} {phase}\n'
+ o E secret
+ |
+ o D secret
+ |
+ o C draft
+ |
+ o B draft
+ |
+ o A public
+
+Phases are restored when unbundling
+ $ hg bundle --base B -r E bundle
+ 3 changesets found
+ $ hg debugbundle bundle
+ Stream params: sortdict([('Compression', 'BZ')])
+ changegroup -- "sortdict([('version', '02'), ('nbchanges', '3')])"
+ 26805aba1e600a82e93661149f2313866a221a7b
+ f585351a92f85104bff7c284233c338b10eb1df7
+ 9bc730a19041f9ec7cb33c626e811aa233efb18c
+ phase-heads -- 'sortdict()'
+ 26805aba1e600a82e93661149f2313866a221a7b draft
+ $ hg strip --no-backup C
+ $ hg unbundle -q bundle
+ $ rm bundle
+ $ hg log -G -T '{desc} {phase}\n'
+ o E secret
+ |
+ o D secret
+ |
+ o C draft
+ |
+ o B draft
+ |
+ o A public
+
+Root revision's phase is preserved
+ $ hg bundle -a bundle
+ 5 changesets found
+ $ hg strip --no-backup A
+ $ hg unbundle -q bundle
+ $ rm bundle
+ $ hg log -G -T '{desc} {phase}\n'
+ o E secret
+ |
+ o D secret
+ |
+ o C draft
+ |
+ o B draft
+ |
+ o A public
+
+Completely public history can be restored
+ $ hg phase --public E
+ $ hg bundle -a bundle
+ 5 changesets found
+ $ hg strip --no-backup A
+ $ hg unbundle -q bundle
+ $ rm bundle
+ $ hg log -G -T '{desc} {phase}\n'
+ o E public
+ |
+ o D public
+ |
+ o C public
+ |
+ o B public
+ |
+ o A public
+
+Direct transition from public to secret can be restored
+ $ hg phase --secret --force D
+ $ hg bundle -a bundle
+ 5 changesets found
+ $ hg strip --no-backup A
+ $ hg unbundle -q bundle
+ $ rm bundle
+ $ hg log -G -T '{desc} {phase}\n'
+ o E secret
+ |
+ o D secret
+ |
+ o C public
+ |
+ o B public
+ |
+ o A public
+
+Revisions within bundle preserve their phase even if parent changes its phase
+ $ hg phase --draft --force B
+ $ hg bundle --base B -r E bundle
+ 3 changesets found
+ $ hg strip --no-backup C
+ $ hg phase --public B
+ $ hg unbundle -q bundle
+ $ rm bundle
+ $ hg log -G -T '{desc} {phase}\n'
+ o E secret
+ |
+ o D secret
+ |
+ o C draft
+ |
+ o B public
+ |
+ o A public
+
+Phase of ancestors of stripped node get advanced to accommodate child
+ $ hg bundle --base B -r E bundle
+ 3 changesets found
+ $ hg strip --no-backup C
+ $ hg phase --force --secret B
+ $ hg unbundle -q bundle
+ $ rm bundle
+ $ hg log -G -T '{desc} {phase}\n'
+ o E secret
+ |
+ o D secret
+ |
+ o C draft
+ |
+ o B draft
+ |
+ o A public
+
+Unbundling advances phases of changesets even if they were already in the repo.
+To test that, create a bundle of everything in draft phase and then unbundle
+to see that secret becomes draft, but public remains public.
+ $ hg phase --draft --force A
+ $ hg phase --draft E
+ $ hg bundle -a bundle
+ 5 changesets found
+ $ hg phase --public A
+ $ hg phase --secret --force E
+ $ hg unbundle -q bundle
+ $ rm bundle
+ $ hg log -G -T '{desc} {phase}\n'
+ o E draft
+ |
+ o D draft
+ |
+ o C draft
+ |
+ o B draft
+ |
+ o A public
+
+ $ cd ..
+
+Set up repo with non-linear history
+ $ hg init non-linear
+ $ cd non-linear
+ $ hg debugdrawdag <<'EOF'
+ > D E
+ > |\|
+ > B C
+ > |/
+ > A
+ > EOF
+ $ hg phase --public C
+ $ hg phase --force --secret B
+ $ hg log -G -T '{node|short} {desc} {phase}\n'
+ o 03ca77807e91 E draft
+ |
+ | o 215e7b0814e1 D secret
+ |/|
+ o | dc0947a82db8 C public
+ | |
+ | o 112478962961 B secret
+ |/
+ o 426bada5c675 A public
+
+
+Restore bundle of entire repo
+ $ hg bundle -a bundle
+ 5 changesets found
+ $ hg debugbundle bundle
+ Stream params: sortdict([('Compression', 'BZ')])
+ changegroup -- "sortdict([('version', '02'), ('nbchanges', '5')])"
+ 426bada5c67598ca65036d57d9e4b64b0c1ce7a0
+ 112478962961147124edd43549aedd1a335e44bf
+ dc0947a82db884575bb76ea10ac97b08536bfa03
+ 215e7b0814e1cac8e2614e7284f2a5dc266b4323
+ 03ca77807e919db8807c3749086dc36fb478cac0
+ phase-heads -- 'sortdict()'
+ dc0947a82db884575bb76ea10ac97b08536bfa03 public
+ 03ca77807e919db8807c3749086dc36fb478cac0 draft
+ $ hg strip --no-backup A
+ $ hg unbundle -q bundle
+ $ rm bundle
+ $ hg log -G -T '{node|short} {desc} {phase}\n'
+ o 03ca77807e91 E draft
+ |
+ | o 215e7b0814e1 D secret
+ |/|
+ o | dc0947a82db8 C public
+ | |
+ | o 112478962961 B secret
+ |/
+ o 426bada5c675 A public
+
+
+ $ hg bundle --base 'A + C' -r D bundle
+ 2 changesets found
+ $ hg debugbundle bundle
+ Stream params: sortdict([('Compression', 'BZ')])
+ changegroup -- "sortdict([('version', '02'), ('nbchanges', '2')])"
+ 112478962961147124edd43549aedd1a335e44bf
+ 215e7b0814e1cac8e2614e7284f2a5dc266b4323
+ phase-heads -- 'sortdict()'
+ $ rm bundle
+
+ $ hg bundle --base A -r D bundle
+ 3 changesets found
+ $ hg debugbundle bundle
+ Stream params: sortdict([('Compression', 'BZ')])
+ changegroup -- "sortdict([('version', '02'), ('nbchanges', '3')])"
+ 112478962961147124edd43549aedd1a335e44bf
+ dc0947a82db884575bb76ea10ac97b08536bfa03
+ 215e7b0814e1cac8e2614e7284f2a5dc266b4323
+ phase-heads -- 'sortdict()'
+ dc0947a82db884575bb76ea10ac97b08536bfa03 public
+ $ rm bundle
+
+ $ hg bundle --base 'B + C' -r 'D + E' bundle
+ 2 changesets found
+ $ hg debugbundle bundle
+ Stream params: sortdict([('Compression', 'BZ')])
+ changegroup -- "sortdict([('version', '02'), ('nbchanges', '2')])"
+ 215e7b0814e1cac8e2614e7284f2a5dc266b4323
+ 03ca77807e919db8807c3749086dc36fb478cac0
+ phase-heads -- 'sortdict()'
+ 03ca77807e919db8807c3749086dc36fb478cac0 draft
+ $ rm bundle