manifestv2: add support for reading new manifest format
authorMartin von Zweigbergk <martinvonz@google.com>
Fri, 27 Mar 2015 22:26:41 -0700
changeset 24572 b83679eb5f86
parent 24571 919f8ce040be
child 24573 701d3554de0e
manifestv2: add support for reading new manifest format The new manifest format is designed to be smaller, in particular to produce smaller deltas. It stores hashes in binary and puts the hash on a new line (for smaller deltas). It also uses stem compression to save space for long paths. The format has room for metadata, but that's there only for future-proofing. The parser thus accepts any metadata and throws it away. For more information, see http://mercurial.selenic.com/wiki/ManifestV2Plan. The current manifest format doesn't allow an empty filename, so we use an empty filename on the first line to tell a manifest of the new format from the old. Since we still never write manifests in the new format, the added code is unused, but it is tested by test-manifest.py.
mercurial/manifest.py
tests/test-manifest.py
--- a/mercurial/manifest.py	Tue Mar 31 22:45:45 2015 -0700
+++ b/mercurial/manifest.py	Fri Mar 27 22:26:41 2015 -0700
@@ -11,8 +11,7 @@
 
 propertycache = util.propertycache
 
-def _parse(data):
-    """Generates (path, node, flags) tuples from a manifest text"""
+def _parsev1(data):
     # This method does a little bit of excessive-looking
     # precondition checking. This is so that the behavior of this
     # class exactly matches its C counterpart to try and help
@@ -31,6 +30,34 @@
         else:
             yield f, revlog.bin(n), ''
 
+def _parsev2(data):
+    metadataend = data.find('\n')
+    # Just ignore metadata for now
+    pos = metadataend + 1
+    prevf = ''
+    while pos < len(data):
+        end = data.find('\n', pos + 1) # +1 to skip stem length byte
+        if end == -1:
+            raise ValueError('Manifest ended with incomplete file entry.')
+        stemlen = ord(data[pos])
+        items = data[pos + 1:end].split('\0')
+        f = prevf[:stemlen] + items[0]
+        if prevf > f:
+            raise ValueError('Manifest entries not in sorted order.')
+        fl = items[1]
+        # Just ignore metadata (items[2:] for now)
+        n = data[end + 1:end + 21]
+        yield f, n, fl
+        pos = end + 22
+        prevf = f
+
+def _parse(data):
+    """Generates (path, node, flags) tuples from a manifest text"""
+    if data.startswith('\0'):
+        return iter(_parsev2(data))
+    else:
+        return iter(_parsev1(data))
+
 def _text(it):
     """Given an iterator over (path, node, flags) tuples, returns a manifest
     text"""
@@ -116,7 +143,13 @@
 
 class manifestdict(object):
     def __init__(self, data=''):
-        self._lm = _lazymanifest(data)
+        if data.startswith('\0'):
+            #_lazymanifest can not parse v2
+            self._lm = _lazymanifest('')
+            for f, n, fl in _parsev2(data):
+                self._lm[f] = n, fl
+        else:
+            self._lm = _lazymanifest(data)
 
     def __getitem__(self, key):
         return self._lm[key][0]
--- a/tests/test-manifest.py	Tue Mar 31 22:45:45 2015 -0700
+++ b/tests/test-manifest.py	Fri Mar 27 22:26:41 2015 -0700
@@ -8,6 +8,7 @@
 from mercurial import match as matchmod
 
 EMTPY_MANIFEST = ''
+EMTPY_MANIFEST_V2 = '\0\n'
 
 HASH_1 = '1' * 40
 BIN_HASH_1 = binascii.unhexlify(HASH_1)
@@ -24,6 +25,42 @@
          'flag2': 'l',
          }
 
+# Same data as A_SHORT_MANIFEST
+A_SHORT_MANIFEST_V2 = (
+    '\0\n'
+    '\x00bar/baz/qux.py\0%(flag2)s\n%(hash2)s\n'
+    '\x00foo\0%(flag1)s\n%(hash1)s\n'
+    ) % {'hash1': BIN_HASH_1,
+         'flag1': '',
+         'hash2': BIN_HASH_2,
+         'flag2': 'l',
+         }
+
+# Same data as A_SHORT_MANIFEST
+A_METADATA_MANIFEST = (
+    '\0foo\0bar\n'
+    '\x00bar/baz/qux.py\0%(flag2)s\0foo\0bar\n%(hash2)s\n' # flag and metadata
+    '\x00foo\0%(flag1)s\0foo\n%(hash1)s\n' # no flag, but metadata
+    ) % {'hash1': BIN_HASH_1,
+         'flag1': '',
+         'hash2': BIN_HASH_2,
+         'flag2': 'l',
+         }
+
+A_STEM_COMPRESSED_MANIFEST = (
+    '\0\n'
+    '\x00bar/baz/qux.py\0%(flag2)s\n%(hash2)s\n'
+    '\x04qux/foo.py\0%(flag1)s\n%(hash1)s\n' # simple case of 4 stem chars
+    '\x0az.py\0%(flag1)s\n%(hash1)s\n' # tricky newline = 10 stem characters
+    '\x00%(verylongdir)sx/x\0\n%(hash1)s\n'
+    '\xffx/y\0\n%(hash2)s\n' # more than 255 stem chars
+    ) % {'hash1': BIN_HASH_1,
+         'flag1': '',
+         'hash2': BIN_HASH_2,
+         'flag2': 'l',
+         'verylongdir': 255 * 'x',
+         }
+
 A_DEEPER_MANIFEST = (
     'a/b/c/bar.py\0%(hash3)s%(flag1)s\n'
     'a/b/c/bar.txt\0%(hash1)s%(flag1)s\n'
@@ -77,6 +114,11 @@
         self.assertEqual(0, len(m))
         self.assertEqual([], list(m))
 
+    def testEmptyManifestv2(self):
+        m = parsemanifest(EMTPY_MANIFEST_V2)
+        self.assertEqual(0, len(m))
+        self.assertEqual([], list(m))
+
     def testManifest(self):
         m = parsemanifest(A_SHORT_MANIFEST)
         self.assertEqual(['bar/baz/qux.py', 'foo'], list(m))
@@ -86,6 +128,25 @@
         self.assertEqual('', m.flags('foo'))
         self.assertRaises(KeyError, lambda : m['wat'])
 
+    def testParseManifestV2(self):
+        m1 = parsemanifest(A_SHORT_MANIFEST)
+        m2 = parsemanifest(A_SHORT_MANIFEST_V2)
+        # Should have same content as A_SHORT_MANIFEST
+        self.assertEqual(m1.text(), m2.text())
+
+    def testParseManifestMetadata(self):
+        # Metadata is for future-proofing and should be accepted but ignored
+        m = parsemanifest(A_METADATA_MANIFEST)
+        self.assertEqual(A_SHORT_MANIFEST, m.text())
+
+    def testParseManifestStemCompression(self):
+        m = parsemanifest(A_STEM_COMPRESSED_MANIFEST)
+        self.assertIn('bar/baz/qux.py', m)
+        self.assertIn('bar/qux/foo.py', m)
+        self.assertIn('bar/qux/foz.py', m)
+        self.assertIn(256 * 'x' + '/x', m)
+        self.assertIn(256 * 'x' + '/y', m)
+
     def testSetItem(self):
         want = BIN_HASH_1