def get_packed_refs(self): """Get contents of the packed-refs file. :return: Dictionary mapping ref names to SHA1s :note: Will return an empty dictionary when no packed-refs file is present. """ # TODO: invalidate the cache on repacking if self._packed_refs is None: # set both to empty because we want _peeled_refs to be # None if and only if _packed_refs is also None. self._packed_refs = {} self._peeled_refs = {} path = os.path.join(self.path, 'packed-refs') try: f = GitFile(path, 'rb') except IOError, e: if e.errno == errno.ENOENT: return {} raise try: first_line = iter(f).next().rstrip() if (first_line.startswith("# pack-refs") and " peeled" in first_line): for sha, name, peeled in read_packed_refs_with_peeled(f): self._packed_refs[name] = sha if peeled: self._peeled_refs[name] = peeled else: f.seek(0) for sha, name in read_packed_refs(f): self._packed_refs[name] = sha finally: f.close()
class PackData(object): """The data contained in a packfile. Pack files can be accessed both sequentially for exploding a pack, and directly with the help of an index to retrieve a specific object. The objects within are either complete or a delta aginst another. The header is variable length. If the MSB of each byte is set then it indicates that the subsequent byte is still part of the header. For the first byte the next MS bits are the type, which tells you the type of object, and whether it is a delta. The LS byte is the lowest bits of the size. For each subsequent byte the LS 7 bits are the next MS bits of the size, i.e. the last byte of the header contains the MS bits of the size. For the complete objects the data is stored as zlib deflated data. The size in the header is the uncompressed object size, so to uncompress you need to just keep feeding data to zlib until you get an object back, or it errors on bad data. This is done here by just giving the complete buffer from the start of the deflated object on. This is bad, but until I get mmap sorted out it will have to do. Currently there are no integrity checks done. Also no attempt is made to try and detect the delta case, or a request for an object at the wrong position. It will all just throw a zlib or KeyError. """ def __init__(self, filename, file=None, size=None): """Create a PackData object representing the pack in the given filename. The file must exist and stay readable until the object is disposed of. It must also stay the same size. It will be mapped whenever needed. Currently there is a restriction on the size of the pack as the python mmap implementation is flawed. """ self._filename = filename self._size = size self._header_size = 12 if file is None: self._file = GitFile(self._filename, 'rb') else: self._file = file (version, self._num_objects) = read_pack_header(self._file.read) self._offset_cache = LRUSizeCache(1024*1024*20, compute_size=_compute_object_size) self.pack = None @classmethod def from_file(cls, file, size): return cls(str(file), file=file, size=size) @classmethod def from_path(cls, path): return cls(filename=path) def close(self): self._file.close() def _get_size(self): if self._size is not None: return self._size self._size = os.path.getsize(self._filename) if self._size < self._header_size: errmsg = ("%s is too small for a packfile (%d < %d)" % (self._filename, self._size, self._header_size)) raise AssertionError(errmsg) return self._size def __len__(self): """Returns the number of objects in this pack.""" return self._num_objects def calculate_checksum(self): """Calculate the checksum for this pack. :return: 20-byte binary SHA1 digest """ s = make_sha() self._file.seek(0) todo = self._get_size() - 20 while todo > 0: x = self._file.read(min(todo, 1<<16)) s.update(x) todo -= len(x) return s.digest() def get_ref(self, sha): """Get the object for a ref SHA, only looking in this pack.""" # TODO: cache these results if self.pack is None: raise KeyError(sha) offset = self.pack.index.object_index(sha) if not offset: raise KeyError(sha) type, obj = self.get_object_at(offset) return offset, type, obj def resolve_object(self, offset, type, obj, get_ref=None): """Resolve an object, possibly resolving deltas when necessary. :return: Tuple with object type and contents. """ if type not in DELTA_TYPES: return type, obj if get_ref is None: get_ref = self.get_ref if type == OFS_DELTA: (delta_offset, delta) = obj # TODO: clean up asserts and replace with nicer error messages assert isinstance(offset, int) assert isinstance(delta_offset, int) base_offset = offset-delta_offset type, base_obj = self.get_object_at(base_offset) assert isinstance(type, int) elif type == REF_DELTA: (basename, delta) = obj assert isinstance(basename, str) and len(basename) == 20 base_offset, type, base_obj = get_ref(basename) assert isinstance(type, int) type, base_chunks = self.resolve_object(base_offset, type, base_obj) chunks = apply_delta(base_chunks, delta) # TODO(dborowitz): This can result in poor performance if large base # objects are separated from deltas in the pack. We should reorganize # so that we apply deltas to all objects in a chain one after the other # to optimize cache performance. if offset is not None: self._offset_cache[offset] = type, chunks return type, chunks def iterobjects(self, progress=None): return PackObjectIterator(self, progress) def iterentries(self, progress=None): """Yield entries summarizing the contents of this pack. :param progress: Progress function, called with current and total object count. :return: iterator of tuples with (sha, offset, crc32) """ for offset, type, obj, crc32 in self.iterobjects(progress=progress): assert isinstance(offset, int) assert isinstance(type, int) assert isinstance(obj, list) or isinstance(obj, tuple) type, obj = self.resolve_object(offset, type, obj) yield obj_sha(type, obj), offset, crc32 def sorted_entries(self, progress=None): """Return entries in this pack, sorted by SHA. :param progress: Progress function, called with current and total object count :return: List of tuples with (sha, offset, crc32) """ ret = list(self.iterentries(progress=progress)) ret.sort() return ret def create_index_v1(self, filename, progress=None): """Create a version 1 file for this data file. :param filename: Index filename. :param progress: Progress report function :return: Checksum of index file """ entries = self.sorted_entries(progress=progress) f = GitFile(filename, 'wb') try: return write_pack_index_v1(f, entries, self.calculate_checksum()) finally: f.close() def create_index_v2(self, filename, progress=None): """Create a version 2 index file for this data file. :param filename: Index filename. :param progress: Progress report function :return: Checksum of index file """ entries = self.sorted_entries(progress=progress) f = GitFile(filename, 'wb') try: return write_pack_index_v2(f, entries, self.calculate_checksum()) finally: f.close() def create_index(self, filename, progress=None, version=2): """Create an index file for this data file. :param filename: Index filename. :param progress: Progress report function :return: Checksum of index file """ if version == 1: return self.create_index_v1(filename, progress) elif version == 2: return self.create_index_v2(filename, progress) else: raise ValueError("unknown index format %d" % version) def get_stored_checksum(self): """Return the expected checksum stored in this pack.""" self._file.seek(self._get_size()-20) return self._file.read(20) def check(self): """Check the consistency of this pack.""" actual = self.calculate_checksum() stored = self.get_stored_checksum() if actual != stored: raise ChecksumMismatch(stored, actual) def get_object_at(self, offset): """Given an offset in to the packfile return the object that is there. Using the associated index the location of an object can be looked up, and then the packfile can be asked directly for that object using this function. """ if offset in self._offset_cache: return self._offset_cache[offset] assert isinstance(offset, long) or isinstance(offset, int),\ "offset was %r" % offset assert offset >= self._header_size self._file.seek(offset) return unpack_object(self._file.read)[:2]