[PATCH 2 of 2] server: introduce a 'server.single-head' option

Gregory Szorc gregory.szorc at gmail.com
Thu Nov 16 12:32:55 EST 2017


On Thu, Nov 16, 2017 at 8:16 AM, Boris Feld <boris.feld at octobus.net> wrote:

> # HG changeset patch
> # User Boris Feld <boris.feld at octobus.net>
> # Date 1510800762 -3600
> #      Thu Nov 16 03:52:42 2017 +0100
> # Node ID 921ee05859ea9fc17f5a123baa16c2bbe2c49001
> # Parent  c46c0f855a64868b90260da603769c8bc1ebdcf7
> # EXP-Topic single-heads
> # Available At https://bitbucket.org/octobus/mercurial-devel/
> #              hg pull https://bitbucket.org/octobus/mercurial-devel/ -r
> 921ee05859ea
> server: introduce a 'server.single-head' option
>

I like this feature and feel it should be supported by Mercurial out of the
box. (Mozilla has a hook checking for single heads and we would love to
delete custom code and use a core feature.)

I don't want to scope bloat you and say no to a good patch. However, this
feature is the historical territory of hooks. I value having this feature
in the core distribution. But I feel that a "built-in hooks" mechanism is
the better vehicle for this feature. Only a few weeks ago Augie and I were
discussing the idea of shipping some common, easy-to-enable hooks with
Mercurial. There seems to be some level of agreement that the feature
should exist. What we don't have is a mechanism to easily define them.

I would feel better if the config option naming for this were related to
hooks somehow. If we do that, we can refactor the code to leverage hooks
(or some hooks-like mechanism) later while preserving the user-facing API.
Does that sound reasonable?


>
> When the option is set, the repository will reject any transaction adding
> multiple heads to the same named branch.
>
> For now we reject all scenario with multiple heads. One could imagine
> handling
> closed branches differently. We prefer to keep things simple for now. The
> feature might get extended later. Branch closing is not the best experience
> Mercurial has to offer anyway.
>
> diff --git a/mercurial/configitems.py b/mercurial/configitems.py
> --- a/mercurial/configitems.py
> +++ b/mercurial/configitems.py
> @@ -787,6 +787,9 @@ coreconfigitem('server', 'validate',
>  coreconfigitem('server', 'zliblevel',
>      default=-1,
>  )
> +coreconfigitem('server', 'single-head',
> +    default=False,
> +)
>  coreconfigitem('share', 'pool',
>      default=None,
>  )
> diff --git a/mercurial/help/config.txt b/mercurial/help/config.txt
> --- a/mercurial/help/config.txt
> +++ b/mercurial/help/config.txt
> @@ -1723,6 +1723,10 @@ Alias definitions for revsets. See :hg:`
>
>  Controls generic server settings.
>
> +``single-head``
> +    Force the repository to always contains a single head for each named
> +    branches. Any push/pull commit adding a new heads will be rejected.
> +
>  ``compressionengines``
>      List of compression engines and their relative priority to advertise
>      to clients.
> diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
> --- a/mercurial/localrepo.py
> +++ b/mercurial/localrepo.py
> @@ -1244,6 +1244,8 @@ class localrepository(object):
>              # gating.
>              tracktags(tr2)
>              repo = reporef()
> +            if repo.ui.configbool('server', 'single-head'):
> +                scmutil.enforcesinglehead(repo, tr2)
>

Using "server" in the context of localrepo doesn't feel correct. This code
is running as part of the generic transaction closing code, which means it
applies to *all* repo contexts. I think the config option section should
reflect that.


>              if hook.hashook(repo.ui, 'pretxnclose-bookmark'):
>                  for name, (old, new) in sorted(tr.changes['bookmarks']
> .items()):
>                      args = tr.hookargs.copy()
> diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py
> --- a/mercurial/scmutil.py
> +++ b/mercurial/scmutil.py
> @@ -1277,3 +1277,15 @@ def nodesummaries(repo, nodes, maxnumnod
>          return ' '.join(short(h) for h in nodes)
>      first = ' '.join(short(h) for h in nodes[:maxnumnodes])
>      return _("%s and %s others") % (first, len(nodes) - maxnumnodes)
> +
> +def enforcesinglehead(repo, tr):
> +    """check that no named branch has multiple heads"""
> +    visible = repo.filtered('visible')
> +    # possible improvement: we could restrict the check to affected branch
> +    for name, heads in visible.branchmap().iteritems():
> +        if len(heads) > 1:
> +            msg = _('rejecting multiple heads on branch "%s"')
> +            msg %= name
> +            hint = _('%d heads: %s')
> +            hint %= (len(heads), nodesummaries(repo, heads))
> +            raise error.Abort(msg, hint=hint)
>

What happens when you `hg strip` a repo that already has multiple heads on
a named branch? I /think/ that when the stripped changesets are re-applied
to the repo, this code prevents them from being applied and the resulting
repo is truncated and somewhere there is a backup or temp bundle containing
the changesets that would have been re-applied. That's obviously bad. For
this reason, many of Mozilla's "prevent changes" hooks no-op if the
transaction "source" is "strip."


> diff --git a/tests/test-single-head.t b/tests/test-single-head.t
> new file mode 100644
> --- /dev/null
> +++ b/tests/test-single-head.t
> @@ -0,0 +1,115 @@
> +=====================
> +Test workflow options
> +=====================
> +
> +  $ . "$TESTDIR/testlib/obsmarker-common.sh"
> +
> +Test single head enforcing - Setup
> +=============================================
> +
> +  $ cat << EOF >> $HGRCPATH
> +  > [experimental]
> +  > evolution = all
> +  > EOF
> +  $ hg init single-head-server
> +  $ cd single-head-server
> +  $ cat <<EOF >> .hg/hgrc
> +  > [phases]
> +  > publish = no
> +  > [server]
> +  > single-head = yes
> +  > EOF
> +  $ mkcommit ROOT
> +  $ mkcommit c_dA0
> +  $ cd ..
> +
> +  $ hg clone single-head-server client
> +  updating to branch default
> +  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
> +
> +Test single head enforcing - with branch only
> +---------------------------------------------
> +
> +  $ cd client
> +
> +continuing the current defaultbranch
> +
> +  $ mkcommit c_dB0
> +  $ hg push
> +  pushing to $TESTTMP/single-head-server (glob)
> +  searching for changes
> +  adding changesets
> +  adding manifests
> +  adding file changes
> +  added 1 changesets with 1 changes to 1 files
> +
> +creating a new branch
> +
> +  $ hg up 'desc("ROOT")'
> +  0 files updated, 0 files merged, 2 files removed, 0 files unresolved
> +  $ hg branch branch_A
> +  marked working directory as branch branch_A
> +  (branches are permanent and global, did you want a bookmark?)
> +  $ mkcommit c_aC0
> +  $ hg push --new-branch
> +  pushing to $TESTTMP/single-head-server (glob)
> +  searching for changes
> +  adding changesets
> +  adding manifests
> +  adding file changes
> +  added 1 changesets with 1 changes to 1 files (+1 heads)
> +
> +Create a new head on the default branch
> +
> +  $ hg up 'desc("c_dA0")'
> +  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
> +  $ mkcommit c_dD0
> +  created new head
> +  $ hg push -f
> +  pushing to $TESTTMP/single-head-server (glob)
> +  searching for changes
> +  adding changesets
> +  adding manifests
> +  adding file changes
> +  added 1 changesets with 1 changes to 1 files (+1 heads)
> +  transaction abort!
> +  rollback completed
> +  abort: rejecting multiple heads on branch "default"
> +  (2 heads: 286d02a6e2a2 9bf953aa81f6)
> +  [255]
> +
> +remerge them
> +
> +  $ hg merge
> +  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
> +  (branch merge, don't forget to commit)
> +  $ mkcommit c_dE0
> +  $ hg push
> +  pushing to $TESTTMP/single-head-server (glob)
> +  searching for changes
> +  adding changesets
> +  adding manifests
> +  adding file changes
> +  added 2 changesets with 2 changes to 2 files
> +
> +Test single head enforcing - after rewrite
> +------------------------------------------
> +
> +  $ mkcommit c_dF0
> +  $ hg push
> +  pushing to $TESTTMP/single-head-server (glob)
> +  searching for changes
> +  adding changesets
> +  adding manifests
> +  adding file changes
> +  added 1 changesets with 1 changes to 1 files
> +  $ hg commit --amend -m c_dF1
> +  $ hg push
> +  pushing to $TESTTMP/single-head-server (glob)
> +  searching for changes
> +  adding changesets
> +  adding manifests
> +  adding file changes
> +  added 1 changesets with 0 changes to 1 files (+1 heads)
> +  1 new obsolescence markers
> +  obsoleted 1 changesets
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel at mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://www.mercurial-scm.org/pipermail/mercurial-devel/attachments/20171116/4bd38eb3/attachment.html>


More information about the Mercurial-devel mailing list