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.
--- 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: