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

login
register
mail settings
Submitter Boris Feld
Date Nov. 20, 2017, 5:15 p.m.
Message ID <2d706154205c1fbff7ad.1511198122@FB>
Download mbox | patch
Permalink /patch/25667/
State Accepted
Headers show

Comments

Boris Feld - Nov. 20, 2017, 5:15 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 2d706154205c1fbff7ad5baea90697c7848ad61c
# Parent  5312b5738172718b868c1ecfd28ada8d4d7e85de
# EXP-Topic single-heads
# Available At https://bitbucket.org/octobus/mercurial-devel/
#              hg pull https://bitbucket.org/octobus/mercurial-devel/ -r 2d706154205c
server: introduce a 'experimental.single-head-per-branch' 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.
Augie Fackler - Nov. 20, 2017, 9:14 p.m.
On Mon, Nov 20, 2017 at 06:15:22PM +0100, Boris Feld wrote:
> # HG changeset patch
> # User Boris Feld <boris.feld@octobus.net>
> # Date 1510800762 -3600
> #      Thu Nov 16 03:52:42 2017 +0100
> # Node ID 2d706154205c1fbff7ad5baea90697c7848ad61c
> # Parent  5312b5738172718b868c1ecfd28ada8d4d7e85de
> # EXP-Topic single-heads
> # Available At https://bitbucket.org/octobus/mercurial-devel/
> #              hg pull https://bitbucket.org/octobus/mercurial-devel/ -r 2d706154205c
> server: introduce a 'experimental.single-head-per-branch' option

Convince me this should be done like this rather than as an example
hook? We've wanted example hooks in contrib/example-hooks for years,
maybe this would be a good start.
Boris Feld - Nov. 21, 2017, 7:13 p.m.
On Mon, 2017-11-20 at 16:14 -0500, Augie Fackler wrote:
> On Mon, Nov 20, 2017 at 06:15:22PM +0100, Boris Feld wrote:
> > # HG changeset patch
> > # User Boris Feld <boris.feld@octobus.net>
> > # Date 1510800762 -3600
> > #      Thu Nov 16 03:52:42 2017 +0100
> > # Node ID 2d706154205c1fbff7ad5baea90697c7848ad61c
> > # Parent  5312b5738172718b868c1ecfd28ada8d4d7e85de
> > # EXP-Topic single-heads
> > # Available At https://bitbucket.org/octobus/mercurial-devel/
> > #              hg pull https://bitbucket.org/octobus/mercurial-deve
> > l/ -r 2d706154205c
> > server: introduce a 'experimental.single-head-per-branch' option
> 
> Convince me this should be done like this rather than as an example
> hook? We've wanted example hooks in contrib/example-hooks for years,
> maybe this would be a good start.

We discussed the possibility to use a hook with Gregory, but found
several limitations which explains why we went with this approach
instead:
    
    - The performance impact. Having the code in Python meant it has
access to more data in a more efficient way.
    - We don't really want to promote the Python hook API much because
it uses the internal API.
    - No cross-compatibility issues like we could have with shell
scripts on Windows. And not all Mercurial installations on Windows have
access to a Python interpreter.
    - Named branch policy might really fit as standard hooks. However,
we can think of multiple "mode" that would not really fit well as
"hooks (see later in the email for details).

Gregory seemed ready to take this series except for UI considerations,
so we updated the series to put the config in experimental so we could
land it while we bikeshed about how to activate it and what is the best
form it should take.


This series is a start of an experimental travel to tune the behavior
of Mercurial in order to better support specific workflows. This series
starts with enforcing a single head but it's only a part of a bigger
plan. Here are the areas we already thought about that would receive
such tuning options:

named-branches:
   * current behavior
   * enforcing single heads
   * refuse any named branch (but default)

bookmarks:
   * current,
   * forbid any bookmarks,
   * request all heads to have a bookmark,

subrepositories:
   * allow all,
   * allow hg only
   * allow none

phases: (getting into topic territory)
   * publish all,
   * publish draft without topic,
   * refuse draft without topic,
   * publish none,

These settings have non-binary values, we are thinking about something
like:

   [repo-mode]
   named-branch = single-head
   
Or:

  [repo-mode]
  name-branch = disallow

We could try to "force" the value in the hooks section, but it would
feels awkward to us:
    
    [hooks]
    internals.name-branch.single-head = yes
    internals.name-branch.refuse = no

We want to add similar tunings to other areas. Each area will likely
have several modes. From our understanding of internal hooks, they
would be either enabled or disabled. This difference let us 

We think that having a dedicated section to that will be clearer. We
are not sure about the name yet, maybe 'constraints' or 'workflow'.
They should probably come with a requirement to prevent older clients
to break things.

