diff --git a/mercurial/bundle2.py b/mercurial/bundle2.py --- a/mercurial/bundle2.py +++ b/mercurial/bundle2.py @@ -158,6 +158,7 @@ from . import ( changegroup, error, obsolete, + phases, pushkey, pycompat, tags, @@ -178,6 +179,8 @@ urlreq = util.urlreq _fpayloadsize = '>i' _fpartparamcount = '>BB' +_fphasesentry = '>i20s' + preferedchunksize = 4096 _parttypeforbidden = re.compile('[^a-zA-Z0-9_:-]') @@ -1387,6 +1390,14 @@ def _addpartsfromopts(ui, repo, bundler, 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 @@ def handlepushkey(op, inpart): 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""" diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -1230,6 +1230,8 @@ def bundle(ui, repo, fname, dest=None, * 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) diff --git a/mercurial/debugcommands.py b/mercurial/debugcommands.py --- a/mercurial/debugcommands.py +++ b/mercurial/debugcommands.py @@ -311,6 +311,15 @@ def _debugobsmarkers(ui, part, indent=0, 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 @@ def _debugbundle2(ui, gen, all=None, **o _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')), diff --git a/mercurial/phases.py b/mercurial/phases.py --- a/mercurial/phases.py +++ b/mercurial/phases.py @@ -430,6 +430,32 @@ def pushphase(repo, nhex, oldphasestr, n 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 diff --git a/tests/test-bundle-phases.t b/tests/test-bundle-phases.t new file mode 100644 --- /dev/null +++ b/tests/test-bundle-phases.t @@ -0,0 +1,259 @@ + $ cat >> $HGRCPATH < [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