changeset 27266:4dccc37b87bd

ui: support declaring path push urls as sub-options Power users often want to apply per-path configuration options. For example, they may want to declare an alternate URL for push operations or declare a revset of revisions to push when `hg push` is used (as opposed to attempting to push all revisions by default). This patch establishes the use of sub-options (config options with ":" in the name) to declare additional behavior for paths. New sub-options are declared by using the new ``@ui.pathsuboption`` decorator. This decorator serves multiple purposes: * Declaring which sub-options are registered * Declaring how a sub-option maps to an attribute on ``path`` instances (this is needed to `hg paths` can render sub-options and values properly) * Validation and normalization of config options to attribute values * Allows extensions to declare new sub-options without monkeypatching * Allows extensions to overwrite built-in behavior for sub-option handling As convenient as the new option registration decorator is, extensions (and even core functionality) may still need an additional hook point to perform finalization of path instances. For example, they may wish to validate that multiple options/attributes aren't conflicting with each other. This hook point could be added later, if needed. To prove this new functionality works, we implement the "pushurl" path sub-option. This option declares the URL that `hg push` should use by default. We require that "pushurl" is an actual URL. This requirement might be controversial and could be dropped if there is opposition. However, objectors should read the complicated code in ui.path.__init__ and commands.push for resolving non-URL values before making a judgement. We also don't allow #fragment in the URLs. I intend to introduce a ":pushrev" (or similar) option to define a revset to control which revisions are pushed when "-r <rev>" isn't passed into `hg push`. This is much more powerful than #fragment and I don't think #fragment is useful enough to continue supporting. The [paths] section of the "config" help page has been updated significantly. `hg paths` has been taught to display path sub-options. The docs mention that "default-push" is now deprecated. However, there are several references to it that need to be cleaned up. A large part of this is converting more consumers to the new paths API. This will happen naturally as more path sub-options are added and more and more components need to access them.
author Gregory Szorc <gregory.szorc@gmail.com>
date Sat, 05 Dec 2015 21:11:04 -0800
parents 47539ea08bdb
children d6859d86a5d5
files mercurial/commands.py mercurial/help/config.txt mercurial/ui.py tests/test-default-push.t tests/test-help.t tests/test-paths.t
diffstat 6 files changed, 212 insertions(+), 41 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/commands.py	Sun Dec 06 12:31:46 2015 -0800
+++ b/mercurial/commands.py	Sat Dec 05 21:11:04 2015 -0800
@@ -5364,6 +5364,8 @@
             else:
                 ui.write("%s = %s\n" % (name,
                                         util.hidepassword(path.loc)))
+                for subopt, value in sorted(path.suboptions.items()):
+                    ui.write('%s:%s = %s\n' % (name, subopt, value))
 
 @command('phase',
     [('p', 'public', False, _('set changeset phase to public')),
--- a/mercurial/help/config.txt	Sun Dec 06 12:31:46 2015 -0800
+++ b/mercurial/help/config.txt	Sat Dec 05 21:11:04 2015 -0800
@@ -1127,29 +1127,43 @@
 ``paths``
 ---------
 
-Assigns symbolic names to repositories. The left side is the
-symbolic name, and the right gives the directory or URL that is the
-location of the repository. Default paths can be declared by setting
-the following entries.
+Assigns symbolic names and behavior to repositories.
+
+Options are symbolic names defining the URL or directory that is the
+location of the repository. Example::
+
+    [paths]
+    my_server = https://example.com/my_repo
+    local_path = /home/me/repo
+
+These symbolic names can be used from the command line. To pull
+from ``my_server``: :hg:`pull my_server`. To push to ``local_path``:
+:hg:`push local_path`.
+
+Options containing colons (``:``) denote sub-options that can influence
+behavior for that specific path. Example::
+
+    [paths]
+    my_server = https://example.com/my_path
+    my_server:pushurl = ssh://example.com/my_path
+
+The following sub-options can be defined:
+
+``pushurl``
+   The URL to use for push operations. If not defined, the location
+   defined by the path's main entry is used.
+
+The following special named paths exist:
 
 ``default``
-    Directory or URL to use when pulling if no source is specified.
-    (default: repository from which the current repository was cloned)
+   The URL or directory to use when no source or remote is specified.
+
+   :hg:`clone` will automatically define this path to the location the
+   repository was cloned from.
 
 ``default-push``
-    Optional. Directory or URL to use when pushing if no destination
-    is specified.
-
-Custom paths can be defined by assigning the path to a name that later can be
-used from the command line. Example::
-
-    [paths]
-    my_path = http://example.com/path
-
-To push to the path defined in ``my_path`` run the command::
-
-    hg push my_path
-
+   (deprecated) The URL or directory for the default :hg:`push` location.
+   ``default:pushurl`` should be used instead.
 
 ``phases``
 ----------
--- a/mercurial/ui.py	Sun Dec 06 12:31:46 2015 -0800
+++ b/mercurial/ui.py	Sat Dec 05 21:11:04 2015 -0800
@@ -1063,7 +1063,7 @@
     def __init__(self, ui):
         dict.__init__(self)
 
-        for name, loc in ui.configitems('paths'):
+        for name, loc in ui.configitems('paths', ignoresub=True):
             # No location is the same as not existing.
             if not loc:
                 continue
@@ -1071,7 +1071,8 @@
             # TODO ignore default-push once all consumers stop referencing it
             # since it is handled specifically below.
 
-            self[name] = path(ui, name, rawloc=loc)
+            loc, sub = ui.configsuboptions('paths', name)
+            self[name] = path(ui, name, rawloc=loc, suboptions=sub)
 
         # Handle default-push, which is a one-off that defines the push URL for
         # the "default" path.
@@ -1120,10 +1121,48 @@
 
         assert False
 
+_pathsuboptions = {}
+
+def pathsuboption(option, attr):
+    """Decorator used to declare a path sub-option.
+
+    Arguments are the sub-option name and the attribute it should set on
+    ``path`` instances.
+
+    The decorated function will receive as arguments a ``ui`` instance,
+    ``path`` instance, and the string value of this option from the config.
+    The function should return the value that will be set on the ``path``
+    instance.
+
+    This decorator can be used to perform additional verification of
+    sub-options and to change the type of sub-options.
+    """
+    def register(func):
+        _pathsuboptions[option] = (attr, func)
+        return func
+    return register
+
+@pathsuboption('pushurl', 'pushloc')
+def pushurlpathoption(ui, path, value):
+    u = util.url(value)
+    # Actually require a URL.
+    if not u.scheme:
+        ui.warn(_('(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
+        return None
+
+    # Don't support the #foo syntax in the push URL to declare branch to
+    # push.
+    if u.fragment:
+        ui.warn(_('("#fragment" in paths.%s:pushurl not supported; '
+                  'ignoring)\n') % path.name)
+        u.fragment = None
+
+    return str(u)
+
 class path(object):
     """Represents an individual path and its configuration."""
 
-    def __init__(self, ui, name, rawloc=None, pushloc=None):
+    def __init__(self, ui, name, rawloc=None, suboptions=None):
         """Construct a path from its config options.
 
         ``ui`` is the ``ui`` instance the path is coming from.
@@ -1151,7 +1190,6 @@
         self.name = name
         self.rawloc = rawloc
         self.loc = str(u)
-        self.pushloc = pushloc
 
         # When given a raw location but not a symbolic name, validate the
         # location is valid.
@@ -1159,6 +1197,19 @@
             raise ValueError('location is not a URL or path to a local '
                              'repo: %s' % rawloc)
 
+        suboptions = suboptions or {}
+
+        # Now process the sub-options. If a sub-option is registered, its
+        # attribute will always be present. The value will be None if there
+        # was no valid sub-option.
+        for suboption, (attr, func) in _pathsuboptions.iteritems():
+            if suboption not in suboptions:
+                setattr(self, attr, None)
+                continue
+
+            value = func(ui, self, suboptions[suboption])
+            setattr(self, attr, value)
+
     def _isvalidlocalpath(self, path):
         """Returns True if the given path is a potentially valid repository.
         This is its own function so that extensions can change the definition of
@@ -1166,6 +1217,19 @@
         one)."""
         return os.path.isdir(os.path.join(path, '.hg'))
 
+    @property
+    def suboptions(self):
+        """Return sub-options and their values for this path.
+
+        This is intended to be used for presentation purposes.
+        """
+        d = {}
+        for subopt, (attr, _func) in _pathsuboptions.iteritems():
+            value = getattr(self, attr)
+            if value is not None:
+                d[subopt] = value
+        return d
+
 # we instantiate one globally shared progress bar to avoid
 # competing progress bars when multiple UI objects get created
 _progresssingleton = None
--- a/tests/test-default-push.t	Sun Dec 06 12:31:46 2015 -0800
+++ b/tests/test-default-push.t	Sat Dec 05 21:11:04 2015 -0800
@@ -69,3 +69,26 @@
   $ hg --cwd b push doesnotexist
   abort: repository doesnotexist does not exist!
   [255]
+
+:pushurl is used when defined
+
+  $ hg -q clone a pushurlsource
+  $ hg -q clone a pushurldest
+  $ cd pushurlsource
+  $ cat > .hg/hgrc << EOF
+  > [paths]
+  > default = https://example.com/not/relevant
+  > default:pushurl = file://`pwd`/../pushurldest
+  > EOF
+
+  $ touch pushurl
+  $ hg -q commit -A -m 'add pushurl'
+  $ hg push
+  pushing to file:/*/$TESTTMP/pushurlsource/../pushurldest (glob)
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+
+  $ cd ..
--- a/tests/test-help.t	Sun Dec 06 12:31:46 2015 -0800
+++ b/tests/test-help.t	Sat Dec 05 21:11:04 2015 -0800
@@ -1207,28 +1207,43 @@
       "paths"
       -------
   
-      Assigns symbolic names to repositories. The left side is the symbolic
-      name, and the right gives the directory or URL that is the location of the
-      repository. Default paths can be declared by setting the following
-      entries.
+      Assigns symbolic names and behavior to repositories.
+  
+      Options are symbolic names defining the URL or directory that is the
+      location of the repository. Example:
+  
+        [paths]
+        my_server = https://example.com/my_repo
+        local_path = /home/me/repo
+  
+      These symbolic names can be used from the command line. To pull from
+      "my_server": "hg pull my_server". To push to "local_path": "hg push
+      local_path".
+  
+      Options containing colons (":") denote sub-options that can influence
+      behavior for that specific path. Example:
+  
+        [paths]
+        my_server = https://example.com/my_path
+        my_server:pushurl = ssh://example.com/my_path
+  
+      The following sub-options can be defined:
+  
+      "pushurl"
+         The URL to use for push operations. If not defined, the location
+         defined by the path's main entry is used.
+  
+      The following special named paths exist:
   
       "default"
-          Directory or URL to use when pulling if no source is specified.
-          (default: repository from which the current repository was cloned)
+         The URL or directory to use when no source or remote is specified.
+  
+         "hg clone" will automatically define this path to the location the
+         repository was cloned from.
   
       "default-push"
-          Optional. Directory or URL to use when pushing if no destination is
-          specified.
-  
-      Custom paths can be defined by assigning the path to a name that later can
-      be used from the command line. Example:
-  
-        [paths]
-        my_path = http://example.com/path
-  
-      To push to the path defined in "my_path" run the command:
-  
-        hg push my_path
+         (deprecated) The URL or directory for the default "hg push" location.
+         "default:pushurl" should be used instead.
   
   $ hg help glossary.mcguffin
   abort: help section not found
--- a/tests/test-paths.t	Sun Dec 06 12:31:46 2015 -0800
+++ b/tests/test-paths.t	Sat Dec 05 21:11:04 2015 -0800
@@ -44,6 +44,59 @@
   [1]
   $ cd ..
 
+sub-options for an undeclared path are ignored
+
+  $ hg init suboptions
+  $ cd suboptions
+
+  $ cat > .hg/hgrc << EOF
+  > [paths]
+  > path0 = https://example.com/path0
+  > path1:pushurl = https://example.com/path1
+  > EOF
+  $ hg paths
+  path0 = https://example.com/path0
+
+unknown sub-options aren't displayed
+
+  $ cat > .hg/hgrc << EOF
+  > [paths]
+  > path0 = https://example.com/path0
+  > path0:foo = https://example.com/path1
+  > EOF
+
+  $ hg paths
+  path0 = https://example.com/path0
+
+:pushurl must be a URL
+
+  $ cat > .hg/hgrc << EOF
+  > [paths]
+  > default = /path/to/nothing
+  > default:pushurl = /not/a/url
+  > EOF
+
+  $ hg paths
+  (paths.default:pushurl not a URL; ignoring)
+  default = /path/to/nothing
+
+#fragment is not allowed in :pushurl
+
+  $ cat > .hg/hgrc << EOF
+  > [paths]
+  > default = https://example.com/repo
+  > invalid = https://example.com/repo
+  > invalid:pushurl = https://example.com/repo#branch
+  > EOF
+
+  $ hg paths
+  ("#fragment" in paths.invalid:pushurl not supported; ignoring)
+  default = https://example.com/repo
+  invalid = https://example.com/repo
+  invalid:pushurl = https://example.com/repo
+
+  $ cd ..
+
 'file:' disables [paths] entries for clone destination
 
   $ cat >> $HGRCPATH <<EOF