The example above is to start a standalone discussion. We prefer to
have this series gets into core (under the experimental section) in the
meantime.
Augie Fackler - Nov. 30, 2017, 9:15 p.m.
On Tue, Nov 21, 2017 at 08:13:57PM +0100, Boris Feld wrote:
> On Mon, 2017-11-20 at 16:14 -0500, Augie Fackler wrote:
> > On Mon, Nov 20, 2017 at 06:15:22PM +0100, Boris Feld wrote:
> > > # HG changeset patch
> > > # User Boris Feld <boris.feld@octobus.net>
> > > # Date 1510800762 -3600
> > > #      Thu Nov 16 03:52:42 2017 +0100
> > > # Node ID 2d706154205c1fbff7ad5baea90697c7848ad61c
> > > # Parent  5312b5738172718b868c1ecfd28ada8d4d7e85de
> > > # EXP-Topic single-heads
> > > # Available At https://bitbucket.org/octobus/mercurial-devel/
> > > #              hg pull https://bitbucket.org/octobus/mercurial-deve
> > > l/ -r 2d706154205c
> > > server: introduce a 'experimental.single-head-per-branch' option
> >
> > Convince me this should be done like this rather than as an example
> > hook? We've wanted example hooks in contrib/example-hooks for years,
> > maybe this would be a good start.
>
> We discussed the possibility to use a hook with Gregory, but found
> several limitations which explains why we went with this approach
> instead:
>
>     - The performance impact. Having the code in Python meant it has
> access to more data in a more efficient way.
>     - We don't really want to promote the Python hook API much because
> it uses the internal API.
>     - No cross-compatibility issues like we could have with shell
> scripts on Windows. And not all Mercurial installations on Windows have
> access to a Python interpreter.
>     - Named branch policy might really fit as standard hooks. However,
> we can think of multiple "mode" that would not really fit well as
> "hooks (see later in the email for details).
>
> Gregory seemed ready to take this series except for UI considerations,
> so we updated the series to put the config in experimental so we could
> land it while we bikeshed about how to activate it and what is the best
> form it should take.

Works for me. Since it's experimental, we can always drop it.

I'd still _very much_ like to see some sample hooks, and "only one
head in the repo" seems like a great starter hook for a
contrib/example-hooks/... collection. Might be a good way to earn some
brownie points.

Patch

diff --git a/mercurial/configitems.py b/mercurial/configitems.py
--- a/mercurial/configitems.py
+++ b/mercurial/configitems.py
@@ -442,6 +442,9 @@  coreconfigitem('experimental', 'rebase.m
 coreconfigitem('experimental', 'revlogv2',
     default=None,
 )
+coreconfigitem('experimental', 'single-head-per-branch',
+    default=False,
+)
 coreconfigitem('experimental', 'spacemovesdown',
     default=False,
 )
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('experimental', 'single-head-per-branch'):
+                scmutil.enforcesinglehead(repo, tr2, desc)
             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,18 @@  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, desc):
+    """check that no named branch has multiple heads"""
+    if desc in ('strip', 'repair'):
+        # skip the logic during strip
+        return
+    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,203 @@ 
+=====================
+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
+  > [experimental]
+  > single-head-per-branch = 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
+
+Check it does to interfer with strip
+------------------------------------
+
+setup
+
+  $ hg branch branch_A --force
+  marked working directory as branch branch_A
+  $ mkcommit c_aG0
+  created new head
+  $ hg update 'desc("c_dF1")'
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ mkcommit c_dH0
+  $ hg update 'desc("c_aG0")'
+  1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ hg merge
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ mkcommit c_aI0
+  $ hg log -G
+  @    changeset:   10:49003e504178
+  |\   branch:      branch_A
+  | |  tag:         tip
+  | |  parent:      8:a33fb808fb4b
+  | |  parent:      3:840af1c6bc88
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     c_aI0
+  | |
+  | | o  changeset:   9:fe47ea669cea
+  | | |  parent:      7:99a2dc242c5d
+  | | |  user:        test
+  | | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | | |  summary:     c_dH0
+  | | |
+  | o |  changeset:   8:a33fb808fb4b
+  | |/   branch:      branch_A
+  | |    user:        test
+  | |    date:        Thu Jan 01 00:00:00 1970 +0000
+  | |    summary:     c_aG0
+  | |
+  | o  changeset:   7:99a2dc242c5d
+  | |  parent:      5:6ed1df20edb1
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     c_dF1
+  | |
+  | o    changeset:   5:6ed1df20edb1
+  | |\   parent:      4:9bf953aa81f6
+  | | |  parent:      2:286d02a6e2a2
+  | | |  user:        test
+  | | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | | |  summary:     c_dE0
+  | | |
+  | | o  changeset:   4:9bf953aa81f6
+  | | |  parent:      1:134bc3852ad2
+  | | |  user:        test
+  | | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | | |  summary:     c_dD0
+  | | |
+  o | |  changeset:   3:840af1c6bc88
+  | | |  branch:      branch_A
+  | | |  parent:      0:ea207398892e
+  | | |  user:        test
+  | | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | | |  summary:     c_aC0
+  | | |
+  | o |  changeset:   2:286d02a6e2a2
+  | |/   user:        test
+  | |    date:        Thu Jan 01 00:00:00 1970 +0000
+  | |    summary:     c_dB0
+  | |
+  | o  changeset:   1:134bc3852ad2
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     c_dA0
+  |
+  o  changeset:   0:ea207398892e
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     ROOT
+  
+
+actual stripping
+
+  $ hg strip --config extensions.strip= --rev 'desc("c_dH0")'
+  saved backup bundle to $TESTTMP/client/.hg/strip-backup/fe47ea669cea-a41bf5a9-backup.hg (glob)
+