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

login
register
mail settings
Submitter Boris Feld
Date Nov. 16, 2017, 4:16 p.m.
Message ID <921ee05859ea9fc17f5a.1510848972@FB>
Download mbox | patch
Permalink /patch/25609/
State Accepted
Headers show

Comments

Boris Feld - Nov. 16, 2017, 4:16 p.m.
# HG changeset patch
# User Boris Feld <boris.feld@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

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.
Pulkit Goyal - Nov. 16, 2017, 4:43 p.m.
On Thu, Nov 16, 2017 at 9:46 PM, Boris Feld <boris.feld@octobus.net> wrote:
> # HG changeset patch
> # User Boris Feld <boris.feld@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
>
> 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.

I like the option. Can you please add a releasenotes section?
Gregory Szorc - Nov. 16, 2017, 5:32 p.m.
On Thu, Nov 16, 2017 at 8:16 AM, Boris Feld <boris.feld@octobus.net> wrote:

> # HG changeset patch
> # User Boris Feld <boris.feld@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@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
>

Patch

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)
             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)
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