# HG changeset patch # User Gregory Szorc # Date 2015-10-15 00:00:34 # Node ID c0f475ac997e634b71ed0bed9ae9652c2c950811 # Parent bde7ef23340d5633c562a22e42ce1d3868cd0f1e 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. diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -1244,7 +1244,7 @@ def bundle(ui, repo, fname, dest=None, * 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), diff --git a/mercurial/exchange.py b/mercurial/exchange.py --- a/mercurial/exchange.py +++ b/mercurial/exchange.py @@ -39,10 +39,12 @@ def parsebundlespec(repo, spec, strict=T The string currently has the form: - - + -[;[;]] Where is one of the supported compression formats - and is (currently) a version string. + and 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) is required. Otherwise, it is optional. @@ -50,8 +52,8 @@ def parsebundlespec(repo, spec, strict=T 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 @@ 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; ' @@ -74,6 +97,8 @@ def parsebundlespec(repo, spec, strict=T 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 @@ def parsebundlespec(repo, spec, strict=T # 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 @@ 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) @@ -1691,8 +1718,8 @@ def parseclonebundlesmanifest(repo, s): # 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: