~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/revfile.py

merge David Clymer's patch for TestCaseInTestDir.runcmd

Show diffs side-by-side

added added

removed removed

Lines of Context:
52
52
is that sequence numbers are stable references.  But not every
53
53
repository in the world will assign the same sequence numbers,
54
54
therefore the SHA-1 is the only universally unique reference.
 
55
 
 
56
This is meant to scale to hold 100,000 revisions of a single file, by
 
57
which time the index file will be ~4.8MB and a bit big to read
 
58
sequentially.
 
59
 
 
60
Some of the reserved fields could be used to implement a (semi?)
 
61
balanced tree indexed by SHA1 so we can much more efficiently find the
 
62
index associated with a particular hash.  For 100,000 revs we would be
 
63
able to find it in about 17 random reads, which is not too bad.
 
64
 
 
65
This performs pretty well except when trying to calculate deltas of
 
66
really large files.  For that the main thing would be to plug in
 
67
something faster than difflib, which is after all pure Python.
 
68
Another approach is to just store the gzipped full text of big files,
 
69
though perhaps that's too perverse?
 
70
 
55
71
The iter method here will generally read through the whole index file
56
72
in one go.  With readahead in the kernel and python/libc (typically
57
73
128kB) this means that there should be no seeks and often only one
73
89
# if there are gaps and that can happen if we're interrupted while
74
90
# writing to the datafile.  Overlapping would be very bad though.
75
91
 
76
 
 
 
92
# TODO: Shouldn't need to lock if we always write in append mode and
 
93
# then ftell after writing to see where it went.  In any case we
 
94
# assume the whole branch is protected by a lock.
77
95
 
78
96
import sys, zlib, struct, mdiff, stat, os, sha
79
97
from binascii import hexlify, unhexlify
80
98
 
81
 
factor = 10
82
 
 
83
99
_RECORDSIZE = 48
84
100
 
85
101
_HEADER = "bzr revfile v1\n"
96
112
FL_GZIP = 1
97
113
 
98
114
# maximum number of patches in a row before recording a whole text.
99
 
CHAIN_LIMIT = 50
 
115
CHAIN_LIMIT = 10
100
116
 
101
117
 
102
118
class RevfileError(Exception):
105
121
class LimitHitException(Exception):
106
122
    pass
107
123
 
108
 
class Revfile:
 
124
class Revfile(object):
109
125
    def __init__(self, basename, mode):
110
126
        # TODO: Lock file  while open
111
127
 
133
149
            self.idxfile = open(idxname, 'w+b')
134
150
            self.datafile = open(dataname, 'w+b')
135
151
            
136
 
            print 'init empty file'
137
152
            self.idxfile.write(_HEADER)
138
153
            self.idxfile.flush()
139
154
        else:
227
242
        return self._add_compressed(text_sha, text, _NO_RECORD, compress)
228
243
 
229
244
 
 
245
    # NOT USED
 
246
    def _choose_base(self, seed, base):
 
247
        while seed & 3 == 3:
 
248
            if base == _NO_RECORD:
 
249
                return _NO_RECORD
 
250
            idxrec = self[base]
 
251
            if idxrec[I_BASE] == _NO_RECORD:
 
252
                return base
 
253
 
 
254
            base = idxrec[I_BASE]
 
255
            seed >>= 2
 
256
                
 
257
        return base        # relative to this full text
 
258
        
 
259
 
 
260
 
230
261
    def _add_delta(self, text, text_sha, base, compress):
231
262
        """Add a text stored relative to a previous text."""
232
263
        self._check_index(base)
233
 
        
 
264
 
234
265
        try:
235
 
            base_text = self.get(base, recursion_limit=CHAIN_LIMIT)
 
266
            base_text = self.get(base, CHAIN_LIMIT)
236
267
        except LimitHitException:
237
268
            return self._add_full_text(text, text_sha, compress)
238
269
        
239
270
        data = mdiff.bdiff(base_text, text)
 
271
 
 
272
 
 
273
        if True: # paranoid early check for bad diff
 
274
            result = mdiff.bpatch(base_text, data)
 
275
            assert result == text
 
276
            
240
277
        
241
278
        # If the delta is larger than the text, we might as well just
242
279
        # store the text.  (OK, the delta might be more compressible,
248
285
            return self._add_compressed(text_sha, data, base, compress)
249
286
 
250
287
 
251
 
    def add(self, text, base=_NO_RECORD, compress=True):
 
288
    def add(self, text, base=None, compress=True):
252
289
        """Add a new text to the revfile.
253
290
 
254
291
        If the text is already present them its existing id is
262
299
        only be used if it would be a size win and if the existing
263
300
        base is not at too long of a delta chain already.
264
301
        """
 
302
        if base == None:
 
303
            base = _NO_RECORD
 
304
        
265
305
        self._check_write()
266
306
        
267
307
        text_sha = sha.new(text).digest()
272
312
            # it's the same, in case someone ever breaks SHA-1.
273
313
            return idx                  # already present
274
314
        
 
315
        # base = self._choose_base(ord(text_sha[0]), base)
 
316
 
275
317
        if base == _NO_RECORD:
276
318
            return self._add_full_text(text, text_sha, compress)
277
319
        else:
