# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: john@arbash-meinel.com-20090317203354-77ub807e883l8qx1
# target_branch: http://bzr.arbash-meinel.com/branches/bzr/brisbane\
#   /core
# testament_sha1: cbf89938d891f59d76daec3f0fc79a29aca41644
# timestamp: 2009-03-17 15:35:15 -0500
# source_branch: http://bzr.arbash-meinel.com/branches/bzr/brisbane\
#   /lazy_gc_stream
# base_revision_id: john@arbash-meinel.com-20090317201340-\
#   amjnj1wl78iwcxae
# 
# Begin patch
=== modified file 'bzrlib/groupcompress.py'
--- bzrlib/groupcompress.py	2009-03-16 21:35:44 +0000
+++ bzrlib/groupcompress.py	2009-03-17 20:33:54 +0000
@@ -19,6 +19,7 @@
 from itertools import izip
 from cStringIO import StringIO
 import struct
+import time
 import zlib
 try:
     import pylzma
@@ -34,6 +35,7 @@
     osutils,
     pack,
     patiencediff,
+    trace,
     )
 from bzrlib.graph import Graph
 from bzrlib.knit import _DirectPackAccess
@@ -54,7 +56,7 @@
     )
 
 _USE_LZMA = False and (pylzma is not None)
-_NO_LABELS = False
+_NO_LABELS = True
 _FAST = False
 
 def encode_base128_int(val):
@@ -131,6 +133,14 @@
             self.key, self.type, self.sha1, self.start, self.length
             )
 
+    @property
+    def end(self):
+        return self.start + self.length
+
+# The max zlib window size is 32kB, so if we set 'max_size' output of the
+# decompressor to the requested bytes + 32kB, then we should guarantee
+# num_bytes coming out.
+_ZLIB_DECOMP_WINDOW = 32*1024
 
 class GroupCompressBlock(object):
     """An object which maintains the internal structure of the compressed data.
@@ -145,21 +155,34 @@
     def __init__(self):
         # map by key? or just order in file?
         self._entries = {}
+        self._compressor_name = None
+        self._z_header_length = None
+        self._header_length = None
+        self._z_header = None
+        self._z_content = None
+        self._z_content_decompressor = None
+        self._z_content_length = None
+        self._content_length = None
         self._content = None
-        self._size = 0
+
+    def __len__(self):
+        return self._content_length + self._header_length
 
     def _parse_header(self):
-        """Parse the meta-info from the stream."""
-
-    def __len__(self):
-        return self._size
-
-    def _parse_header_bytes(self, header_bytes):
         """Parse the header part of the block."""
-        if _NO_LABELS:
-            # Don't parse the label structure if we aren't going to use it
+        assert self._z_header is not None
+        if self._z_header == '':
+            # Nothing to process
+            self._z_header = None
             return
-        lines = header_bytes.split('\n')
+        if self._compressor_name == 'lzma':
+            header = pylzma.decompress(self._z_header)
+        else:
+            assert self._compressor_name == 'zlib'
+            header = zlib.decompress(self._z_header)
+        self._z_header = None # We have consumed the header
+        lines = header.split('\n')
+        del header
         info_dict = {}
         for line in lines:
             if not line: #End of record
@@ -177,88 +200,178 @@
                 value = intern(value)
             info_dict[key] = value
 
+    def _ensure_content(self, num_bytes=None):
+        """Make sure that content has been expanded enough.
+
+        :param num_bytes: Ensure that we have extracted at least num_bytes of
+            content. If None, consume everything
+        """
+        # TODO: If we re-use the same content block at different times during
+        #       get_record_stream(), it is possible that the first pass will
+        #       get inserted, triggering an extract/_ensure_content() which
+        #       will get rid of _z_content. And then the next use of the block
+        #       will try to access _z_content (to send it over the wire), and
+        #       fail because it is already extracted. Consider never releasing
+        #       _z_content because of this.
+        if num_bytes is None:
+            num_bytes = self._content_length
+        if self._content_length is not None:
+            assert num_bytes <= self._content_length
+        if self._content is None:
+            assert self._z_content is not None
+            if self._z_content == '':
+                self._content = ''
+            elif self._compressor_name == 'lzma':
+                # We don't do partial lzma decomp yet
+                self._content = pylma.decompress(self._z_content)
+            else:
+                # Start a zlib decompressor
+                assert self._compressor_name == 'zlib'
+                self._z_content_decompressor = zlib.decompressobj()
+                # Seed the decompressor with the uncompressed bytes, so that
+                # the rest of the code is simplified
+                self._content = self._z_content_decompressor.decompress(
+                    self._z_content, _ZLIB_DECOMP_WINDOW)
+                # Any bytes remaining to be decompressed will be in the
+                # decompressors 'unconsumed_tail'
+            self._z_content = None
+        # Do we have enough bytes already?
+        if num_bytes is not None and len(self._content) >= num_bytes:
+            return
+        # If we got this far, and don't have a decompressor, something is wrong
+        assert self._z_content_decompressor is not None
+        remaining_decomp = self._z_content_decompressor.unconsumed_tail
+        if num_bytes is None:
+            if remaining_decomp:
+                # We don't know how much is left, but we'll decompress it all
+                self._content += self._z_content_decompressor.decompress(
+                    remaining_decomp)
+                # Note: There what I consider a bug in zlib.decompressobj
+                #       If you pass back in the entire unconsumed_tail, only
+                #       this time you don't pass a max-size, it doesn't
+                #       change the unconsumed_tail back to None/''.
+                #       However, we know we are done with the whole stream
+                self._z_content_decompressor = None
+            self._content_length = len(self._content)
+        else:
+            # If we have nothing left to decomp, we ran out of decomp bytes
+            assert remaining_decomp
+            needed_bytes = num_bytes - len(self._content)
+            # We always set max_size to 32kB over the minimum needed, so that
+            # zlib will give us as much as we really want.
+            # TODO: If this isn't good enough, we could make a loop here,
+            #       that keeps expanding the request until we get enough
+            self._content += self._z_content_decompressor.decompress(
+                remaining_decomp, needed_bytes + _ZLIB_DECOMP_WINDOW)
+            assert len(self._content) >= num_bytes
+            if not self._z_content_decompressor.unconsumed_tail:
+                # The stream is finished
+                self._z_content_decompressor = None
+
+    def _parse_bytes(self, bytes):
+        """Read the various lengths from the header.
+
+        This also populates the various 'compressed' buffers.
+
+        :return: The position in bytes just after the last newline
+        """
+        # At present, there are 4 lengths to be read, we have 2 integers for
+        # the length of the compressed and uncompressed header, and 2 integers
+        # for the compressed and uncompressed content
+        # 14 bytes can represent > 1TB, so to avoid checking too far, cap the
+        # search to 14 bytes.
+        pos = bytes.index('\n', 6, 20)
+        self._z_header_length = int(bytes[6:pos])
+        pos += 1
+        pos2 = bytes.index('\n', pos, pos + 14)
+        self._header_length = int(bytes[pos:pos2])
+        end_of_z_lengths = pos2
+        pos2 += 1
+        # Older versions don't have the content lengths, if we want to preserve
+        # backwards compatibility, we could try/except over these, and allow
+        # them to be skipped
+        try:
+            pos = bytes.index('\n', pos2, pos2 + 14)
+            self._z_content_length = int(bytes[pos2:pos])
+            pos += 1
+            pos2 = bytes.index('\n', pos, pos + 14)
+            self._content_length = int(bytes[pos:pos2])
+            pos = pos2 + 1
+            assert len(bytes) == (pos + self._z_header_length +
+                                  self._z_content_length)
+            pos2 = pos + self._z_header_length
+            self._z_header = bytes[pos:pos2]
+            self._z_content = bytes[pos2:]
+            assert len(self._z_content) == self._z_content_length
+        except ValueError:
+            # This is the older form, which did not encode its content length
+            pos = end_of_z_lengths + 1
+            pos2 = pos + self._z_header_length
+            self._z_header = bytes[pos:pos2]
+            self._z_content = bytes[pos2:]
+            self._z_content_length = len(self._z_content)
+
     @classmethod
     def from_bytes(cls, bytes):
         out = cls()
         if bytes[:6] not in (cls.GCB_HEADER, cls.GCB_LZ_HEADER):
             raise ValueError('bytes did not start with %r' % (cls.GCB_HEADER,))
         if bytes[4] == 'z':
-            decomp = zlib.decompress
+            out._compressor_name = 'zlib'
         elif bytes[4] == 'l':
-            decomp = pylzma.decompress
+            out._compressor_name = 'lzma'
         else:
             raise ValueError('unknown compressor: %r' % (bytes,))
-        pos = bytes.index('\n', 6)
-        z_header_length = int(bytes[6:pos])
-        pos += 1
-        pos2 = bytes.index('\n', pos)
-        header_length = int(bytes[pos:pos2])
-        if z_header_length == 0:
-            if header_length != 0:
-                raise ValueError('z_header_length 0, but header length != 0')
-            zcontent = bytes[pos2+1:]
-            if zcontent:
-                out._content = decomp(zcontent)
-                out._size = len(out._content)
-            return out
-        pos = pos2 + 1
-        pos2 = pos + z_header_length
-        z_header_bytes = bytes[pos:pos2]
-        if len(z_header_bytes) != z_header_length:
-            raise ValueError('Wrong length of compressed header. %s != %s'
-                             % (len(z_header_bytes), z_header_length))
-        header_bytes = decomp(z_header_bytes)
-        if len(header_bytes) != header_length:
-            raise ValueError('Wrong length of header. %s != %s'
-                             % (len(header_bytes), header_length))
-        del z_header_bytes
-        out._parse_header_bytes(header_bytes)
-        del header_bytes
-        zcontent = bytes[pos2:]
-        if zcontent:
-            out._content = decomp(zcontent)
-            out._size = header_length + len(out._content)
+        out._parse_bytes(bytes)
+        if not _NO_LABELS:
+            out._parse_header()
         return out
 
-    def extract(self, key, index_memo, sha1=None):
+    def extract(self, key, start, end, sha1=None):
         """Extract the text for a specific key.
 
         :param key: The label used for this content
         :param sha1: TODO (should we validate only when sha1 is supplied?)
         :return: The bytes for the content
         """
-        if _NO_LABELS or not self._entries:
-            start, end = index_memo[3:5]
-            # The bytes are 'f' or 'd' for the type, then a variable-length
-            # base128 integer for the content size, then the actual content
-            # We know that the variable-length integer won't be longer than 10
-            # bytes (it only takes 5 bytes to encode 2^32)
-            c = self._content[start]
-            if c == 'f':
-                type = 'fulltext'
-            else:
-                if c != 'd':
-                    raise ValueError('Unknown content control code: %s'
-                                     % (c,))
-                type = 'delta'
-            entry = GroupCompressBlockEntry(key, type, sha1=None,
-                                            start=start, length=end-start)
-        else:
-            entry = self._entries[key]
-            c = self._content[entry.start]
-            if entry.type == 'fulltext':
-                if c != 'f':
-                    raise ValueError('Label claimed fulltext, byte claims: %s'
-                                     % (c,))
-            elif entry.type == 'delta':
-                if c != 'd':
-                    raise ValueError('Label claimed delta, byte claims: %s'
-                                     % (c,))
-            start = entry.start
+        # Make sure we have enough bytes for this record
+        # TODO: if we didn't want to track the end of this entry, we could
+        #       _ensure_content(start+enough_bytes_for_type_and_length), and
+        #       then decode the entry length, and
+        #       _ensure_content(start+1+length)
+        #       It is 2 calls to _ensure_content(), but we always buffer a bit
+        #       extra anyway, and it means 1 less offset stored in the index,
+        #       and transmitted over the wire
+        if end is None:
+            # it takes 5 bytes to encode 2^32, so we need 1 byte to hold the
+            # 'f' or 'd' declaration, and then 5 more for the record length.
+            self._ensure_content(start + 6)
+        else:
+            self._ensure_content(end)
+        # The bytes are 'f' or 'd' for the type, then a variable-length
+        # base128 integer for the content size, then the actual content
+        # We know that the variable-length integer won't be longer than 5
+        # bytes (it takes 5 bytes to encode 2^32)
+        c = self._content[start]
+        if c == 'f':
+            type = 'fulltext'
+        else:
+            if c != 'd':
+                raise ValueError('Unknown content control code: %s'
+                                 % (c,))
+            type = 'delta'
         content_len, len_len = decode_base128_int(
-                            self._content[entry.start + 1:entry.start + 11])
-        content_start = entry.start + 1 + len_len
-        end = entry.start + entry.length
+                            self._content[start + 1:start + 6])
+        content_start = start + 1 + len_len
+        if end is None:
+            end = content_start + content_len
+            self._ensure_content(end)
+        else:
+            if end != content_start + content_len:
+                raise ValueError('end != len according to field header'
+                    ' %s != %s' % (end, content_start + content_len))
+        entry = GroupCompressBlockEntry(key, type, sha1=None,
+                                        start=start, length=end-start)
         content = self._content[content_start:end]
         if c == 'f':
             bytes = content
@@ -284,7 +397,14 @@
         self._entries[key] = entry
         return entry
 
-    def to_bytes(self, content=''):
+    def set_content(self, content):
+        """Set the content of this block."""
+        self._content_length = len(content)
+        self._content = content
+        self._z_content = None
+        self._z_header_length = None
+
+    def to_bytes(self):
         """Encode the information into a byte stream."""
         compress = zlib.compress
         if _USE_LZMA:
@@ -307,9 +427,9 @@
             chunks.append(chunk)
         bytes = ''.join(chunks)
         info_len = len(bytes)
-        z_bytes = []
-        z_bytes.append(compress(bytes))
-        del bytes
+        z_header_bytes = compress(bytes)
+        del bytes, chunks
+        z_header_len = len(z_header_bytes)
         # TODO: we may want to have the header compressed in the same chain
         #       as the data, or we may not, evaulate it
         #       having them compressed together is probably a win for
@@ -317,26 +437,283 @@
         #       label in the header is duplicated in the text.
         #       For chk pages and real bytes, I would guess this is not
         #       true.
-        z_len = sum(map(len, z_bytes))
-        c_len = len(content)
         if _NO_LABELS:
-            z_bytes = []
-            z_len = 0
+            z_header_bytes = ''
+            z_header_len = 0
             info_len = 0
-        z_bytes.append(compress(content))
+        if self._z_content is not None:
+            content_len = self._content_length
+            z_content_len = self._z_content_length
+            z_content_bytes = self._z_content
+        else:
+            assert self._content is not None
+            content_len = self._content_length
+            z_content_bytes = compress(self._content)
+            self._z_content = z_content_bytes
+            z_content_len = len(z_content_bytes)
+            self._z_content_length = z_content_len
         if _USE_LZMA:
             header = self.GCB_LZ_HEADER
         else:
             header = self.GCB_HEADER
         chunks = [header,
-                  '%d\n' % (z_len,),
-                  '%d\n' % (info_len,),
-                  #'%d\n' % (c_len,),
+                  '%d\n%d\n%d\n%d\n' % (z_header_len, info_len,
+                                        z_content_len, content_len)
                  ]
-        chunks.extend(z_bytes)
+        chunks.append(z_header_bytes)
+        chunks.append(z_content_bytes)
         return ''.join(chunks)
 
 
+class _LazyGroupCompressFactory(object):
+    """Yield content from a GroupCompressBlock on demand."""
+
+    def __init__(self, key, parents, manager, start, end, first):
+        """Create a _LazyGroupCompressFactory
+
+        :param key: The key of just this record
+        :param parents: The parents of this key (possibly None)
+        :param gc_block: A GroupCompressBlock object
+        :param start: Offset of the first byte for this record in the
+            uncompressd content
+        :param end: Offset of the byte just after the end of this record
+            (ie, bytes = content[start:end])
+        :param first: Is this the first Factory for the given block?
+        """
+        self.key = key
+        self.parents = parents
+        self.sha1 = None
+        # Note: This attribute coupled with Manager._factories creates a
+        #       reference cycle. Perhaps we would rather use a weakref(), or
+        #       find an appropriate time to release the ref. After the first
+        #       get_bytes_as call? After Manager.get_record_stream() returns
+        #       the object?
+        self._manager = manager
+        self.storage_kind = 'groupcompress-block'
+        if not first:
+            self.storage_kind = 'groupcompress-block-ref'
+        self._first = first
+        self._start = start
+        self._end = end
+
+    def __repr__(self):
+        return '%s(%s, first=%s)' % (self.__class__.__name__,
+            self.key, self._first)
+
+    def get_bytes_as(self, storage_kind):
+        if storage_kind == self.storage_kind:
+            if self._first:
+                # wire bytes, something...
+                return self._manager._wire_bytes()
+            else:
+                return ''
+        if storage_kind in ('fulltext', 'chunked'):
+            block = self._manager._block
+            _, bytes = block.extract(self.key, self._start, self._end)
+            if storage_kind == 'fulltext':
+                return bytes
+            else:
+                return [bytes]
+        raise errors.UnavailableRepresentation(self.key, storage_kind,
+            self.storage_kind)
+
+
+class _LazyGroupContentManager(object):
+    """This manages a group of _LazyGroupCompressFactory objects."""
+
+    def __init__(self, block):
+        self._block = block
+        # We need to preserve the ordering
+        self._factories = []
+
+    def add_factory(self, key, parents, start, end):
+        if not self._factories:
+            first = True
+        else:
+            first = False
+        # Note that this creates a reference cycle....
+        factory = _LazyGroupCompressFactory(key, parents, self,
+            start, end, first=first)
+        self._factories.append(factory)
+
+    def get_record_stream(self):
+        """Get a record for all keys added so far."""
+        for factory in self._factories:
+            yield factory
+        # TODO: Consider setting self._factories = None after the above loop,
+        #       as it will break the reference cycle
+
+    def _trim_block(self, last_byte):
+        """Create a new GroupCompressBlock, with just some of the content."""
+        # None of the factories need to be adjusted, because the content is
+        # located in an identical place. Just that some of the unreferenced
+        # trailing bytes are stripped
+        trace.mutter('stripping trailing bytes from groupcompress block'
+                     ' %d => %d', self._block._content_length, last_byte)
+        new_block = GroupCompressBlock()
+        self._block._ensure_content(last_byte)
+        new_block.set_content(self._block._content[:last_byte])
+        self._block = new_block
+
+    def _rebuild_block(self):
+        """Create a new GroupCompressBlock with only the referenced texts."""
+        compressor = GroupCompressor()
+        tstart = time.time()
+        old_length = self._block._content_length
+        cur_endpoint = 0
+        for factory in self._factories:
+            bytes = factory.get_bytes_as('fulltext')
+            (found_sha1, end_point, type,
+             length) = compressor.compress(factory.key, bytes, factory.sha1)
+            # Now update this factory with the new offsets, etc
+            factory.sha1 = found_sha1
+            factory._start = cur_endpoint
+            factory._end = end_point
+            cur_endpoint = end_point
+        new_block = compressor.flush()
+        # TODO: Should we check that new_block really *is* smaller than the old
+        #       block? It seems hard to come up with a method that it would
+        #       expand, since we do full compression again. Perhaps based on a
+        #       request that ends up poorly ordered?
+        delta = time.time() - tstart
+        self._block = new_block
+        trace.mutter('creating new compressed block on-the-fly in %.3fs'
+                     ' %d bytes => %d bytes', delta, old_length,
+                     self._block._content_length)
+
+    def _check_rebuild_block(self):
+        """Check to see if our block should be repacked."""
+        total_bytes_used = 0
+        last_byte_used = 0
+        for factory in self._factories:
+            total_bytes_used += factory._end - factory._start
+            last_byte_used = max(last_byte_used, factory._end)
+        # If we are using most of the bytes from the block, we have nothing
+        # else to check (currently more that 1/2)
+        if total_bytes_used * 2 >= self._block._content_length:
+            return
+        # Can we just strip off the trailing bytes? If we are going to be
+        # transmitting more than 50% of the front of the content, go ahead
+        if total_bytes_used * 2 > last_byte_used:
+            self._trim_block(last_byte_used)
+            return
+
+        # We are using a small amount of the data, and it isn't just packed
+        # nicely at the front, so rebuild the content.
+        # Note: This would be *nicer* as a strip-data-from-group, rather than
+        #       building it up again from scratch
+        #       It might be reasonable to consider the fulltext sizes for
+        #       different bits when deciding this, too. As you may have a small
+        #       fulltext, and a trivial delta, and you are just trading around
+        #       for another fulltext. If we do a simple 'prune' you may end up
+        #       expanding many deltas into fulltexts, as well.
+        #       If we build a cheap enough 'strip', then we could try a strip,
+        #       if that expands the content, we then rebuild.
+        self._rebuild_block()
+
+    def _wire_bytes(self):
+        """Return a byte stream suitable for transmitting over the wire."""
+        self._check_rebuild_block()
+        # The outer block starts with:
+        #   'groupcompress-block\n'
+        #   <length of compressed key info>\n
+        #   <length of uncompressed info>\n
+        #   <length of gc block>\n
+        #   <header bytes>
+        #   <gc-block>
+        lines = ['groupcompress-block\n']
+        # The minimal info we need is the key, the start offset, and the
+        # parents. The length and type are encoded in the record itself.
+        # However, passing in the other bits makes it easier.  The list of
+        # keys, and the start offset, the length
+        # 1 line key
+        # 1 line with parents, '' for ()
+        # 1 line for start offset
+        # 1 line for end byte
+        header_lines = []
+        for factory in self._factories:
+            key_bytes = '\x00'.join(factory.key)
+            parents = factory.parents
+            if parents is None:
+                parent_bytes = 'None:'
+            else:
+                parent_bytes = '\t'.join('\x00'.join(key) for key in parents)
+            record_header = '%s\n%s\n%d\n%d\n' % (
+                key_bytes, parent_bytes, factory._start, factory._end)
+            header_lines.append(record_header)
+        header_bytes = ''.join(header_lines)
+        del header_lines
+        header_bytes_len = len(header_bytes)
+        z_header_bytes = zlib.compress(header_bytes)
+        del header_bytes
+        z_header_bytes_len = len(z_header_bytes)
+        block_bytes = self._block.to_bytes()
+        lines.append('%d\n%d\n%d\n' % (z_header_bytes_len, header_bytes_len,
+                                       len(block_bytes)))
+        lines.append(z_header_bytes)
+        lines.append(block_bytes)
+        del z_header_bytes, block_bytes
+        return ''.join(lines)
+
+    @classmethod
+    def from_bytes(cls, bytes):
+        # TODO: This does extra string copying, probably better to do it a
+        #       different way
+        (storage_kind, z_header_len, header_len,
+         block_len, rest) = bytes.split('\n', 4)
+        del bytes
+        if storage_kind != 'groupcompress-block':
+            raise ValueError('Unknown storage kind: %s' % (storage_kind,))
+        z_header_len = int(z_header_len)
+        if len(rest) < z_header_len:
+            raise ValueError('Compressed header len shorter than all bytes')
+        z_header = rest[:z_header_len]
+        header_len = int(header_len)
+        header = zlib.decompress(z_header)
+        if len(header) != header_len:
+            raise ValueError('invalid length for decompressed bytes')
+        del z_header
+        block_len = int(block_len)
+        if len(rest) != z_header_len + block_len:
+            raise ValueError('Invalid length for block')
+        block_bytes = rest[z_header_len:]
+        del rest
+        # So now we have a valid GCB, we just need to parse the factories that
+        # were sent to us
+        header_lines = header.split('\n')
+        del header
+        last = header_lines.pop()
+        if last != '':
+            raise ValueError('header lines did not end with a trailing'
+                             ' newline')
+        if len(header_lines) % 4 != 0:
+            raise ValueError('The header was not an even multiple of 4 lines')
+        block = GroupCompressBlock.from_bytes(block_bytes)
+        del block_bytes
+        result = cls(block)
+        for start in xrange(0, len(header_lines), 4):
+            # intern()?
+            key = tuple(header_lines[start].split('\x00'))
+            parents_line = header_lines[start+1]
+            if parents_line == 'None:':
+                parents = None
+            else:
+                parents = tuple([tuple(segment.split('\x00'))
+                                 for segment in parents_line.split('\t')
+                                  if segment])
+            start_offset = int(header_lines[start+2])
+            end_offset = int(header_lines[start+3])
+            result.add_factory(key, parents, start_offset, end_offset)
+        return result
+
+
+def network_block_to_records(storage_kind, bytes, line_end):
+    if storage_kind != 'groupcompress-block':
+        raise ValueError('Unknown storage kind: %s' % (storage_kind,))
+    manager = _LazyGroupContentManager.from_bytes(bytes)
+    return manager.get_record_stream()
+
+
 class GroupCompressor(object):
     """Produce a serialised group of compressed texts.
 
@@ -353,11 +730,8 @@
        left side.
     """
 
-    def __init__(self, delta=True):
-        """Create a GroupCompressor.
-
-        :param delta: If False, do not compress records.
-        """
+    def __init__(self):
+        """Create a GroupCompressor."""
         # Consider seeding the lines with some sort of GC Start flag, or
         # putting it as part of the output stream, rather than in the
         # compressed bytes.
@@ -488,6 +862,13 @@
                              % (entry.sha1, bytes_sha1))
         return bytes, entry.sha1
 
+    def flush(self):
+        """Finish this group, creating a formatted stream."""
+        content = ''.join(self.lines)
+        self.lines = None
+        self._block.set_content(content)
+        return self._block
+
     def output_chunks(self, new_chunks):
         """Output some chunks.
 
@@ -899,26 +1280,54 @@
                 unadded_keys, source_result)
         for key in missing:
             yield AbsentContentFactory(key)
+        manager = None
+        # TODO: This works fairly well at batching up existing groups into a
+        #       streamable format, and possibly allowing for taking one big
+        #       group and splitting it when it isn't fully utilized.
+        #       However, it doesn't allow us to find under-utilized groups and
+        #       combine them into a bigger group on the fly.
+        #       (Consider the issue with how chk_map inserts texts
+        #       one-at-a-time.) This could be done at insert_record_stream()
+        #       time, but it probably would decrease the number of
+        #       bytes-on-the-wire for fetch.
         for source, keys in source_keys:
             if source is self:
                 for key in keys:
                     if key in self._unadded_refs:
+                        if manager is not None:
+                            # Yield everything buffered so far
+                            for factory in manager.get_record_stream():
+                                yield factory
+                            manager = None
                         bytes, sha1 = self._compressor.extract(key)
                         parents = self._unadded_refs[key]
+                        yield FulltextContentFactory(key, parents, sha1, bytes)
                     else:
                         index_memo, _, parents, (method, _) = locations[key]
                         block = self._get_block(index_memo)
-                        entry, bytes = block.extract(key, index_memo)
-                        sha1 = entry.sha1
-                        # TODO: If we don't have labels, then the sha1 here is computed
-                        #       from the data, so we don't want to re-sha the string.
-                        if not _FAST and sha_string(bytes) != sha1:
-                            raise AssertionError('sha1 sum did not match')
-                    yield FulltextContentFactory(key, parents, sha1, bytes)
+                        start, end = index_memo[3:5]
+                        if manager is None:
+                            manager = _LazyGroupContentManager(block)
+                        elif manager._block is not block:
+                            # Flush and create a new manager
+                            for factory in manager.get_record_stream():
+                                yield factory
+                            manager = _LazyGroupContentManager(block)
+                        manager.add_factory(key, parents, start, end)
             else:
+                if manager is not None:
+                    # Yield everything buffered so far
+                    for factory in manager.get_record_stream():
+                        yield factory
+                    manager = None
                 for record in source.get_record_stream(keys, ordering,
                                                        include_delta_closure):
                     yield record
+        if manager is not None:
+            # Yield everything buffered so far
+            for factory in manager.get_record_stream():
+                yield factory
+            manager = None
 
     def get_sha1s(self, keys):
         """See VersionedFiles.get_sha1s()."""
@@ -928,7 +1337,7 @@
                 result[record.key] = record.sha1
             else:
                 if record.storage_kind != 'absent':
-                    result[record.key] == sha_string(record.get_bytes_as(
+                    result[record.key] = sha_string(record.get_bytes_as(
                         'fulltext'))
         return result
 
@@ -942,7 +1351,8 @@
         for _ in self._insert_record_stream(stream):
             pass
 
-    def _insert_record_stream(self, stream, random_id=False, nostore_sha=None):
+    def _insert_record_stream(self, stream, random_id=False, nostore_sha=None,
+                              reuse_blocks=True):
         """Internal core to insert a record stream into this container.
 
         This helper function has a different interface than insert_record_stream
@@ -951,6 +1361,9 @@
         :param stream: A stream of records to insert.
         :param nostore_sha: If the sha1 of a given text matches nostore_sha,
             raise ExistingContent, rather than committing the new text.
+        :param reuse_blocks: If the source is streaming from
+            groupcompress-blocks, just insert the blocks as-is, rather than
+            expanding the texts and inserting again.
         :return: An iterator over the sha1 of the inserted records.
         :seealso insert_record_stream:
         :seealso add_lines:
@@ -966,13 +1379,12 @@
                 return adapter
         # This will go up to fulltexts for gc to gc fetching, which isn't
         # ideal.
-        self._compressor = GroupCompressor(self._delta)
+        self._compressor = GroupCompressor()
         self._unadded_refs = {}
         keys_to_add = []
         basis_end = 0
         def flush():
-            bytes = self._compressor._block.to_bytes(
-                ''.join(self._compressor.lines))
+            bytes = self._compressor.flush().to_bytes()
             index, start, length = self._access.add_raw_records(
                 [(None, len(bytes))], bytes)[0]
             nodes = []
@@ -981,16 +1393,41 @@
             self._index.add_records(nodes, random_id=random_id)
             self._unadded_refs = {}
             del keys_to_add[:]
-            self._compressor = GroupCompressor(self._delta)
+            self._compressor = GroupCompressor()
 
         last_prefix = None
         last_fulltext_len = None
         max_fulltext_len = 0
         max_fulltext_prefix = None
+        insert_manager = None
+        block_start = None
+        block_length = None
         for record in stream:
             # Raise an error when a record is missing.
             if record.storage_kind == 'absent':
                 raise errors.RevisionNotPresent(record.key, self)
+            if reuse_blocks:
+                # If the reuse_blocks flag is set, check to see if we can just
+                # copy a groupcompress block as-is.
+                if record.storage_kind == 'groupcompress-block':
+                    # Insert the raw block into the target repo
+                    insert_manager = record._manager
+                    record._manager._check_rebuild_block()
+                    bytes = record._manager._block.to_bytes()
+                    _, start, length = self._access.add_raw_records(
+                        [(None, len(bytes))], bytes)[0]
+                    del bytes
+                    block_start = start
+                    block_length = length
+                if record.storage_kind in ('groupcompress-block',
+                                           'groupcompress-block-ref'):
+                    assert insert_manager is not None
+                    assert record._manager is insert_manager
+                    value = "%d %d %d %d" % (block_start, block_length,
+                                             record._start, record._end)
+                    nodes = [(record.key, value, (record.parents,))]
+                    self._index.add_records(nodes, random_id=random_id)
+                    continue
             try:
                 bytes = record.get_bytes_as('fulltext')
             except errors.UnavailableRepresentation:

=== modified file 'bzrlib/repofmt/groupcompress_repo.py'
--- bzrlib/repofmt/groupcompress_repo.py	2009-03-16 08:34:58 +0000
+++ bzrlib/repofmt/groupcompress_repo.py	2009-03-17 20:33:54 +0000
@@ -381,7 +381,9 @@
         child_pb = ui.ui_factory.nested_progress_bar()
         try:
             stream = vf_to_stream(source_vf, keys, message, child_pb)
-            target_vf.insert_record_stream(stream)
+            for _ in target_vf._insert_record_stream(stream,
+                                                     reuse_blocks=False):
+                pass
         finally:
             child_pb.finished()
 
@@ -412,7 +414,9 @@
         try:
             for stream in self._get_chk_streams(source_vf, total_keys,
                                                 pb=child_pb):
-                target_vf.insert_record_stream(stream)
+                for _ in target_vf._insert_record_stream(stream,
+                                                         reuse_blocks=False):
+                    pass
         finally:
             child_pb.finished()
 

=== modified file 'bzrlib/repofmt/pack_repo.py'
--- bzrlib/repofmt/pack_repo.py	2009-03-17 20:13:40 +0000
+++ bzrlib/repofmt/pack_repo.py	2009-03-17 20:33:54 +0000
@@ -2467,6 +2467,9 @@
             # This is cheating a bit to use the last grabbed 'inv', but it
             # works
             for name, bytes in items:
+                # TODO: We should use something cheaper than _bytes_to_entry,
+                #       which has to .decode() the entry name, etc.
+                #       We only care about a couple of the fields in the bytes.
                 entry = inv._bytes_to_entry(bytes)
                 if entry.name == '' and not rich_root:
                     continue

=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py	2009-03-17 20:13:40 +0000
+++ bzrlib/tests/__init__.py	2009-03-17 20:33:54 +0000
@@ -3300,9 +3300,10 @@
         osutils.rmtree(dirname)
     except OSError, e:
         if sys.platform == 'win32' and e.errno == errno.EACCES:
-            sys.stderr.write(('Permission denied: '
-                                 'unable to remove testing dir '
-                                 '%s\n' % os.path.basename(dirname)))
+            sys.stderr.write('Permission denied: '
+                             'unable to remove testing dir '
+                             '%s\n%s'
+                             % (os.path.basename(dirname), e))
         else:
             raise
 

=== modified file 'bzrlib/tests/test_groupcompress.py'
--- bzrlib/tests/test_groupcompress.py	2009-03-11 06:50:59 +0000
+++ bzrlib/tests/test_groupcompress.py	2009-03-17 19:38:14 +0000
@@ -20,7 +20,10 @@
 
 from bzrlib import (
     groupcompress,
+    errors,
+    osutils,
     tests,
+    versionedfile,
     )
 from bzrlib.osutils import sha_string
 from bzrlib.tests import (
@@ -29,18 +32,16 @@
     )
 
 
-
-
 class TestGroupCompressor(tests.TestCase):
     """Tests for GroupCompressor"""
 
     def test_empty_delta(self):
-        compressor = groupcompress.GroupCompressor(True)
+        compressor = groupcompress.GroupCompressor()
         self.assertEqual([], compressor.lines)
 
     def test_one_nosha_delta(self):
         # diff against NUKK
-        compressor = groupcompress.GroupCompressor(True)
+        compressor = groupcompress.GroupCompressor()
         sha1, end_point, _, _ = compressor.compress(('label',),
             'strange\ncommon\n', None)
         self.assertEqual(sha_string('strange\ncommon\n'), sha1)
@@ -66,7 +67,7 @@
                              self._chunks_to_repr_lines(actual))
 
     def test_two_nosha_delta(self):
-        compressor = groupcompress.GroupCompressor(True)
+        compressor = groupcompress.GroupCompressor()
         sha1_1, _, _, _ = compressor.compress(('label',),
             'strange\ncommon long line\nthat needs a 16 byte match\n', None)
         expected_lines = list(compressor.lines)
@@ -91,7 +92,7 @@
     def test_three_nosha_delta(self):
         # The first interesting test: make a change that should use lines from
         # both parents.
-        compressor = groupcompress.GroupCompressor(True)
+        compressor = groupcompress.GroupCompressor()
         sha1_1, end_point, _, _ = compressor.compress(('label',),
             'strange\ncommon very very long line\nwith some extra text\n', None)
         sha1_2, _, _, _ = compressor.compress(('newlabel',),
@@ -121,7 +122,7 @@
         self.assertEqual(sum(map(len, expected_lines)), end_point)
 
     def test_stats(self):
-        compressor = groupcompress.GroupCompressor(True)
+        compressor = groupcompress.GroupCompressor()
         compressor.compress(('label',), 'strange\ncommon long line\n'
                                         'plus more text\n', None)
         compressor.compress(('newlabel',),
@@ -135,7 +136,7 @@
     def test_extract_from_compressor(self):
         # Knit fetching will try to reconstruct texts locally which results in
         # reading something that is in the compressor stream already.
-        compressor = groupcompress.GroupCompressor(True)
+        compressor = groupcompress.GroupCompressor()
         sha1_1, _, _, _ = compressor.compress(('label',),
             'strange\ncommon long line\nthat needs a 16 byte match\n', None)
         expected_lines = list(compressor.lines)
@@ -187,22 +188,37 @@
 
 class TestGroupCompressBlock(tests.TestCase):
 
+    def make_block(self, key_to_text):
+        """Create a GroupCompressBlock, filling it with the given texts."""
+        compressor = groupcompress.GroupCompressor()
+        start = 0
+        for key in sorted(key_to_text):
+            compressor.compress(key, key_to_text[key], None)
+        block = compressor.flush()
+        entries = block._entries
+        # Go through from_bytes(to_bytes()) so that we start with a compressed
+        # content object
+        return entries, groupcompress.GroupCompressBlock.from_bytes(
+            block.to_bytes())
+
     def test_from_empty_bytes(self):
         self.assertRaises(ValueError,
                           groupcompress.GroupCompressBlock.from_bytes, '')
 
     def test_from_minimal_bytes(self):
-        block = groupcompress.GroupCompressBlock.from_bytes('gcb1z\n0\n0\n')
+        block = groupcompress.GroupCompressBlock.from_bytes(
+            'gcb1z\n0\n0\n0\n0\n')
         self.assertIsInstance(block, groupcompress.GroupCompressBlock)
         self.assertEqual({}, block._entries)
+        self.assertIs(None, block._content)
+        self.assertEqual('', block._z_content)
+        block._ensure_content()
+        self.assertEqual('', block._content)
+        self.assertIs(None, block._z_content)
+        block._ensure_content() # Ensure content is safe to call 2x
 
-    def test_from_bytes(self):
-        z_header_bytes = (
-            'gcb1z\n' # group compress block v1 plain
-            '76\n' # Length of zlib bytes
-            '183\n' # Length of all meta-info
-            + zlib.compress(
-            'key:bing\n'
+    def test_from_bytes_with_labels(self):
+        header = ('key:bing\n'
             'sha1:abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd\n'
             'type:fulltext\n'
             'start:100\n'
@@ -213,10 +229,24 @@
             'type:fulltext\n'
             'start:0\n'
             'length:100\n'
-            '\n'))
+            '\n')
+        z_header = zlib.compress(header)
+        content = ('a tiny bit of content\n')
+        z_content = zlib.compress(content)
+        z_bytes = (
+            'gcb1z\n' # group compress block v1 plain
+            '%d\n' # Length of zlib bytes
+            '%d\n' # Length of all meta-info
+            '%d\n' # Length of compressed content
+            '%d\n' # Length of uncompressed content
+            '%s'   # Compressed header
+            '%s'   # Compressed content
+            ) % (len(z_header), len(header),
+                 len(z_content), len(content),
+                 z_header, z_content)
         block = groupcompress.GroupCompressBlock.from_bytes(
-            z_header_bytes)
-        self.assertIs(None, block._content)
+            z_bytes)
+        block._parse_header()
         self.assertIsInstance(block, groupcompress.GroupCompressBlock)
         self.assertEqual([('bing',), ('foo', 'bar')], sorted(block._entries))
         bing = block._entries[('bing',)]
@@ -231,6 +261,29 @@
         self.assertEqual('abcd'*10, foobar.sha1)
         self.assertEqual(0, foobar.start)
         self.assertEqual(100, foobar.length)
+        self.assertEqual(z_content, block._z_content)
+        self.assertIs(None, block._content)
+        block._ensure_content()
+        self.assertIs(None, block._z_content)
+        self.assertEqual(content, block._content)
+
+    def test_from_old_bytes(self):
+        # Backwards compatibility, with groups that didn't define content length
+        content = ('a tiny bit of content\n')
+        z_content = zlib.compress(content)
+        z_bytes = (
+            'gcb1z\n' # group compress block v1 plain
+            '0\n' # Length of zlib bytes
+            '0\n' # Length of all meta-info
+            ''    # Compressed header
+            '%s'   # Compressed content
+            ) % (z_content)
+        block = groupcompress.GroupCompressBlock.from_bytes(
+            z_bytes)
+        self.assertIsInstance(block, groupcompress.GroupCompressBlock)
+        block._ensure_content()
+        self.assertIs(None, block._z_content)
+        self.assertEqual(content, block._content)
 
     def test_add_entry(self):
         gcb = groupcompress.GroupCompressBlock()
@@ -243,16 +296,25 @@
         self.assertEqual(100, e.length)
 
     def test_to_bytes(self):
+        no_labels = groupcompress._NO_LABELS
+        def reset():
+            groupcompress._NO_LABELS = no_labels
+        self.addCleanup(reset)
+        groupcompress._NO_LABELS = False
         gcb = groupcompress.GroupCompressBlock()
         gcb.add_entry(('foo', 'bar'), 'fulltext', 'abcd'*10, 0, 100)
         gcb.add_entry(('bing',), 'fulltext', 'abcd'*10, 100, 100)
+        gcb.set_content('this is some content\n'
+                        'this content will be compressed\n')
         bytes = gcb.to_bytes()
-        self.assertStartsWith(bytes,
-                              'gcb1z\n' # group compress block v1 zlib
-                              '76\n' # Length of compressed bytes
-                              '183\n' # Length of all meta-info
-                             )
-        remaining_bytes = bytes[13:]
+        expected_header =('gcb1z\n' # group compress block v1 zlib
+                          '76\n' # Length of compressed bytes
+                          '183\n' # Length of uncompressed meta-info
+                          '50\n' # Length of compressed content
+                          '53\n' # Length of uncompressed content
+                         )
+        self.assertStartsWith(bytes, expected_header)
+        remaining_bytes = bytes[len(expected_header):]
         raw_bytes = zlib.decompress(remaining_bytes)
         self.assertEqualDiff('key:bing\n'
                              'sha1:abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd\n'
@@ -266,3 +328,416 @@
                              'start:0\n'
                              'length:100\n'
                              '\n', raw_bytes)
+
+    def test_extract_no_end(self):
+        # We should be able to extract a record, even if we only know the start
+        # of the bytes.
+        texts = {
+            ('key1',): 'text for key1\nhas bytes that are common\n',
+            ('key2',): 'text for key2\nhas bytes that are common\n',
+        }
+        entries, block = self.make_block(texts)
+        self.assertEqualDiff('text for key1\nhas bytes that are common\n',
+                             block.extract(('key1',), entries[('key1',)].start,
+                                           end=None)[1])
+        self.assertEqualDiff('text for key2\nhas bytes that are common\n',
+                             block.extract(('key2',), entries[('key2',)].start,
+                                           end=None)[1])
+
+    def test_partial_decomp(self):
+        content_chunks = []
+        # We need a sufficient amount of data so that zlib.decompress has
+        # partial decompression to work with. Most auto-generated data
+        # compresses a bit too well, we want a combination, so we combine a sha
+        # hash with compressible data.
+        for i in xrange(2048):
+            next_content = '%d\nThis is a bit of duplicate text\n' % (i,)
+            content_chunks.append(next_content)
+            next_sha1 = osutils.sha_string(next_content)
+            content_chunks.append(next_sha1 + '\n')
+        content = ''.join(content_chunks)
+        self.assertEqual(158634, len(content))
+        z_content = zlib.compress(content)
+        self.assertEqual(57182, len(z_content))
+        block = groupcompress.GroupCompressBlock()
+        block._z_content = z_content
+        block._z_content_length = len(z_content)
+        block._compressor_name = 'zlib'
+        block._content_length = 158634
+        self.assertIs(None, block._content)
+        block._ensure_content(100)
+        self.assertIsNot(None, block._content)
+        # We have decompressed at least 100 bytes
+        self.assertTrue(len(block._content) >= 100)
+        # We have not decompressed the whole content
+        self.assertTrue(len(block._content) < 158634)
+        self.assertEqualDiff(content[:len(block._content)], block._content)
+        # ensuring content that we already have shouldn't cause any more data
+        # to be extracted
+        cur_len = len(block._content)
+        block._ensure_content(cur_len - 10)
+        self.assertEqual(cur_len, len(block._content))
+        # Now we want a bit more content
+        cur_len += 10
+        block._ensure_content(cur_len)
+        self.assertTrue(len(block._content) >= cur_len)
+        self.assertTrue(len(block._content) < 158634)
+        self.assertEqualDiff(content[:len(block._content)], block._content)
+        # And now lets finish
+        block._ensure_content(158634)
+        self.assertEqualDiff(content, block._content)
+        # And the decompressor is finalized
+        self.assertIs(None, block._z_content_decompressor)
+
+    def test_partial_decomp_no_known_length(self):
+        content_chunks = []
+        for i in xrange(2048):
+            next_content = '%d\nThis is a bit of duplicate text\n' % (i,)
+            content_chunks.append(next_content)
+            next_sha1 = osutils.sha_string(next_content)
+            content_chunks.append(next_sha1 + '\n')
+        content = ''.join(content_chunks)
+        self.assertEqual(158634, len(content))
+        z_content = zlib.compress(content)
+        self.assertEqual(57182, len(z_content))
+        block = groupcompress.GroupCompressBlock()
+        block._z_content = z_content
+        block._z_content_length = len(z_content)
+        block._compressor_name = 'zlib'
+        block._content_length = None # Don't tell the decompressed length
+        self.assertIs(None, block._content)
+        block._ensure_content(100)
+        self.assertIsNot(None, block._content)
+        # We have decompressed at least 100 bytes
+        self.assertTrue(len(block._content) >= 100)
+        # We have not decompressed the whole content
+        self.assertTrue(len(block._content) < 158634)
+        self.assertEqualDiff(content[:len(block._content)], block._content)
+        # ensuring content that we already have shouldn't cause any more data
+        # to be extracted
+        cur_len = len(block._content)
+        block._ensure_content(cur_len - 10)
+        self.assertEqual(cur_len, len(block._content))
+        # Now we want a bit more content
+        cur_len += 10
+        block._ensure_content(cur_len)
+        self.assertTrue(len(block._content) >= cur_len)
+        self.assertTrue(len(block._content) < 158634)
+        self.assertEqualDiff(content[:len(block._content)], block._content)
+        # And now lets finish
+        block._ensure_content()
+        self.assertEqualDiff(content, block._content)
+        # And the decompressor is finalized
+        self.assertIs(None, block._z_content_decompressor)
+
+
+class TestCaseWithGroupCompressVersionedFiles(tests.TestCaseWithTransport):
+
+    def make_test_vf(self, create_graph, keylength=1, do_cleanup=True,
+                     dir='.'):
+        t = self.get_transport(dir)
+        t.ensure_base()
+        vf = groupcompress.make_pack_factory(graph=create_graph,
+            delta=False, keylength=keylength)(t)
+        if do_cleanup:
+            self.addCleanup(groupcompress.cleanup_pack_group, vf)
+        return vf
+
+
+class TestGroupCompressVersionedFiles(TestCaseWithGroupCompressVersionedFiles):
+
+    def test_get_record_stream_as_requested(self):
+        # Consider promoting 'as-requested' to general availability, and
+        # make this a VF interface test
+        vf = self.make_test_vf(False, dir='source')
+        vf.add_lines(('a',), (), ['lines\n'])
+        vf.add_lines(('b',), (), ['lines\n'])
+        vf.add_lines(('c',), (), ['lines\n'])
+        vf.add_lines(('d',), (), ['lines\n'])
+        vf.writer.end()
+        keys = [record.key for record in vf.get_record_stream(
+                    [('a',), ('b',), ('c',), ('d',)],
+                    'as-requested', False)]
+        self.assertEqual([('a',), ('b',), ('c',), ('d',)], keys)
+        keys = [record.key for record in vf.get_record_stream(
+                    [('b',), ('a',), ('d',), ('c',)],
+                    'as-requested', False)]
+        self.assertEqual([('b',), ('a',), ('d',), ('c',)], keys)
+
+        # It should work even after being repacked into another VF
+        vf2 = self.make_test_vf(False, dir='target')
+        vf2.insert_record_stream(vf.get_record_stream(
+                    [('b',), ('a',), ('d',), ('c',)], 'as-requested', False))
+        vf2.writer.end()
+
+        keys = [record.key for record in vf2.get_record_stream(
+                    [('a',), ('b',), ('c',), ('d',)],
+                    'as-requested', False)]
+        self.assertEqual([('a',), ('b',), ('c',), ('d',)], keys)
+        keys = [record.key for record in vf2.get_record_stream(
+                    [('b',), ('a',), ('d',), ('c',)],
+                    'as-requested', False)]
+        self.assertEqual([('b',), ('a',), ('d',), ('c',)], keys)
+
+    def test_insert_record_stream_re_uses_blocks(self):
+        vf = self.make_test_vf(True, dir='source')
+        def grouped_stream(revision_ids, first_parents=()):
+            parents = first_parents
+            for revision_id in revision_ids:
+                key = (revision_id,)
+                record = versionedfile.FulltextContentFactory(
+                    key, parents, None,
+                    'some content that is\n'
+                    'identical except for\n'
+                    'revision_id:%s\n' % (revision_id,))
+                yield record
+                parents = (key,)
+        # One group, a-d
+        vf.insert_record_stream(grouped_stream(['a', 'b', 'c', 'd']))
+        # Second group, e-h
+        vf.insert_record_stream(grouped_stream(['e', 'f', 'g', 'h'],
+                                               first_parents=(('d',),)))
+        block_bytes = {}
+        stream = vf.get_record_stream([(r,) for r in 'abcdefgh'],
+                                      'unordered', False)
+        num_records = 0
+        for record in stream:
+            if record.key in [('a',), ('e',)]:
+                self.assertEqual('groupcompress-block', record.storage_kind)
+            else:
+                self.assertEqual('groupcompress-block-ref',
+                                 record.storage_kind)
+            block_bytes[record.key] = record._manager._block._z_content
+            num_records += 1
+        self.assertEqual(8, num_records)
+        for r in 'abcd':
+            key = (r,)
+            self.assertIs(block_bytes[key], block_bytes[('a',)])
+            self.assertNotEqual(block_bytes[key], block_bytes[('e',)])
+        for r in 'efgh':
+            key = (r,)
+            self.assertIs(block_bytes[key], block_bytes[('e',)])
+            self.assertNotEqual(block_bytes[key], block_bytes[('a',)])
+        # Now copy the blocks into another vf, and ensure that the blocks are
+        # preserved without creating new entries
+        vf2 = self.make_test_vf(True, dir='target')
+        # ordering in 'groupcompress' order, should actually swap the groups in
+        # the target vf, but the groups themselves should not be disturbed.
+        vf2.insert_record_stream(vf.get_record_stream(
+            [(r,) for r in 'abcdefgh'], 'groupcompress', False))
+        stream = vf2.get_record_stream([(r,) for r in 'abcdefgh'],
+                                       'groupcompress', False)
+        vf2.writer.end()
+        num_records = 0
+        for record in stream:
+            num_records += 1
+            self.assertEqual(block_bytes[record.key],
+                             record._manager._block._z_content)
+        self.assertEqual(8, num_records)
+
+    def test__insert_record_stream_no_reuse_block(self):
+        vf = self.make_test_vf(True, dir='source')
+        def grouped_stream(revision_ids, first_parents=()):
+            parents = first_parents
+            for revision_id in revision_ids:
+                key = (revision_id,)
+                record = versionedfile.FulltextContentFactory(
+                    key, parents, None,
+                    'some content that is\n'
+                    'identical except for\n'
+                    'revision_id:%s\n' % (revision_id,))
+                yield record
+                parents = (key,)
+        # One group, a-d
+        vf.insert_record_stream(grouped_stream(['a', 'b', 'c', 'd']))
+        # Second group, e-h
+        vf.insert_record_stream(grouped_stream(['e', 'f', 'g', 'h'],
+                                               first_parents=(('d',),)))
+        vf.writer.end()
+        self.assertEqual(8, len(list(vf.get_record_stream(
+                                        [(r,) for r in 'abcdefgh'],
+                                        'unordered', False))))
+        # Now copy the blocks into another vf, and ensure that the blocks are
+        # preserved without creating new entries
+        vf2 = self.make_test_vf(True, dir='target')
+        # ordering in 'groupcompress' order, should actually swap the groups in
+        # the target vf, but the groups themselves should not be disturbed.
+        list(vf2._insert_record_stream(vf.get_record_stream(
+            [(r,) for r in 'abcdefgh'], 'groupcompress', False),
+            reuse_blocks=False))
+        vf2.writer.end()
+        # After inserting with reuse_blocks=False, we should have everything in
+        # a single new block.
+        stream = vf2.get_record_stream([(r,) for r in 'abcdefgh'],
+                                       'groupcompress', False)
+        block = None
+        for record in stream:
+            if block is None:
+                block = record._manager._block
+            else:
+                self.assertIs(block, record._manager._block)
+
+
+class TestLazyGroupCompress(tests.TestCaseWithTransport):
+
+    _texts = {
+        ('key1',): "this is a text\n"
+                   "with a reasonable amount of compressible bytes\n",
+        ('key2',): "another text\n"
+                   "with a reasonable amount of compressible bytes\n",
+        ('key3',): "yet another text which won't be extracted\n"
+                   "with a reasonable amount of compressible bytes\n",
+        ('key4',): "this will be extracted\n"
+                   "but references bytes from\n"
+                   "yet another text which won't be extracted\n"
+                   "with a reasonable amount of compressible bytes\n",
+    }
+    def make_block(self, key_to_text):
+        """Create a GroupCompressBlock, filling it with the given texts."""
+        compressor = groupcompress.GroupCompressor()
+        start = 0
+        for key in sorted(key_to_text):
+            compressor.compress(key, key_to_text[key], None)
+        block = compressor.flush()
+        entries = block._entries
+        raw_bytes = block.to_bytes()
+        return entries, groupcompress.GroupCompressBlock.from_bytes(raw_bytes)
+
+    def add_key_to_manager(self, key, entries, block, manager):
+        entry = entries[key]
+        manager.add_factory(entry.key, (), entry.start, entry.end)
+
+    def test_get_fulltexts(self):
+        entries, block = self.make_block(self._texts)
+        manager = groupcompress._LazyGroupContentManager(block)
+        self.add_key_to_manager(('key1',), entries, block, manager)
+        self.add_key_to_manager(('key2',), entries, block, manager)
+        result_order = []
+        for record in manager.get_record_stream():
+            result_order.append(record.key)
+            text = self._texts[record.key]
+            self.assertEqual(text, record.get_bytes_as('fulltext'))
+        self.assertEqual([('key1',), ('key2',)], result_order)
+
+        # If we build the manager in the opposite order, we should get them
+        # back in the opposite order
+        manager = groupcompress._LazyGroupContentManager(block)
+        self.add_key_to_manager(('key2',), entries, block, manager)
+        self.add_key_to_manager(('key1',), entries, block, manager)
+        result_order = []
+        for record in manager.get_record_stream():
+            result_order.append(record.key)
+            text = self._texts[record.key]
+            self.assertEqual(text, record.get_bytes_as('fulltext'))
+        self.assertEqual([('key2',), ('key1',)], result_order)
+
+    def test__wire_bytes_no_keys(self):
+        entries, block = self.make_block(self._texts)
+        manager = groupcompress._LazyGroupContentManager(block)
+        wire_bytes = manager._wire_bytes()
+        block_length = len(block.to_bytes())
+        # We should have triggered a strip, since we aren't using any content
+        stripped_block = manager._block.to_bytes()
+        self.assertTrue(block_length > len(stripped_block))
+        empty_z_header = zlib.compress('')
+        self.assertEqual('groupcompress-block\n'
+                         '8\n' # len(compress(''))
+                         '0\n' # len('')
+                         '%d\n'# compressed block len
+                         '%s'  # zheader
+                         '%s'  # block
+                         % (len(stripped_block), empty_z_header,
+                            stripped_block),
+                         wire_bytes)
+
+    def test__wire_bytes(self):
+        entries, block = self.make_block(self._texts)
+        manager = groupcompress._LazyGroupContentManager(block)
+        self.add_key_to_manager(('key1',), entries, block, manager)
+        self.add_key_to_manager(('key4',), entries, block, manager)
+        block_bytes = block.to_bytes()
+        wire_bytes = manager._wire_bytes()
+        (storage_kind, z_header_len, header_len,
+         block_len, rest) = wire_bytes.split('\n', 4)
+        z_header_len = int(z_header_len)
+        header_len = int(header_len)
+        block_len = int(block_len)
+        self.assertEqual('groupcompress-block', storage_kind)
+        self.assertEqual(33, z_header_len)
+        self.assertEqual(25, header_len)
+        self.assertEqual(len(block_bytes), block_len)
+        z_header = rest[:z_header_len]
+        header = zlib.decompress(z_header)
+        self.assertEqual(header_len, len(header))
+        entry1 = entries[('key1',)]
+        entry4 = entries[('key4',)]
+        self.assertEqualDiff('key1\n'
+                             '\n'  # no parents
+                             '%d\n' # start offset
+                             '%d\n' # end byte
+                             'key4\n'
+                             '\n'
+                             '%d\n'
+                             '%d\n'
+                             % (entry1.start, entry1.end,
+                                entry4.start, entry4.end),
+                            header)
+        z_block = rest[z_header_len:]
+        self.assertEqual(block_bytes, z_block)
+
+    def test_from_bytes(self):
+        entries, block = self.make_block(self._texts)
+        manager = groupcompress._LazyGroupContentManager(block)
+        self.add_key_to_manager(('key1',), entries, block, manager)
+        self.add_key_to_manager(('key4',), entries, block, manager)
+        wire_bytes = manager._wire_bytes()
+        self.assertStartsWith(wire_bytes, 'groupcompress-block\n')
+        manager = groupcompress._LazyGroupContentManager.from_bytes(wire_bytes)
+        self.assertIsInstance(manager, groupcompress._LazyGroupContentManager)
+        self.assertEqual(2, len(manager._factories))
+        self.assertEqual(block._z_content, manager._block._z_content)
+        result_order = []
+        for record in manager.get_record_stream():
+            result_order.append(record.key)
+            text = self._texts[record.key]
+            self.assertEqual(text, record.get_bytes_as('fulltext'))
+        self.assertEqual([('key1',), ('key4',)], result_order)
+
+    def test__check_rebuild_no_changes(self):
+        entries, block = self.make_block(self._texts)
+        manager = groupcompress._LazyGroupContentManager(block)
+        # Request all the keys, which ensures that we won't rebuild
+        self.add_key_to_manager(('key1',), entries, block, manager)
+        self.add_key_to_manager(('key2',), entries, block, manager)
+        self.add_key_to_manager(('key3',), entries, block, manager)
+        self.add_key_to_manager(('key4',), entries, block, manager)
+        manager._check_rebuild_block()
+        self.assertIs(block, manager._block)
+
+    def test__check_rebuild_only_one(self):
+        entries, block = self.make_block(self._texts)
+        manager = groupcompress._LazyGroupContentManager(block)
+        # Request just the first key, which should trigger a 'strip' action
+        self.add_key_to_manager(('key1',), entries, block, manager)
+        manager._check_rebuild_block()
+        self.assertIsNot(block, manager._block)
+        self.assertTrue(block._content_length > manager._block._content_length)
+        # We should be able to still get the content out of this block, though
+        # it should only have 1 entry
+        for record in manager.get_record_stream():
+            self.assertEqual(('key1',), record.key)
+            self.assertEqual(self._texts[record.key],
+                             record.get_bytes_as('fulltext'))
+
+    def test__check_rebuild_middle(self):
+        entries, block = self.make_block(self._texts)
+        manager = groupcompress._LazyGroupContentManager(block)
+        # Request a small key in the middle should trigger a 'rebuild'
+        self.add_key_to_manager(('key4',), entries, block, manager)
+        manager._check_rebuild_block()
+        self.assertIsNot(block, manager._block)
+        self.assertTrue(block._content_length > manager._block._content_length)
+        for record in manager.get_record_stream():
+            self.assertEqual(('key4',), record.key)
+            self.assertEqual(self._texts[record.key],
+                             record.get_bytes_as('fulltext'))

=== modified file 'bzrlib/tests/test_versionedfile.py'
--- bzrlib/tests/test_versionedfile.py	2009-03-13 06:08:29 +0000
+++ bzrlib/tests/test_versionedfile.py	2009-03-16 21:17:44 +0000
@@ -1663,15 +1663,17 @@
              'knit-ft', 'knit-delta', 'chunked', 'fulltext',
              'knit-annotated-ft-gz', 'knit-annotated-delta-gz', 'knit-ft-gz',
              'knit-delta-gz',
-             'knit-delta-closure', 'knit-delta-closure-ref'])
+             'knit-delta-closure', 'knit-delta-closure-ref',
+             'groupcompress-block', 'groupcompress-block-ref'])
 
     def capture_stream(self, f, entries, on_seen, parents):
         """Capture a stream for testing."""
         for factory in entries:
             on_seen(factory.key)
             self.assertValidStorageKind(factory.storage_kind)
-            self.assertEqual(f.get_sha1s([factory.key])[factory.key],
-                factory.sha1)
+            if factory.sha1 is not None:
+                self.assertEqual(f.get_sha1s([factory.key])[factory.key],
+                    factory.sha1)
             self.assertEqual(parents[factory.key], factory.parents)
             self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
                 str)
@@ -1814,8 +1816,9 @@
         for factory in entries:
             seen.add(factory.key)
             self.assertValidStorageKind(factory.storage_kind)
-            self.assertEqual(files.get_sha1s([factory.key])[factory.key],
-                factory.sha1)
+            if factory.sha1 is not None:
+                self.assertEqual(files.get_sha1s([factory.key])[factory.key],
+                                 factory.sha1)
             self.assertEqual(parent_map[factory.key], factory.parents)
             # currently no stream emits mpdiff
             self.assertRaises(errors.UnavailableRepresentation,
@@ -2019,8 +2022,9 @@
                 self.assertEqual(None, factory.parents)
             else:
                 self.assertValidStorageKind(factory.storage_kind)
-                self.assertEqual(files.get_sha1s([factory.key])[factory.key],
-                    factory.sha1)
+                if factory.sha1 is not None:
+                    sha1 = files.get_sha1s([factory.key])[factory.key]
+                    self.assertEqual(sha1, factory.sha1)
                 self.assertEqual(parents[factory.key], factory.parents)
                 self.assertIsInstance(factory.get_bytes_as(factory.storage_kind),
                     str)

=== modified file 'bzrlib/versionedfile.py'
--- bzrlib/versionedfile.py	2009-03-17 20:13:40 +0000
+++ bzrlib/versionedfile.py	2009-03-17 20:33:54 +0000
@@ -31,6 +31,7 @@
 
 from bzrlib import (
     errors,
+    groupcompress,
     index,
     knit,
     osutils,
@@ -1518,6 +1519,7 @@
             'knit-annotated-delta-gz':knit.knit_network_to_record,
             'knit-delta-closure':knit.knit_delta_closure_to_records,
             'fulltext':fulltext_network_to_record,
+            'groupcompress-block':groupcompress.network_block_to_records,
             }
 
     def read(self):

# Begin bundle
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