/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to dulwich/pack.py

  • Committer: Jelmer Vernooij
  • Date: 2009-01-14 18:24:38 UTC
  • mto: (0.222.3 dulwich)
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@samba.org-20090114182438-c0tn5eczyupi4ztn
Fix download url, add version number.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# pack.py -- For dealing wih packed git objects.
 
2
# Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
 
3
# Copryight (C) 2008 Jelmer Vernooij <jelmer@samba.org>
 
4
# The code is loosely based on that in the sha1_file.c file from git itself,
 
5
# which is Copyright (C) Linus Torvalds, 2005 and distributed under the
 
6
# GPL version 2.
 
7
 
8
# This program is free software; you can redistribute it and/or
 
9
# modify it under the terms of the GNU General Public License
 
10
# as published by the Free Software Foundation; version 2
 
11
# of the License.
 
12
 
13
# This program is distributed in the hope that it will be useful,
 
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
16
# GNU General Public License for more details.
 
17
 
18
# You should have received a copy of the GNU General Public License
 
19
# along with this program; if not, write to the Free Software
 
20
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 
21
# MA  02110-1301, USA.
 
22
 
 
23
"""Classes for dealing with packed git objects.
 
24
 
 
25
A pack is a compact representation of a bunch of objects, stored
 
26
using deltas where possible.
 
27
 
 
28
They have two parts, the pack file, which stores the data, and an index
 
29
that tells you where the data is.
 
30
 
 
31
To find an object you look in all of the index files 'til you find a
 
32
match for the object name. You then use the pointer got from this as
 
33
a pointer in to the corresponding packfile.
 
34
"""
 
35
 
 
36
from collections import defaultdict
 
37
import hashlib
 
38
from itertools import imap, izip
 
39
import mmap
 
40
import os
 
41
import sha
 
42
import struct
 
43
import sys
 
44
import zlib
 
45
import difflib
 
46
 
 
47
from objects import (
 
48
        ShaFile,
 
49
        hex_to_sha,
 
50
        sha_to_hex,
 
51
        )
 
52
from errors import ApplyDeltaError
 
53
 
 
54
supports_mmap_offset = (sys.version_info[0] >= 3 or 
 
55
        (sys.version_info[0] == 2 and sys.version_info[1] >= 6))
 
56
 
 
57
 
 
58
def take_msb_bytes(map, offset):
 
59
    ret = []
 
60
    while len(ret) == 0 or ret[-1] & 0x80:
 
61
        ret.append(ord(map[offset]))
 
62
        offset += 1
 
63
    return ret
 
64
 
 
65
 
 
66
def read_zlib(data, offset, dec_size):
 
67
    obj = zlib.decompressobj()
 
68
    x = ""
 
69
    fed = 0
 
70
    while obj.unused_data == "":
 
71
        base = offset+fed
 
72
        add = data[base:base+1024]
 
73
        fed += len(add)
 
74
        x += obj.decompress(add)
 
75
    assert len(x) == dec_size
 
76
    comp_len = fed-len(obj.unused_data)
 
77
    return x, comp_len
 
78
 
 
79
 
 
80
def iter_sha1(iter):
 
81
    sha = hashlib.sha1()
 
82
    for name in iter:
 
83
        sha.update(name)
 
84
    return sha.hexdigest()
 
85
 
 
86
 
 
87
MAX_MMAP_SIZE = 256 * 1024 * 1024
 
88
 
 
89
def simple_mmap(f, offset, size, access=mmap.ACCESS_READ):
 
90
    """Simple wrapper for mmap() which always supports the offset parameter.
 
91
 
 
92
    :param f: File object.
 
93
    :param offset: Offset in the file, from the beginning of the file.
 
94
    :param size: Size of the mmap'ed area
 
95
    :param access: Access mechanism.
 
96
    :return: MMAP'd area.
 
97
    """
 
98
    if offset+size > MAX_MMAP_SIZE and not supports_mmap_offset:
 
99
        raise AssertionError("%s is larger than 256 meg, and this version "
 
100
            "of Python does not support the offset argument to mmap().")
 
101
    if supports_mmap_offset:
 
102
        return mmap.mmap(f.fileno(), size, access=access, offset=offset)
 
103
    else:
 
104
        class ArraySkipper(object):
 
105
 
 
106
            def __init__(self, array, offset):
 
107
                self.array = array
 
108
                self.offset = offset
 
109
 
 
110
            def __getslice__(self, i, j):
 
111
                return self.array[i+self.offset:j+self.offset]
 
112
 
 
113
            def __getitem__(self, i):
 
114
                return self.array[i+self.offset]
 
115
 
 
116
            def __len__(self):
 
117
                return len(self.array) - self.offset
 
118
 
 
119
            def __str__(self):
 
120
                return str(self.array[self.offset:])
 
121
 
 
122
        mem = mmap.mmap(f.fileno(), size+offset, access=access)
 
123
        if offset == 0:
 
124
            return mem
 
125
        return ArraySkipper(mem, offset)
 
126
 
 
127
 
 
128
def resolve_object(offset, type, obj, get_ref, get_offset):
 
129
  """Resolve an object, possibly resolving deltas when necessary."""
 
130
  if not type in (6, 7): # Not a delta
 
131
     return type, obj
 
132
 
 
133
  if type == 6: # offset delta
 
134
     (delta_offset, delta) = obj
 
135
     assert isinstance(delta_offset, int)
 
136
     assert isinstance(delta, str)
 
137
     offset = offset-delta_offset
 
138
     type, base_obj = get_offset(offset)
 
139
     assert isinstance(type, int)
 
140
  elif type == 7: # ref delta
 
141
     (basename, delta) = obj
 
142
     assert isinstance(basename, str) and len(basename) == 20
 
143
     assert isinstance(delta, str)
 
144
     type, base_obj = get_ref(basename)
 
145
     assert isinstance(type, int)
 
146
  type, base_text = resolve_object(offset, type, base_obj, get_ref, get_offset)
 
147
  return type, apply_delta(base_text, delta)
 
148
 
 
149
 
 
150
class PackIndex(object):
 
151
  """An index in to a packfile.
 
152
 
 
153
  Given a sha id of an object a pack index can tell you the location in the
 
154
  packfile of that object if it has it.
 
155
 
 
156
  To do the loop it opens the file, and indexes first 256 4 byte groups
 
157
  with the first byte of the sha id. The value in the four byte group indexed
 
158
  is the end of the group that shares the same starting byte. Subtract one
 
159
  from the starting byte and index again to find the start of the group.
 
160
  The values are sorted by sha id within the group, so do the math to find
 
161
  the start and end offset and then bisect in to find if the value is present.
 
162
  """
 
163
 
 
164
  def __init__(self, filename):
 
165
    """Create a pack index object.
 
166
 
 
167
    Provide it with the name of the index file to consider, and it will map
 
168
    it whenever required.
 
169
    """
 
170
    self._filename = filename
 
171
    # Take the size now, so it can be checked each time we map the file to
 
172
    # ensure that it hasn't changed.
 
173
    self._size = os.path.getsize(filename)
 
174
    self._file = open(filename, 'r')
 
175
    self._contents = simple_mmap(self._file, 0, self._size)
 
176
    if self._contents[:4] != '\377tOc':
 
177
        self.version = 1
 
178
        self._fan_out_table = self._read_fan_out_table(0)
 
179
    else:
 
180
        (self.version, ) = struct.unpack_from(">L", self._contents, 4)
 
181
        assert self.version in (2,), "Version was %d" % self.version
 
182
        self._fan_out_table = self._read_fan_out_table(8)
 
183
        self._name_table_offset = 8 + 0x100 * 4
 
184
        self._crc32_table_offset = self._name_table_offset + 20 * len(self)
 
185
        self._pack_offset_table_offset = self._crc32_table_offset + 4 * len(self)
 
186
 
 
187
  def __eq__(self, other):
 
188
    if type(self) != type(other):
 
189
        return False
 
190
 
 
191
    if self._fan_out_table != other._fan_out_table:
 
192
        return False
 
193
 
 
194
    for (name1, _, _), (name2, _, _) in izip(self.iterentries(), other.iterentries()):
 
195
        if name1 != name2:
 
196
            return False
 
197
    return True
 
198
 
 
199
  def close(self):
 
200
    self._file.close()
 
201
 
 
202
  def __len__(self):
 
203
    """Return the number of entries in this pack index."""
 
204
    return self._fan_out_table[-1]
 
205
 
 
206
  def _unpack_entry(self, i):
 
207
    """Unpack the i-th entry in the index file.
 
208
 
 
209
    :return: Tuple with object name (SHA), offset in pack file and 
 
210
          CRC32 checksum (if known)."""
 
211
    if self.version == 1:
 
212
        (offset, name) = struct.unpack_from(">L20s", self._contents, 
 
213
            (0x100 * 4) + (i * 24))
 
214
        return (name, offset, None)
 
215
    else:
 
216
        return (self._unpack_name(i), self._unpack_offset(i), 
 
217
                self._unpack_crc32_checksum(i))
 
218
 
 
219
  def _unpack_name(self, i):
 
220
    if self.version == 1:
 
221
        return self._unpack_entry(i)[0]
 
222
    else:
 
223
        return struct.unpack_from("20s", self._contents, 
 
224
                                  self._name_table_offset + i * 20)[0]
 
225
 
 
226
  def _unpack_offset(self, i):
 
227
    if self.version == 1:
 
228
        return self._unpack_entry(i)[1]
 
229
    else:
 
230
        return struct.unpack_from(">L", self._contents, 
 
231
                                  self._pack_offset_table_offset + i * 4)[0]
 
232
 
 
233
  def _unpack_crc32_checksum(self, i):
 
234
    if self.version == 1:
 
235
        return None
 
236
    else:
 
237
        return struct.unpack_from(">L", self._contents, 
 
238
                                  self._crc32_table_offset + i * 4)[0]
 
239
 
 
240
  def __iter__(self):
 
241
      return imap(sha_to_hex, self._itersha())
 
242
 
 
243
  def _itersha(self):
 
244
    for i in range(len(self)):
 
245
        yield self._unpack_name(i)
 
246
 
 
247
  def objects_sha1(self):
 
248
    return iter_sha1(self._itersha())
 
249
 
 
250
  def iterentries(self):
 
251
    """Iterate over the entries in this pack index.
 
252
   
 
253
    Will yield tuples with object name, offset in packfile and crc32 checksum.
 
254
    """
 
255
    for i in range(len(self)):
 
256
        yield self._unpack_entry(i)
 
257
 
 
258
  def _read_fan_out_table(self, start_offset):
 
259
    ret = []
 
260
    for i in range(0x100):
 
261
        ret.append(struct.unpack(">L", self._contents[start_offset+i*4:start_offset+(i+1)*4])[0])
 
262
    return ret
 
263
 
 
264
  def check(self):
 
265
    """Check that the stored checksum matches the actual checksum."""
 
266
    return self.calculate_checksum() == self.get_stored_checksums()[1]
 
267
 
 
268
  def calculate_checksum(self):
 
269
    f = open(self._filename, 'r')
 
270
    try:
 
271
        return hashlib.sha1(self._contents[:-20]).digest()
 
272
    finally:
 
273
        f.close()
 
274
 
 
275
  def get_stored_checksums(self):
 
276
    """Return the SHA1 checksums stored for the corresponding packfile and 
 
277
    this header file itself."""
 
278
    return str(self._contents[-40:-20]), str(self._contents[-20:])
 
279
 
 
280
  def object_index(self, sha):
 
281
    """Return the index in to the corresponding packfile for the object.
 
282
 
 
283
    Given the name of an object it will return the offset that object lives
 
284
    at within the corresponding pack file. If the pack file doesn't have the
 
285
    object then None will be returned.
 
286
    """
 
287
    size = os.path.getsize(self._filename)
 
288
    assert size == self._size, "Pack index %s has changed size, I don't " \
 
289
         "like that" % self._filename
 
290
    if len(sha) == 40:
 
291
        sha = hex_to_sha(sha)
 
292
    return self._object_index(sha)
 
293
 
 
294
  def _object_index(self, sha):
 
295
      """See object_index"""
 
296
      idx = ord(sha[0])
 
297
      if idx == 0:
 
298
          start = 0
 
299
      else:
 
300
          start = self._fan_out_table[idx-1]
 
301
      end = self._fan_out_table[idx]
 
302
      assert start <= end
 
303
      while start <= end:
 
304
        i = (start + end)/2
 
305
        file_sha = self._unpack_name(i)
 
306
        if file_sha < sha:
 
307
          start = i + 1
 
308
        elif file_sha > sha:
 
309
          end = i - 1
 
310
        else:
 
311
          return self._unpack_offset(i)
 
312
      return None
 
313
 
 
314
 
 
315
def read_pack_header(f):
 
316
    header = f.read(12)
 
317
    assert header[:4] == "PACK"
 
318
    (version,) = struct.unpack_from(">L", header, 4)
 
319
    assert version in (2, 3), "Version was %d" % version
 
320
    (num_objects,) = struct.unpack_from(">L", header, 8)
 
321
    return (version, num_objects)
 
322
 
 
323
 
 
324
def read_pack_tail(f):
 
325
    return (f.read(20),)
 
326
 
 
327
 
 
328
def unpack_object(map):
 
329
    bytes = take_msb_bytes(map, 0)
 
330
    type = (bytes[0] >> 4) & 0x07
 
331
    size = bytes[0] & 0x0f
 
332
    for i, byte in enumerate(bytes[1:]):
 
333
      size += (byte & 0x7f) << ((i * 7) + 4)
 
334
    raw_base = len(bytes)
 
335
    if type == 6: # offset delta
 
336
        bytes = take_msb_bytes(map, raw_base)
 
337
        assert not (bytes[-1] & 0x80)
 
338
        delta_base_offset = bytes[0] & 0x7f
 
339
        for byte in bytes[1:]:
 
340
            delta_base_offset += 1
 
341
            delta_base_offset <<= 7
 
342
            delta_base_offset += (byte & 0x7f)
 
343
        raw_base+=len(bytes)
 
344
        uncomp, comp_len = read_zlib(map, raw_base, size)
 
345
        assert size == len(uncomp)
 
346
        return type, (delta_base_offset, uncomp), comp_len+raw_base
 
347
    elif type == 7: # ref delta
 
348
        basename = map[raw_base:raw_base+20]
 
349
        uncomp, comp_len = read_zlib(map, raw_base+20, size)
 
350
        assert size == len(uncomp)
 
351
        return type, (basename, uncomp), comp_len+raw_base+20
 
352
    else:
 
353
        uncomp, comp_len = read_zlib(map, raw_base, size)
 
354
        assert len(uncomp) == size
 
355
        return type, uncomp, comp_len+raw_base
 
356
 
 
357
 
 
358
class PackData(object):
 
359
  """The data contained in a packfile.
 
360
 
 
361
  Pack files can be accessed both sequentially for exploding a pack, and
 
362
  directly with the help of an index to retrieve a specific object.
 
363
 
 
364
  The objects within are either complete or a delta aginst another.
 
365
 
 
366
  The header is variable length. If the MSB of each byte is set then it
 
367
  indicates that the subsequent byte is still part of the header.
 
368
  For the first byte the next MS bits are the type, which tells you the type
 
369
  of object, and whether it is a delta. The LS byte is the lowest bits of the
 
370
  size. For each subsequent byte the LS 7 bits are the next MS bits of the
 
371
  size, i.e. the last byte of the header contains the MS bits of the size.
 
372
 
 
373
  For the complete objects the data is stored as zlib deflated data.
 
374
  The size in the header is the uncompressed object size, so to uncompress
 
375
  you need to just keep feeding data to zlib until you get an object back,
 
376
  or it errors on bad data. This is done here by just giving the complete
 
377
  buffer from the start of the deflated object on. This is bad, but until I
 
378
  get mmap sorted out it will have to do.
 
379
 
 
380
  Currently there are no integrity checks done. Also no attempt is made to try
 
381
  and detect the delta case, or a request for an object at the wrong position.
 
382
  It will all just throw a zlib or KeyError.
 
383
  """
 
384
 
 
385
  def __init__(self, filename):
 
386
    """Create a PackData object that represents the pack in the given filename.
 
387
 
 
388
    The file must exist and stay readable until the object is disposed of. It
 
389
    must also stay the same size. It will be mapped whenever needed.
 
390
 
 
391
    Currently there is a restriction on the size of the pack as the python
 
392
    mmap implementation is flawed.
 
393
    """
 
394
    self._filename = filename
 
395
    assert os.path.exists(filename), "%s is not a packfile" % filename
 
396
    self._size = os.path.getsize(filename)
 
397
    self._header_size = 12
 
398
    assert self._size >= self._header_size, "%s is too small for a packfile" % filename
 
399
    self._read_header()
 
400
 
 
401
  def _read_header(self):
 
402
    f = open(self._filename, 'rb')
 
403
    try:
 
404
        (version, self._num_objects) = \
 
405
                read_pack_header(f)
 
406
        f.seek(self._size-20)
 
407
        (self._stored_checksum,) = read_pack_tail(f)
 
408
    finally:
 
409
        f.close()
 
410
 
 
411
  def __len__(self):
 
412
      """Returns the number of objects in this pack."""
 
413
      return self._num_objects
 
414
 
 
415
  def calculate_checksum(self):
 
416
    f = open(self._filename, 'rb')
 
417
    try:
 
418
        map = simple_mmap(f, 0, self._size)
 
419
        return hashlib.sha1(map[:-20]).digest()
 
420
    finally:
 
421
        f.close()
 
422
 
 
423
  def iterobjects(self):
 
424
    offset = self._header_size
 
425
    f = open(self._filename, 'rb')
 
426
    for i in range(len(self)):
 
427
        map = simple_mmap(f, offset, self._size-offset)
 
428
        (type, obj, total_size) = unpack_object(map)
 
429
        yield offset, type, obj
 
430
        offset += total_size
 
431
    f.close()
 
432
 
 
433
  def iterentries(self, ext_resolve_ref=None):
 
434
    found = {}
 
435
    at = {}
 
436
    postponed = defaultdict(list)
 
437
    class Postpone(Exception):
 
438
        """Raised to postpone delta resolving."""
 
439
        
 
440
    def get_ref_text(sha):
 
441
        if sha in found:
 
442
            return found[sha]
 
443
        if ext_resolve_ref:
 
444
            try:
 
445
                return ext_resolve_ref(sha)
 
446
            except KeyError:
 
447
                pass
 
448
        raise Postpone, (sha, )
 
449
    todo = list(self.iterobjects())
 
450
    while todo:
 
451
      (offset, type, obj) = todo.pop(0)
 
452
      at[offset] = (type, obj)
 
453
      assert isinstance(offset, int)
 
454
      assert isinstance(type, int)
 
455
      assert isinstance(obj, tuple) or isinstance(obj, str)
 
456
      try:
 
457
        type, obj = resolve_object(offset, type, obj, get_ref_text,
 
458
            at.__getitem__)
 
459
      except Postpone, (sha, ):
 
460
        postponed[sha].append((offset, type, obj))
 
461
      else:
 
462
        shafile = ShaFile.from_raw_string(type, obj)
 
463
        sha = shafile.sha().digest()
 
464
        found[sha] = (type, obj)
 
465
        yield sha, offset, shafile.crc32()
 
466
        todo += postponed.get(sha, [])
 
467
    if postponed:
 
468
        raise KeyError([sha_to_hex(h) for h in postponed.keys()])
 
469
 
 
470
  def sorted_entries(self, resolve_ext_ref=None):
 
471
    ret = list(self.iterentries(resolve_ext_ref))
 
472
    ret.sort()
 
473
    return ret
 
474
 
 
475
  def create_index_v1(self, filename):
 
476
    entries = self.sorted_entries()
 
477
    write_pack_index_v1(filename, entries, self.calculate_checksum())
 
478
 
 
479
  def create_index_v2(self, filename):
 
480
    entries = self.sorted_entries()
 
481
    write_pack_index_v2(filename, entries, self.calculate_checksum())
 
482
 
 
483
  def get_stored_checksum(self):
 
484
    return self._stored_checksum
 
485
 
 
486
  def check(self):
 
487
    return (self.calculate_checksum() == self.get_stored_checksum())
 
488
 
 
489
  def get_object_at(self, offset):
 
490
    """Given an offset in to the packfile return the object that is there.
 
491
 
 
492
    Using the associated index the location of an object can be looked up, and
 
493
    then the packfile can be asked directly for that object using this
 
494
    function.
 
495
    """
 
496
    assert isinstance(offset, long) or isinstance(offset, int),\
 
497
            "offset was %r" % offset
 
498
    assert offset >= self._header_size
 
499
    size = os.path.getsize(self._filename)
 
500
    assert size == self._size, "Pack data %s has changed size, I don't " \
 
501
         "like that" % self._filename
 
502
    f = open(self._filename, 'rb')
 
503
    try:
 
504
      map = simple_mmap(f, offset, size-offset)
 
505
      return unpack_object(map)[:2]
 
506
    finally:
 
507
      f.close()
 
508
 
 
509
 
 
510
class SHA1Writer(object):
 
511
    
 
512
    def __init__(self, f):
 
513
        self.f = f
 
514
        self.sha1 = hashlib.sha1("")
 
515
 
 
516
    def write(self, data):
 
517
        self.sha1.update(data)
 
518
        self.f.write(data)
 
519
 
 
520
    def write_sha(self):
 
521
        sha = self.sha1.digest()
 
522
        assert len(sha) == 20
 
523
        self.f.write(sha)
 
524
        return sha
 
525
 
 
526
    def close(self):
 
527
        sha = self.write_sha()
 
528
        self.f.close()
 
529
        return sha
 
530
 
 
531
    def tell(self):
 
532
        return self.f.tell()
 
533
 
 
534
 
 
535
def write_pack_object(f, type, object):
 
536
    """Write pack object to a file.
 
537
 
 
538
    :param f: File to write to
 
539
    :param o: Object to write
 
540
    """
 
541
    ret = f.tell()
 
542
    if type == 6: # ref delta
 
543
        (delta_base_offset, object) = object
 
544
    elif type == 7: # offset delta
 
545
        (basename, object) = object
 
546
    size = len(object)
 
547
    c = (type << 4) | (size & 15)
 
548
    size >>= 4
 
549
    while size:
 
550
        f.write(chr(c | 0x80))
 
551
        c = size & 0x7f
 
552
        size >>= 7
 
553
    f.write(chr(c))
 
554
    if type == 6: # offset delta
 
555
        ret = [delta_base_offset & 0x7f]
 
556
        delta_base_offset >>= 7
 
557
        while delta_base_offset:
 
558
            delta_base_offset -= 1
 
559
            ret.insert(0, 0x80 | (delta_base_offset & 0x7f))
 
560
            delta_base_offset >>= 7
 
561
        f.write("".join([chr(x) for x in ret]))
 
562
    elif type == 7: # ref delta
 
563
        assert len(basename) == 20
 
564
        f.write(basename)
 
565
    f.write(zlib.compress(object))
 
566
    return f.tell()
 
567
 
 
568
 
 
569
def write_pack(filename, objects, num_objects):
 
570
    f = open(filename + ".pack", 'w')
 
571
    try:
 
572
        entries, data_sum = write_pack_data(f, objects, num_objects)
 
573
    finally:
 
574
        f.close()
 
575
    entries.sort()
 
576
    write_pack_index_v2(filename + ".idx", entries, data_sum)
 
577
 
 
578
 
 
579
def write_pack_data(f, objects, num_objects, window=10):
 
580
    """Write a new pack file.
 
581
 
 
582
    :param filename: The filename of the new pack file.
 
583
    :param objects: List of objects to write.
 
584
    :return: List with (name, offset, crc32 checksum) entries, pack checksum
 
585
    """
 
586
    recency = list(objects)
 
587
    # FIXME: Somehow limit delta depth
 
588
    # FIXME: Make thin-pack optional (its not used when cloning a pack)
 
589
    # Build a list of objects ordered by the magic Linus heuristic
 
590
    # This helps us find good objects to diff against us
 
591
    magic = []
 
592
    for o in recency:
 
593
        magic.append( (o._num_type, "filename", 1, -len(o.as_raw_string()[1]), o) )
 
594
    magic.sort()
 
595
    # Build a map of objects and their index in magic - so we can find preceeding objects
 
596
    # to diff against
 
597
    offs = {}
 
598
    for i in range(len(magic)):
 
599
        offs[magic[i][4]] = i
 
600
    # Write the pack
 
601
    entries = []
 
602
    f = SHA1Writer(f)
 
603
    f.write("PACK")               # Pack header
 
604
    f.write(struct.pack(">L", 2)) # Pack version
 
605
    f.write(struct.pack(">L", num_objects)) # Number of objects in pack
 
606
    for o in recency:
 
607
        sha1 = o.sha().digest()
 
608
        crc32 = o.crc32()
 
609
        orig_t, raw = o.as_raw_string()
 
610
        winner = raw
 
611
        t = orig_t
 
612
        #for i in range(offs[o]-window, window):
 
613
        #    if i < 0 or i >= len(offs): continue
 
614
        #    b = magic[i][4]
 
615
        #    if b._num_type != orig_t: continue
 
616
        #    _, base = b.as_raw_string()
 
617
        #    delta = create_delta(base, raw)
 
618
        #    if len(delta) < len(winner):
 
619
        #        winner = delta
 
620
        #        t = 6 if magic[i][2] == 1 else 7
 
621
        offset = write_pack_object(f, t, winner)
 
622
        entries.append((sha1, offset, crc32))
 
623
    return entries, f.write_sha()
 
624
 
 
625
 
 
626
def write_pack_index_v1(filename, entries, pack_checksum):
 
627
    """Write a new pack index file.
 
628
 
 
629
    :param filename: The filename of the new pack index file.
 
630
    :param entries: List of tuples with object name (sha), offset_in_pack,  and
 
631
            crc32_checksum.
 
632
    :param pack_checksum: Checksum of the pack file.
 
633
    """
 
634
    f = open(filename, 'w')
 
635
    f = SHA1Writer(f)
 
636
    fan_out_table = defaultdict(lambda: 0)
 
637
    for (name, offset, entry_checksum) in entries:
 
638
        fan_out_table[ord(name[0])] += 1
 
639
    # Fan-out table
 
640
    for i in range(0x100):
 
641
        f.write(struct.pack(">L", fan_out_table[i]))
 
642
        fan_out_table[i+1] += fan_out_table[i]
 
643
    for (name, offset, entry_checksum) in entries:
 
644
        f.write(struct.pack(">L20s", offset, name))
 
645
    assert len(pack_checksum) == 20
 
646
    f.write(pack_checksum)
 
647
    f.close()
 
648
 
 
649
 
 
650
def create_delta(base_buf, target_buf):
 
651
    """Use python difflib to work out how to transform base_buf to target_buf"""
 
652
    assert isinstance(base_buf, str)
 
653
    assert isinstance(target_buf, str)
 
654
    out_buf = ""
 
655
    # write delta header
 
656
    def encode_size(size):
 
657
        ret = ""
 
658
        c = size & 0x7f
 
659
        size >>= 7
 
660
        while size:
 
661
            ret += chr(c | 0x80)
 
662
            c = size & 0x7f
 
663
            size >>= 7
 
664
        ret += chr(c)
 
665
        return ret
 
666
    out_buf += encode_size(len(base_buf))
 
667
    out_buf += encode_size(len(target_buf))
 
668
    # write out delta opcodes
 
669
    seq = difflib.SequenceMatcher(a=base_buf, b=target_buf)
 
670
    for opcode, i1, i2, j1, j2 in seq.get_opcodes():
 
671
        # Git patch opcodes don't care about deletes!
 
672
        #if opcode == "replace" or opcode == "delete":
 
673
        #    pass
 
674
        if opcode == "equal":
 
675
            # If they are equal, unpacker will use data from base_buf
 
676
            # Write out an opcode that says what range to use
 
677
            scratch = ""
 
678
            op = 0x80
 
679
            o = i1
 
680
            for i in range(4):
 
681
                if o & 0xff << i*8:
 
682
                    scratch += chr(o >> i)
 
683
                    op |= 1 << i
 
684
            s = i2 - i1
 
685
            for i in range(2):
 
686
                if s & 0xff << i*8:
 
687
                    scratch += chr(s >> i)
 
688
                    op |= 1 << (4+i)
 
689
            out_buf += chr(op)
 
690
            out_buf += scratch
 
691
        if opcode == "replace" or opcode == "insert":
 
692
            # If we are replacing a range or adding one, then we just
 
693
            # output it to the stream (prefixed by its size)
 
694
            s = j2 - j1
 
695
            o = j1
 
696
            while s > 127:
 
697
                out_buf += chr(127)
 
698
                out_buf += target_buf[o:o+127]
 
699
                s -= 127
 
700
                o += 127
 
701
            out_buf += chr(s)
 
702
            out_buf += target_buf[o:o+s]
 
703
    return out_buf
 
704
 
 
705
 
 
706
def apply_delta(src_buf, delta):
 
707
    """Based on the similar function in git's patch-delta.c."""
 
708
    assert isinstance(src_buf, str), "was %r" % (src_buf,)
 
709
    assert isinstance(delta, str)
 
710
    out = ""
 
711
    def pop(delta):
 
712
        ret = delta[0]
 
713
        delta = delta[1:]
 
714
        return ord(ret), delta
 
715
    def get_delta_header_size(delta):
 
716
        size = 0
 
717
        i = 0
 
718
        while delta:
 
719
            cmd, delta = pop(delta)
 
720
            size |= (cmd & ~0x80) << i
 
721
            i += 7
 
722
            if not cmd & 0x80:
 
723
                break
 
724
        return size, delta
 
725
    src_size, delta = get_delta_header_size(delta)
 
726
    dest_size, delta = get_delta_header_size(delta)
 
727
    assert src_size == len(src_buf), "%d vs %d" % (src_size, len(src_buf))
 
728
    while delta:
 
729
        cmd, delta = pop(delta)
 
730
        if cmd & 0x80:
 
731
            cp_off = 0
 
732
            for i in range(4):
 
733
                if cmd & (1 << i): 
 
734
                    x, delta = pop(delta)
 
735
                    cp_off |= x << (i * 8)
 
736
            cp_size = 0
 
737
            for i in range(3):
 
738
                if cmd & (1 << (4+i)): 
 
739
                    x, delta = pop(delta)
 
740
                    cp_size |= x << (i * 8)
 
741
            if cp_size == 0: 
 
742
                cp_size = 0x10000
 
743
            if (cp_off + cp_size < cp_size or
 
744
                cp_off + cp_size > src_size or
 
745
                cp_size > dest_size):
 
746
                break
 
747
            out += src_buf[cp_off:cp_off+cp_size]
 
748
        elif cmd != 0:
 
749
            out += delta[:cmd]
 
750
            delta = delta[cmd:]
 
751
        else:
 
752
            raise ApplyDeltaError("Invalid opcode 0")
 
753
    
 
754
    if delta != "":
 
755
        raise ApplyDeltaError("delta not empty: %r" % delta)
 
756
 
 
757
    if dest_size != len(out):
 
758
        raise ApplyDeltaError("dest size incorrect")
 
759
 
 
760
    return out
 
761
 
 
762
 
 
763
def write_pack_index_v2(filename, entries, pack_checksum):
 
764
    """Write a new pack index file.
 
765
 
 
766
    :param filename: The filename of the new pack index file.
 
767
    :param entries: List of tuples with object name (sha), offset_in_pack,  and
 
768
            crc32_checksum.
 
769
    :param pack_checksum: Checksum of the pack file.
 
770
    """
 
