changeset 26640:b13fdcc4e700

exchange: refactor bundle specification parsing The old code was tailored to `hg bundle` usage and not appropriate for use as a general API, which clone bundles will require. The code has been rewritten to make it more generally suitable. We introduce dedicated error types to represent invalid and unsupported bundle specifications. The reason we need dedicated error types (rather than error.Abort) is because clone bundles will want to catch these exception as part of filtering entries. We don't want to swallow error.Abort on principle.
author Gregory Szorc <gregory.szorc@gmail.com>
date Tue, 13 Oct 2015 10:57:54 -0700
parents 92d67e5729b9
children 5c57d01fe64e
files mercurial/commands.py mercurial/error.py mercurial/exchange.py tests/test-bundle-type.t
diffstat 4 files changed, 82 insertions(+), 41 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/commands.py	Tue Oct 13 11:43:21 2015 -0700
+++ b/mercurial/commands.py	Tue Oct 13 10:57:54 2015 -0700
@@ -1242,7 +1242,13 @@
         revs = scmutil.revrange(repo, opts['rev'])
 
     bundletype = opts.get('type', 'bzip2').lower()
-    cgversion, bcompression = exchange.parsebundlespec(repo, bundletype)
+    try:
+        bcompression, cgversion = exchange.parsebundlespec(
+                repo, bundletype, strict=False)
+    except error.UnsupportedBundleSpecification as e:
+        raise error.Abort(str(e),
+                          hint=_('see "hg help bundle" for supported '
+                                 'values for --type'))
 
     if opts.get('all'):
         base = ['null']
--- a/mercurial/error.py	Tue Oct 13 11:43:21 2015 -0700
+++ b/mercurial/error.py	Tue Oct 13 10:57:54 2015 -0700
@@ -201,3 +201,12 @@
     operation which replaces the entire base with new content. This ensures
     the delta may be applied by clones which have not censored the base.
     """
+
+class InvalidBundleSpecification(Exception):
+    """error raised when a bundle specification is invalid.
+
+    This is used for syntax errors as opposed to support errors.
+    """
+
+class UnsupportedBundleSpecification(Exception):
+    """error raised when a bundle specification is not supported."""
--- a/mercurial/exchange.py	Tue Oct 13 11:43:21 2015 -0700
+++ b/mercurial/exchange.py	Tue Oct 13 10:57:54 2015 -0700
@@ -15,58 +15,83 @@
 import tags
 import url as urlmod
 
-_bundlecompspecs = {'none': None,
-                    'bzip2': 'BZ',
-                    'gzip': 'GZ',
-                   }
+# Maps bundle compression human names to internal representation.
+_bundlespeccompressions = {'none': None,
+                           'bzip2': 'BZ',
+                           'gzip': 'GZ',
+                          }
 
-_bundleversionspecs = {'v1': '01',
-                       'v2': '02',
-                       'bundle2': '02', #legacy
-                      }
+# Maps bundle version human names to changegroup versions.
+_bundlespeccgversions = {'v1': '01',
+                         'v2': '02',
+                         'bundle2': '02', #legacy
+                        }
+
+def parsebundlespec(repo, spec, strict=True):
+    """Parse a bundle string specification into parts.
+
+    Bundle specifications denote a well-defined bundle/exchange format.
+    The content of a given specification should not change over time in
+    order to ensure that bundles produced by a newer version of Mercurial are
+    readable from an older version.
+
+    The string currently has the form:
 
-def parsebundlespec(repo, spec):
-    """return the internal bundle type to use from a user input
+       <compression>-<type>
+
+    Where <compression> is one of the supported compression formats
+    and <type> is (currently) a version string.
 
-    This is parsing user specified bundle type as accepted in:
+    If ``strict`` is True (the default) <compression> is required. Otherwise,
+    it is optional.
 
-        'hg bundle --type TYPE'.
+    Returns a 2-tuple of (compression, version). Compression will be ``None``
+    if not in strict mode and a compression isn't defined.
 
-    It accept format in the form [compression][-version]|[version]
+    An ``InvalidBundleSpecification`` is raised when the specification is
+    not syntactically well formed.
+
+    An ``UnsupportedBundleSpecification`` is raised when the compression or
+    bundle type/version is not recognized.
 
-    Consensus about extensions of the format for various bundle2 feature
-    is to prefix any feature with "+". eg "+treemanifest" or "gzip+phases"
+    Note: this function will likely eventually return a more complex data
+    structure, including bundle2 part information.
     """
-    comp, version = None, None
+    if strict and '-' not in spec:
+        raise error.InvalidBundleSpecification(
+                _('invalid bundle specification; '
+                  'must be prefixed with compression: %s') % spec)
 
     if '-' in spec:
-        comp, version = spec.split('-', 1)
-    elif spec in _bundlecompspecs:
-        comp = spec
-    elif spec in _bundleversionspecs:
-        version = spec
-    else:
-        raise error.Abort(_('unknown bundle type specified with --type'))
+        compression, version = spec.split('-', 1)
 
-    if comp is None:
-        comp = 'BZ'
+        if compression not in _bundlespeccompressions:
+            raise error.UnsupportedBundleSpecification(
+                    _('%s compression is not supported') % compression)
+
+        if version not in _bundlespeccgversions:
+            raise error.UnsupportedBundleSpecification(
+                    _('%s is not a recognized bundle version') % version)
     else:
-        try:
-            comp = _bundlecompspecs[comp]
-        except KeyError:
-            raise error.Abort(_('unknown bundle type specified with --type'))
+        # 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
 
-    if version is None:
-        version = '01'
-        if 'generaldelta' in repo.requirements:
-            version = '02'
-    else:
-        try:
-            version = _bundleversionspecs[version]
-        except KeyError:
-            raise error.Abort(_('unknown bundle type specified with --type'))
+        if spec in _bundlespeccompressions:
+            compression = spec
+            version = 'v1'
+            if 'generaldelta' in repo.requirements:
+                version = 'v2'
+        elif spec in _bundlespeccgversions:
+            compression = 'bzip2'
+            version = spec
+        else:
+            raise error.UnsupportedBundleSpecification(
+                    _('%s is not a recognized bundle specification') % spec)
 
-    return version, comp
+    compression = _bundlespeccompressions[compression]
+    version = _bundlespeccgversions[version]
+    return compression, version
 
 def readbundle(ui, fh, fname, vfs=None):
     header = changegroup.readexactly(fh, 4)
--- a/tests/test-bundle-type.t	Tue Oct 13 11:43:21 2015 -0700
+++ b/tests/test-bundle-type.t	Tue Oct 13 10:57:54 2015 -0700
@@ -102,6 +102,7 @@
 
   $ cd t1
   $ hg bundle -a -t garbage ../bgarbage
-  abort: unknown bundle type specified with --type
+  abort: garbage is not a recognized bundle specification
+  (see "hg help bundle" for supported values for --type)
   [255]
   $ cd ..