changeset 26759:c0f475ac997e

exchange: support parameters in bundle specification strings Sometimes a basic type string is not sufficient for representing the contents of a bundle. Take bundle2 for example: future bundle2 files may contain parts that today's bundle2 parser can't read. Another example is stream clone data. These require clients to support specific repository formats or they won't be able to read the written files. In both scenarios, we need to describe additional metadata beyond the outer container type. Furthermore, this metadata behaves more like an unordered set, so an order-based declaration format (such as static strings) is not sufficient. We introduce support for "parameters" into the bundle specification string. These are essentially key-value pairs that can be used to encode additional metadata about the bundle. Semicolons are used as the delimiter partially to increase similarity to MIME parameter values (see RFC 2231) and because they are relatively safe from the command line (although values will need quotes to avoid interpretation as multiple shell commands). Alternatives considered were spaces (a bit annoying to encode) and '&' (similar to URL query strings) (which will do bad things in a shell if unquoted). The parsing function now returns a dict of parsed parameters and consumers have been updated accordingly.
author Gregory Szorc <gregory.szorc@gmail.com>
date Wed, 14 Oct 2015 17:00:34 -0700
parents bde7ef23340d
children a18ee7da38c2
files mercurial/commands.py mercurial/exchange.py
diffstat 2 files changed, 35 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/commands.py	Thu Oct 15 13:43:18 2015 -0700
+++ b/mercurial/commands.py	Wed Oct 14 17:00:34 2015 -0700
@@ -1244,7 +1244,7 @@
 
     bundletype = opts.get('type', 'bzip2').lower()
     try:
-        bcompression, cgversion = exchange.parsebundlespec(
+        bcompression, cgversion, params = exchange.parsebundlespec(
                 repo, bundletype, strict=False)
     except error.UnsupportedBundleSpecification as e:
         raise error.Abort(str(e),
--- a/mercurial/exchange.py	Thu Oct 15 13:43:18 2015 -0700
+++ b/mercurial/exchange.py	Wed Oct 14 17:00:34 2015 -0700
@@ -39,10 +39,12 @@
 
     The string currently has the form:
 
-       <compression>-<type>
+       <compression>-<type>[;<parameter0>[;<parameter1>]]
 
     Where <compression> is one of the supported compression formats
-    and <type> is (currently) a version string.
+    and <type> is (currently) a version string. A ";" can follow the type and
+    all text afterwards is interpretted as URI encoded, ";" delimited key=value
+    pairs.
 
     If ``strict`` is True (the default) <compression> is required. Otherwise,
     it is optional.
@@ -50,8 +52,8 @@
     If ``externalnames`` is False (the default), the human-centric names will
     be converted to their internal representation.
 
-    Returns a 2-tuple of (compression, version). Compression will be ``None``
-    if not in strict mode and a compression isn't defined.
+    Returns a 3-tuple of (compression, version, parameters). Compression will
+    be ``None`` if not in strict mode and a compression isn't defined.
 
     An ``InvalidBundleSpecification`` is raised when the specification is
     not syntactically well formed.
@@ -62,6 +64,27 @@
     Note: this function will likely eventually return a more complex data
     structure, including bundle2 part information.
     """
+    def parseparams(s):
+        if ';' not in s:
+            return s, {}
+
+        params = {}
+        version, paramstr = s.split(';', 1)
+
+        for p in paramstr.split(';'):
+            if '=' not in p:
+                raise error.InvalidBundleSpecification(
+                    _('invalid bundle specification: '
+                      'missing "=" in parameter: %s') % p)
+
+            key, value = p.split('=', 1)
+            key = urllib.unquote(key)
+            value = urllib.unquote(value)
+            params[key] = value
+
+        return version, params
+
+
     if strict and '-' not in spec:
         raise error.InvalidBundleSpecification(
                 _('invalid bundle specification; '
@@ -74,6 +97,8 @@
             raise error.UnsupportedBundleSpecification(
                     _('%s compression is not supported') % compression)
 
+        version, params = parseparams(version)
+
         if version not in _bundlespeccgversions:
             raise error.UnsupportedBundleSpecification(
                     _('%s is not a recognized bundle version') % version)
@@ -82,6 +107,8 @@
         # case some defaults are assumed (but only when not in strict mode).
         assert not strict
 
+        spec, params = parseparams(spec)
+
         if spec in _bundlespeccompressions:
             compression = spec
             version = 'v1'
@@ -100,7 +127,7 @@
     if not externalnames:
         compression = _bundlespeccompressions[compression]
         version = _bundlespeccgversions[version]
-    return compression, version
+    return compression, version, params
 
 def readbundle(ui, fh, fname, vfs=None):
     header = changegroup.readexactly(fh, 4)
@@ -1691,8 +1718,8 @@
             # component of the BUNDLESPEC.
             if key == 'BUNDLESPEC':
                 try:
-                    comp, version = parsebundlespec(repo, value,
-                                                    externalnames=True)
+                    comp, version, params = parsebundlespec(repo, value,
+                                                            externalnames=True)
                     attrs['COMPRESSION'] = comp
                     attrs['VERSION'] = version
                 except error.InvalidBundleSpecification: