[PATCH 4 of 5 v4] bundle: add config option to include phases

Pierre-Yves David pierre-yves.david at ens-lyon.org
Mon Jun 26 14:22:06 EDT 2017



On 06/23/2017 10:17 PM, Martin von Zweigbergk via Mercurial-devel wrote:
> # HG changeset patch
> # User Martin von Zweigbergk <martinvonz at google.com>
> # Date 1498151402 25200
> #      Thu Jun 22 10:10:02 2017 -0700
> # Node ID 49d0a354596732f665813b91b7847ee6e7164466
> # Parent  14b44b8d89bc36c9b14289312b7895f40c825d04
> bundle: add config option to include phases
> 
> This adds an experimental.bundle-phases config option to include phase
> information in bundles. As with the recently added support for
> bundling obsmarkers, the support for bundling phases is hidden behind
> the config option until we decide to make a bundlespec v3 that
> includes phases (and obsmarkers and ...).
> 
> We could perhaps use the listkeys format for this, but that's
> considered obsolete according to Pierre-Yves. Instead, we introduce a
> new "phase-heads" bundle part. The new part contains the phase heads
> among the set of bundled revisions. It does not include those in
> secret phase; any head in the bundle that is not mentioned in the
> phase-heads part is assumed to be secret. As a special case, an empty
> phase-heads part thus means that any changesets should be added in
> secret phase. (If we ever add a fourth phase, we'll include secret in
> the part and we'll add a version number.)
> 
> For now, phases are only included by "hg bundle", and not by
> e.g. strip and rebase.
> 
> diff --git a/mercurial/bundle2.py b/mercurial/bundle2.py
> --- a/mercurial/bundle2.py
> +++ b/mercurial/bundle2.py
> @@ -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
> +
> + at 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 @@
>       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 @@
>               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')),
> diff --git a/mercurial/phases.py b/mercurial/phases.py
> --- a/mercurial/phases.py
> +++ b/mercurial/phases.py
> @@ -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)

This tracking of "addednodes" and this retract boundary is still a bit 
bothersome. Such large "unnatural" phase movement will adds extra 
computation, possible caches traumas and will makes it harder to track 
phase movements for hooks. the retract boundary is also unneeded for 
exchange since secret changesets are not exchanged. The "addednodes" set 
is also mostly redundant with tr.changes['revs']
[I can expand this part but wanted to stay compact]

I see a simple alternative using the 'targetphase' parameters of 
changegroup.apply.

The changegroup part can grow a 'targetphase' parameters, that would be 
set if we needs to bundle 'secret' changesets. Then the phase-heads part 
handler just needs to makes (natural) advance-boundary call.

As a nice bonus, we can simplify the information bundled in multiple cases:

* no secret changeset (exchange case):
   → no change to the 'changegroup part'
   → no need to send the draft heads

* publishing repository (current default):
   → no need to send heads for the changegroup content,
     (just need to send heads of common draft remotely)

What do you think ?

> +    # 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 <<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
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel at mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
> 

-- 
Pierre-Yves David


More information about the Mercurial-devel mailing list