771
    f = open(filename, 'w')
 
772
    f = SHA1Writer(f)
 
773
    f.write('\377tOc') # Magic!
 
774
    f.write(struct.pack(">L", 2))
 
775
    fan_out_table = defaultdict(lambda: 0)
 
776
    for (name, offset, entry_checksum) in entries:
 
777
        fan_out_table[ord(name[0])] += 1
 
778
    # Fan-out table
 
779
    for i in range(0x100):
 
780
        f.write(struct.pack(">L", fan_out_table[i]))
 
781
        fan_out_table[i+1] += fan_out_table[i]
 
782
    for (name, offset, entry_checksum) in entries:
 
783
        f.write(name)
 
784
    for (name, offset, entry_checksum) in entries:
 
785
        f.write(struct.pack(">l", entry_checksum))
 
786
    for (name, offset, entry_checksum) in entries:
 
787
        # FIXME: handle if MSBit is set in offset
 
788
        f.write(struct.pack(">L", offset))
 
789
    # FIXME: handle table for pack files > 8 Gb
 
790
    assert len(pack_checksum) == 20
 
791
    f.write(pack_checksum)
 
792
    f.close()
 
793
 
 
794
 
 
795
class Pack(object):
 
796
 
 
797
    def __init__(self, basename):
 
798
        self._basename = basename
 
799
        self._data_path = self._basename + ".pack"
 
800
        self._idx_path = self._basename + ".idx"
 
801
        self._data = None
 
802
        self._idx = None
 
803
 
 
804
    def name(self):
 
805
        return self.idx.objects_sha1()
 
806
 
 
807
    @property
 
808
    def data(self):
 
809
        if self._data is None:
 
810
            self._data = PackData(self._data_path)
 
811
            assert len(self.idx) == len(self._data)
 
812
            assert self.idx.get_stored_checksums()[0] == self._data.get_stored_checksum()
 
813
        return self._data
 
814
 
 
815
    @property
 
816
    def idx(self):
 
817
        if self._idx is None:
 
818
            self._idx = PackIndex(self._idx_path)
 
819
        return self._idx
 
820
 
 
821
    def close(self):
 
822
        if self._data is not None:
 
823
            self._data.close()
 
824
        self.idx.close()
 
825
 
 
826
    def __eq__(self, other):
 
827
        return type(self) == type(other) and self.idx == other.idx
 
828
 
 
829
    def __len__(self):
 
830
        """Number of entries in this pack."""
 
831
        return len(self.idx)
 
832
 
 
833
    def __repr__(self):
 
834
        return "Pack(%r)" % self._basename
 
835
 
 
836
    def __iter__(self):
 
837
        """Iterate over all the sha1s of the objects in this pack."""
 
838
        return iter(self.idx)
 
839
 
 
840
    def check(self):
 
841
        return self.idx.check() and self.data.check()
 
842
 
 
843
    def get_stored_checksum(self):
 
844
        return self.data.get_stored_checksum()
 
845
 
 
846
    def __contains__(self, sha1):
 
847
        """Check whether this pack contains a particular SHA1."""
 
848
        return (self.idx.object_index(sha1) is not None)
 
849
 
 
850
    def get_raw(self, sha1, resolve_ref=None):
 
851
        if resolve_ref is None:
 
852
            resolve_ref = self.get_raw
 
853
        offset = self.idx.object_index(sha1)
 
854
        if offset is None:
 
855
            raise KeyError(sha1)
 
856
 
 
857
        type, obj = self.data.get_object_at(offset)
 
858
        assert isinstance(offset, int)
 
859
        return resolve_object(offset, type, obj, resolve_ref,
 
860
            self.data.get_object_at)
 
861
 
 
862
    def __getitem__(self, sha1):
 
863
        """Retrieve the specified SHA1."""
 
864
        type, uncomp = self.get_raw(sha1)
 
865
        return ShaFile.from_raw_string(type, uncomp)
 
866
 
 
867
    def iterobjects(self):
 
868
        for offset, type, obj in self.data.iterobjects():
 
869
            assert isinstance(offset, int)
 
870
            yield ShaFile.from_raw_string(
 
871
                    *resolve_object(offset, type, obj, self.get_raw, 
 
872
                self.data.get_object_at))
 
873
 
 
874
 
 
875
def load_packs(path):
 
876
    if not os.path.exists(path):
 
877
        return
 
878
    for name in os.listdir(path):
 
879
        if name.startswith("pack-") and name.endswith(".pack"):
 
880
            yield Pack(os.path.join(path, name[:-len(".pack")]))
 
881