296
338
            text = self._get_patched(idx, idxrec, recursion_limit)
297
339
 
298
340
        if sha.new(text).digest() != idxrec[I_SHA]:
299
 
            raise RevfileError("corrupt SHA-1 digest on record %d"
300
 
                               % idx)
 
341
            raise RevfileError("corrupt SHA-1 digest on record %d in %s"
 
342
                               % (idx, self.basename))
301
343
 
302
344
        return text
303
345
 
372
414
        self._seek_index(idx)
373
415
        idxrec = self._read_next_index()
374
416
        if idxrec == None:
375
 
            raise IndexError()
 
417
            raise IndexError("no index %d" % idx)
376
418
        else:
377
419
            return idxrec
378
420
 
388
430
        """Read back all index records.
389
431
 
390
432
        Do not seek the index file while this is underway!"""
391
 
        sys.stderr.write(" ** iter called ** \n")
 
433
        ## sys.stderr.write(" ** iter called ** \n")
392
434
        self._seek_index(0)
393
435
        while True:
394
436
            idxrec = self._read_next_index()
436
478
        for idx in range(len(self)):
437
479
            t += len(self.get(idx))
438
480
        return t
 
481
 
 
482
 
 
483
    def check(self, pb=None):
 
484
        """Extract every version and check its hash."""
 
485
        total = len(self)
 
486
        for i in range(total):
 
487
            if pb:
 
488
                pb.update("check revision", i, total)
 
489
            # the get method implicitly checks the SHA-1
 
490
            self.get(i)
 
491
        if pb:
 
492
            pb.clear()
439
493
        
440
494
 
441
495
 
442
496
def main(argv):
443
497
    try:
444
498
        cmd = argv[1]
 
499
        filename = argv[2]
445
500
    except IndexError:
446
 
        sys.stderr.write("usage: revfile dump\n"
447
 
                         "       revfile add\n"
448
 
                         "       revfile add-delta BASE\n"
449
 
                         "       revfile get IDX\n"
450
 
                         "       revfile find-sha HEX\n"
451
 
                         "       revfile total-text-size\n"
452
 
                         "       revfile last\n")
 
501
        sys.stderr.write("usage: revfile dump REVFILE\n"
 
502
                         "       revfile add REVFILE < INPUT\n"
 
503
                         "       revfile add-delta REVFILE BASE < INPUT\n"
 
504
                         "       revfile add-series REVFILE BASE FILE...\n"
 
505
                         "       revfile get REVFILE IDX\n"
 
506
                         "       revfile find-sha REVFILE HEX\n"
 
507
                         "       revfile total-text-size REVFILE\n"
 
508
                         "       revfile last REVFILE\n")
453
509
        return 1
454
510
 
 
511
    if filename.endswith('.drev') or filename.endswith('.irev'):
 
512
        filename = filename[:-5]
 
513
 
455
514
    def rw():
456
 
        return Revfile('testrev', 'w')
 
515
        return Revfile(filename, 'w')
457
516
 
458
517
    def ro():
459
 
        return Revfile('testrev', 'r')
 
518
        return Revfile(filename, 'r')
460
519
 
461
520
    if cmd == 'add':
462
521
        print rw().add(sys.stdin.read())
463
522
    elif cmd == 'add-delta':
464
 
        print rw().add(sys.stdin.read(), int(argv[2]))
 
523
        print rw().add(sys.stdin.read(), int(argv[3]))
 
524
    elif cmd == 'add-series':
 
525
        r = rw()
 
526
        rev = int(argv[3])
 
527
        for fn in argv[4:]:
 
528
            print rev
 
529
            rev = r.add(file(fn).read(), rev)
465
530
    elif cmd == 'dump':
466
531
        ro().dump()
467
532
    elif cmd == 'get':
468
533
        try:
469
 
            idx = int(argv[2])
 
534
            idx = int(argv[3])
470
535
        except IndexError:
471
 
            sys.stderr.write("usage: revfile get IDX\n")
 
536
            sys.stderr.write("usage: revfile get FILE IDX\n")
472
537
            return 1
473
538
 
 
539
        r = ro()
 
540
 
474
541
        if idx < 0 or idx >= len(r):
475
542
            sys.stderr.write("invalid index %r\n" % idx)
476
543
            return 1
477
544
 
478
 
        sys.stdout.write(ro().get(idx))
 
545
        sys.stdout.write(r.get(idx))
479
546
    elif cmd == 'find-sha':
480
547
        try:
481
 
            s = unhexlify(argv[2])
 
548
            s = unhexlify(argv[3])
482
549
        except IndexError:
483
 
            sys.stderr.write("usage: revfile find-sha HEX\n")
 
550
            sys.stderr.write("usage: revfile find-sha FILE HEX\n")
484
551
            return 1
485
552
 
486
553
        idx = ro().find_sha(s)
493
560
        print ro().total_text_size()
494
561
    elif cmd == 'last':
495
562
        print len(ro())-1
 
563
    elif cmd == 'check':
 
564
        import bzrlib.progress
 
565
        pb = bzrlib.progress.ProgressBar()
 
566
        ro().check(pb)
496
567
    else:
497
568
        sys.stderr.write("unknown command %r\n" % cmd)
498
569
        return 1