diff --git a/mercurial/configitems.py b/mercurial/configitems.py
index bc775b8cc02056881f5f2c23223219b60444a989_bWVyY3VyaWFsL2NvbmZpZ2l0ZW1zLnB5..66ecde8a704d9fa92bd99720923524c4bd3f315b_bWVyY3VyaWFsL2NvbmZpZ2l0ZW1zLnB5 100644
--- a/mercurial/configitems.py
+++ b/mercurial/configitems.py
@@ -535,6 +535,9 @@
 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
index bc775b8cc02056881f5f2c23223219b60444a989_bWVyY3VyaWFsL2xvY2FscmVwby5weQ==..66ecde8a704d9fa92bd99720923524c4bd3f315b_bWVyY3VyaWFsL2xvY2FscmVwby5weQ== 100644
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -1247,6 +1247,8 @@
             # 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
index bc775b8cc02056881f5f2c23223219b60444a989_bWVyY3VyaWFsL3NjbXV0aWwucHk=..66ecde8a704d9fa92bd99720923524c4bd3f315b_bWVyY3VyaWFsL3NjbXV0aWwucHk= 100644
--- a/mercurial/scmutil.py
+++ b/mercurial/scmutil.py
@@ -1286,6 +1286,21 @@
     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)
+
 def wrapconvertsink(sink):
     """Allow extensions to wrap the sink returned by convcmd.convertsink()
     before it is used, whether or not the convert extension was formally loaded.
diff --git a/tests/test-single-head.t b/tests/test-single-head.t
new file mode 100644
index 0000000000000000000000000000000000000000..66ecde8a704d9fa92bd99720923524c4bd3f315b_dGVzdHMvdGVzdC1zaW5nbGUtaGVhZC50
--- /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)
+