topic: option to hide topic changesets to plain client
authorPierre-Yves David <pierre-yves.david@octobus.net>
Wed, 19 Feb 2020 01:35:23 +0100
changeset 5139 19b8ffd23795
parent 5136 bbf33d5f32ef
child 5140 c705c4069fb1
topic: option to hide topic changesets to plain client This is the first version of an option that make topic changeset hidden to client without the extension. It might become the default in the future.
CHANGELOG
hgext3rd/topic/__init__.py
hgext3rd/topic/server.py
tests/test-extension-isolation.t
tests/test-topic-server.t
tests/test-topic.t
--- a/CHANGELOG	Tue Feb 04 16:22:40 2020 -0800
+++ b/CHANGELOG	Wed Feb 19 01:35:23 2020 +0100
@@ -14,6 +14,7 @@
   * compat: compatibility with some changes of the upcoming Mercurial 5.3
   * evolve: add content divergence checking to the standard pre-rewrite check,
   * evolve: improve the message associated with content divergence.
+  * topic: add a `experimental.topic.server-gate-topic-changesets` config
 
 9.2.2 -- 2020-01-31
 -------------------
--- a/hgext3rd/topic/__init__.py	Tue Feb 04 16:22:40 2020 -0800
+++ b/hgext3rd/topic/__init__.py	Wed Feb 19 01:35:23 2020 +0100
@@ -110,6 +110,19 @@
     [experimental]
     topic.allow-publish = no
 
