Patchwork [5,of,8,stream,clone,bundles,V2] exchange: support parameters in bundle specification strings

login
register
mail settings
Submitter Gregory Szorc
Date Oct. 17, 2015, 6:45 p.m.
Message ID <b749836c17b547ff75d0.1445107501@126.1.168.192.in-addr.arpa>
Download mbox | patch
Permalink /patch/11165/
State Accepted
Headers show

Comments

Gregory Szorc - Oct. 17, 2015, 6:45 p.m.
# HG changeset patch
# User Gregory Szorc <gregory.szorc@gmail.com>
# Date 1444867234 25200
#      Wed Oct 14 17:00:34 2015 -0700
# Node ID b749836c17b547ff75d04cba6630d144c4433591
# Parent  c7cc755853aa2d9aa05dba7ac5f8db6d13541b81
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.

Patch

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -1243,9 +1243,9 @@  def bundle(ui, repo, fname, dest=None, *
         revs = scmutil.revrange(repo, opts['rev'])
 
     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),
                           hint=_('see "hg help bundle" for supported '
diff --git a/mercurial/exchange.py b/mercurial/exchange.py
--- a/mercurial/exchange.py
+++ b/mercurial/exchange.py
@@ -38,21 +38,23 @@  def parsebundlespec(repo, spec, strict=T
     readable from an older version.
 
     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.
 
     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.
 
@@ -61,8 +63,29 @@  def parsebundlespec(repo, spec, strict=T
 
     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; '
                   'must be prefixed with compression: %s') % spec)
@@ -73,16 +96,20 @@  def parsebundlespec(repo, spec, strict=T
         if compression not in _bundlespeccompressions:
             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)
     else:
         # Value could be just the compression or just the version, in which
         # 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'
             if 'generaldelta' in repo.requirements:
@@ -99,9 +126,9 @@  def parsebundlespec(repo, spec, strict=T
 
     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)
 
@@ -1690,10 +1717,10 @@  def parseclonebundlesmanifest(repo, s):
             # preferences easier to specify since you can prefer a single
             # 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:
                     pass