+Server side visibility
+======================
+
+Serving changesets with topics to clients without topic extension can get
+confusing. Such clients will have multiple anonymous heads without a clear way
+to distinguish them. They will also "lose" the canonical heads of the branch.
+
+To avoid this confusion, server can be configured to only serve changesets with
+topics to clients with the topic extension (version 9.3+). This might become
+the default in future::
+
+    [experimental]
+    topic.server-gate-topic-changesets = yes
 """
 
 from __future__ import absolute_import
@@ -155,6 +168,7 @@
     flow,
     randomname,
     revset as topicrevset,
+    server,
     stack,
     topicmap,
 )
@@ -222,6 +236,9 @@
     configitem(b'experimental', b'topic-mode.server',
                default=configitems.dynamicdefault,
     )
+    configitem(b'experimental', b'topic.server-gate-topic-changesets',
+               default=False,
+    )
 
     def extsetup(ui):
         # register config that strictly belong to other code (thg, core, etc)
@@ -364,6 +381,8 @@
     # Wrap changelog.add to drop empty topic
     extensions.wrapfunction(changelog.changelog, 'add', wrapadd)
 
+    server.setupserver(ui)
+
 def reposetup(ui, repo):
     if not isinstance(repo, localrepo.localrepository):
         return # this can be a peer in the ssh case (puzzling)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/server.py	Wed Feb 19 01:35:23 2020 +0100
@@ -0,0 +1,98 @@
+# topic/server.py - server specific behavior with topic
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+from mercurial import (
+    extensions,
+    repoview,
+    repoviewutil,
+    wireprototypes,
+    wireprotov1peer,
+    wireprotov1server,
+)
+
+from . import (
+    common,
+    constants,
+)
+
+### Visibility restriction
+#
+# Serving draft changesets with topics to clients without topic extension can
+# confuse them, because they won't see the topic label and will consider them
+# normal anonymous heads. Instead we have the option to not serve changesets
+# with topics to clients without topic support.
+#
+# To achieve this, we alter the behavior of the standard `heads` commands and
+# introduce a new `heads` command that only clients with topic will know about.
+
+# compat version of the wireprotocommand decorator, taken from evolve compat
+
+FILTERNAME = b'served-no-topic'
+
+def computeunservedtopic(repo, visibilityexceptions=None):
+    assert not repo.changelog.filteredrevs
+    filteredrevs = repoview.filtertable[b'served'](repo, visibilityexceptions).copy()
+    mutable = repoview.filtertable[b'immutable'](repo, visibilityexceptions)
+    consider = mutable - filteredrevs
+    cl = repo.changelog
+    extrafiltered = set()
+    for r in consider:
+        if cl.changelogrevision(r).extra.get(constants.extrakey, b''):
+            extrafiltered.add(r)
+    if extrafiltered:
+        filteredrevs = frozenset(filteredrevs | extrafiltered)
+    return filteredrevs
+
+def wireprotocommand(name, args=b'', permission=b'pull'):
+    try:
+        from mercurial.wireprotov1server import wireprotocommand
+    except (ImportError, AttributeError):
+        # hg <= 4.6 (b4d85bc122bd)
+        from mercurial.wireproto import wireprotocommand
+    return wireprotocommand(name, args, permission=permission)
+
+def wrapheads(orig, repo, proto):
+    """wrap head to hide topic^W draft changeset to old client"""
+    hidetopics = repo.ui.configbool(b'experimental', b'topic.server-gate-topic-changesets')
+    if common.hastopicext(repo) and hidetopics:
+        h = repo.filtered(FILTERNAME).heads()
+        return wireprototypes.bytesresponse(wireprototypes.encodelist(h) + b'\n')
+    return orig(repo, proto)
+
+def topicheads(repo, proto):
+    """Same as the normal wireprotocol command, but accessing with a different end point."""
+    h = repo.heads()
+    return wireprototypes.bytesresponse(wireprototypes.encodelist(h) + b'\n')
+
+def wireprotocaps(orig, repo, proto):
+    """advertise the new topic specific `head` command for client with topic"""
+    caps = orig(repo, proto)
+    if common.hastopicext(repo) and repo.peer().capable(b'topics'):
+        caps.append(b'_exttopics_heads')
+    return caps
+
+def setupserver(ui):
+    extensions.wrapfunction(wireprotov1server, 'heads', wrapheads)
+    wireprotov1server.commands.pop(b'heads')
+    wireprotocommand(b'heads', permission=b'pull')(wireprotov1server.heads)
+    wireprotocommand(b'_exttopics_heads', permission=b'pull')(topicheads)
+    extensions.wrapfunction(wireprotov1server, '_capabilities', wireprotocaps)
+
+    class topicpeerexecutor(wireprotov1peer.peerexecutor):
+
+        def callcommand(self, command, args):
+            if command == b'heads':
+                if self._peer.capable(b'_exttopics_heads'):
+                    command = b'_exttopics_heads'
+                    if getattr(self._peer, '_exttopics_heads', None) is None:
+                        self._peer._exttopics_heads = self._peer.heads
+            s = super(topicpeerexecutor, self)
+            return s.callcommand(command, args)
+
+    wireprotov1peer.peerexecutor = topicpeerexecutor
+
+    if FILTERNAME not in repoview.filtertable:
+        repoview.filtertable[FILTERNAME] = computeunservedtopic
+        repoviewutil.subsettable[FILTERNAME] = b'immutable'
+        repoviewutil.subsettable[b'served'] = FILTERNAME
--- a/tests/test-extension-isolation.t	Tue Feb 04 16:22:40 2020 -0800
+++ b/tests/test-extension-isolation.t	Wed Feb 19 01:35:23 2020 +0100
@@ -131,6 +131,7 @@
   $ hg debugcapabilities http://$LOCALIP:$HGPORT/repo-no-ext | egrep 'topics|evoext'
   [1]
   $ hg debugcapabilities http://$LOCALIP:$HGPORT/repo-topic | egrep 'topics|evoext'
+    _exttopics_heads
     topics
   $ hg debugcapabilities http://$LOCALIP:$HGPORT/repo-no-ext | egrep 'topics|evoext'
   [1]
@@ -143,11 +144,13 @@
   $ hg debugcapabilities http://$LOCALIP:$HGPORT/repo-both | egrep 'topics|evoext'
     _evoext_getbundle_obscommon
     _evoext_obshashrange_v1
+    _exttopics_heads
     topics
   $ hg debugcapabilities http://$LOCALIP:$HGPORT/repo-evo | egrep 'topics|evoext'
     _evoext_getbundle_obscommon
     _evoext_obshashrange_v1
   $ hg debugcapabilities http://$LOCALIP:$HGPORT/repo-topic | egrep 'topics|evoext'
+    _exttopics_heads
     topics
   $ hg debugcapabilities http://$LOCALIP:$HGPORT/repo-evo | egrep 'topics|evoext'
     _evoext_getbundle_obscommon
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-topic-server.t	Wed Feb 19 01:35:23 2020 +0100
@@ -0,0 +1,179 @@
+  $ . "$TESTDIR/testlib/topic_setup.sh"
+
+  $ cat << EOF >> $HGRCPATH
+  > [experimental]
+  > evolution = all
+  > topic.publish-bare-branch = yes
+  > topic.server-gate-topic-changesets = yes
+  > 
+  > [extensions]
+  > evolve =
+  > 
+  > [phases]
+  > publish = no
+  > 
+  > [ui]
+  > ssh = "$PYTHON" "$RUNTESTDIR/dummyssh"
+  > EOF
+
+  $ hg init server
+  $ cd server
+
+  $ echo a > a
+  $ hg commit -qAm root
+  $ hg phase --public -r 'all()'
+
+  $ cd ..
+
+  $ hg clone ssh://user@dummy/server client-topic1
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  new changesets 6569b5a81c7e
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg clone ssh://user@dummy/server client-topic2
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  new changesets 6569b5a81c7e
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg version -v -R client-topic1
+  warning: --repository ignored
+  Mercurial Distributed SCM (*) (glob)
+  (see https://mercurial-scm.org for more information)
+  
+  Copyright (C) 2005-* Matt Mackall and others (glob)
+  This is free software; see the source for copying conditions. There is NO
+  warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  
+  Enabled extensions:
+  
+    rebase  internal  
+    topic   external  * (glob)
+    evolve  external  * (glob)
+  $ hg clone ssh://user@dummy/server client-plain
+  requesting all changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  new changesets 6569b5a81c7e
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cat << EOF >> client-plain/.hg/hgrc
+  > [extensions]
+  > topic = !
+  > EOF
+  $ hg version -v -R client-plain
+  warning: --repository ignored
+  Mercurial Distributed SCM (*) (glob)
+  (see https://mercurial-scm.org for more information)
+  
+  Copyright (C) 2005-* Matt Mackall and others (glob)
+  This is free software; see the source for copying conditions. There is NO
+  warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+  
+  Enabled extensions:
+  
+    rebase  internal  
+    evolve  external  * (glob)
+
+Make two commits, with and without a topic, and push them to the server
+
+  $ cd client-topic1
+  $ echo b > b
+  $ hg topic some-work
+  marked working directory as topic: some-work
+  $ hg commit -Am 'adding b (topic)'
+  adding b
+  active topic 'some-work' grew its first changeset
+  (see 'hg help topics' for more information)
+  $ hg up 0
+  0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+  $ echo c > c
+  $ hg commit -Am 'adding c (no topic)'
+  adding c
+  $ hg log -r 'all() - 0'
+  changeset:   1:2a2e8b3520f2
+  topic:       some-work
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     adding b (topic)
+  
+  changeset:   2:b46feb4d24f9
+  tag:         tip
+  parent:      0:6569b5a81c7e
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     adding c (no topic)
+  
+
+(disable the bare publishing to put a bare draft on the server)
+
+  $ cat << EOF >> ../server/.hg/hgrc
+  > [experimental]
+  > topic.publish-bare-branch = no
+  > EOF
+  $ hg push
+  pushing to ssh://user@dummy/server
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 2 changesets with 2 changes to 2 files (+1 heads)
+  $ cd ..
+  $ hg --cwd server phase -r 'tip'
+  2: draft
+
+Clients with topic can exchange draft changesets both with and without a topic through the server
+
+  $ hg --cwd client-topic2 pull
+  pulling from ssh://user@dummy/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 2 changesets with 2 changes to 2 files (+1 heads)
+  new changesets 2a2e8b3520f2:b46feb4d24f9 (2 drafts)
+  (run 'hg heads' to see heads)
+  $ hg --cwd client-topic2 log -r 'all() - 0'
+  changeset:   1:2a2e8b3520f2
+  topic:       some-work
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     adding b (topic)
+  
+  changeset:   2:b46feb4d24f9
+  tag:         tip
+  parent:      0:6569b5a81c7e
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     adding c (no topic)
+  
+
+Client without topic only sees draft changesets if they don't have a topic
+
+  $ hg --cwd client-plain pull
+  pulling from ssh://user@dummy/server
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  new changesets b46feb4d24f9 (1 drafts)
+  (run 'hg update' to get a working copy)
+  $ hg --cwd client-plain phase -r 'all() - 0'
+  1: draft
+  $ hg --cwd client-plain log -r 'all() - 0'
+  changeset:   1:b46feb4d24f9
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     adding c (no topic)
+  
--- a/tests/test-topic.t	Tue Feb 04 16:22:40 2020 -0800
+++ b/tests/test-topic.t	Wed Feb 19 01:35:23 2020 +0100
@@ -120,6 +120,20 @@
     [experimental]
     topic.allow-publish = no
   
+  Server side visibility
+  ======================
+  
+  Serving changesets with topics to clients without topic extension can get
+  confusing. Such clients will have multiple anonymous heads without a clear way
+  to distinguish them. They will also "lose" the canonical heads of the branch.
+  
+  To avoid this confusion, server can be configured to only serve changesets
+  with topics to clients with the topic extension (version 9.3+). This might
+  become the default in future:
+  
+    [experimental]
+    topic.server-gate-topic-changesets = yes
+  
   list of commands:
   
   Change organization: