/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 cache.py

Special-case NULL_REVISION when looking for Git shas.

Show diffs side-by-side

added added

removed removed

Lines of Context:
23
23
import os
24
24
import threading
25
25
 
 
26
from dulwich.objects import (
 
27
    ShaFile,
 
28
    )
 
29
 
26
30
import bzrlib
27
31
from bzrlib import (
 
32
    btree_index as _mod_btree_index,
 
33
    index as _mod_index,
 
34
    osutils,
28
35
    registry,
29
36
    trace,
 
37
    versionedfile,
30
38
    )
31
39
from bzrlib.transport import (
32
40
    get_transport,
135
143
class ContentCache(object):
136
144
    """Object that can cache Git objects."""
137
145
 
 
146
    def add(self, object):
 
147
        """Add an object."""
 
148
        raise NotImplementedError(self.add)
 
149
 
 
150
    def add_multi(self, objects):
 
151
        """Add multiple objects."""
 
152
        for obj in objects:
 
153
            self.add(obj)
 
154
 
138
155
    def __getitem__(self, sha):
139
156
        """Retrieve an item, by SHA."""
140
157
        raise NotImplementedError(self.__getitem__)
141
158
 
142
159
 
143
160
class BzrGitCacheFormat(object):
 
161
    """Bazaar-Git Cache Format."""
144
162
 
145
163
    def get_format_string(self):
 
164
        """Return a single-line unique format string for this cache format."""
146
165
        raise NotImplementedError(self.get_format_string)
147
166
 
148
167
    def open(self, transport):
 
168
        """Open this format on a transport."""
149
169
        raise NotImplementedError(self.open)
150
170
 
151
171
    def initialize(self, transport):
 
172
        """Create a new instance of this cache format at transport."""
152
173
        transport.put_bytes('format', self.get_format_string())
153
174
 
154
175
    @classmethod
155
 
    def from_repository(self, repository):
156
 
        repo_transport = getattr(repository, "_transport", None)
157
 
        if repo_transport is not None:
158
 
            try:
159
 
                repo_transport.mkdir('git')
160
 
            except bzrlib.errors.FileExists:
161
 
                pass
162
 
            transport = repo_transport.clone('git')
163
 
        else:
164
 
            transport = get_remote_cache_transport()
 
176
    def from_transport(self, transport):
 
177
        """Open a cache file present on a transport, or initialize one.
 
178
 
 
179
        :param transport: Transport to use
 
180
        :return: A BzrGitCache instance
 
181
        """
165
182
        try:
166
183
            format_name = transport.get_bytes('format')
167
184
            format = formats.get(format_name)
170
187
            format.initialize(transport)
171
188
        return format.open(transport)
172
189
 
 
190
    @classmethod
 
191
    def from_repository(cls, repository):
 
192
        """Open a cache file for a repository.
 
193
 
 
194
        This will use the repository's transport to store the cache file, or
 
195
        use the users global cache directory if the repository has no 
 
196
        transport associated with it.
 
197
 
 
198
        :param repository: Repository to open the cache for
 
199
        :return: A `BzrGitCache`
 
200
        """
 
201
        repo_transport = getattr(repository, "_transport", None)
 
202
        if repo_transport is not None:
 
203
            # Even if we don't write to this repo, we should be able 
 
204
            # to update its cache.
 
205
            repo_transport = remove_readonly_transport_decorator(repo_transport)
 
206
            try:
 
207
                repo_transport.mkdir('git')
 
208
            except bzrlib.errors.FileExists:
 
209
                pass
 
210
            transport = repo_transport.clone('git')
 
211
        else:
 
212
            transport = get_remote_cache_transport()
 
213
        return cls.from_transport(transport)
 
214
 
173
215
 
174
216
class CacheUpdater(object):
 
217
    """Base class for objects that can update a bzr-git cache."""
175
218
 
176
 
    def add_object(self, obj, ie):
 
219
    def add_object(self, obj, ie, path):
177
220
        raise NotImplementedError(self.add_object)
178
221
 
179
222
    def finish(self):
189
232
        self._cache_updater_klass = cache_updater_klass
190
233
 
191
234
    def get_updater(self, rev):
 
235
        """Update an object that implements the CacheUpdater interface for 
 
236
        updating this cache.
 
237
        """
192
238
        return self._cache_updater_klass(self, rev)
193
239
 
194
240
 
196
242
 
197
243
 
198
244
class DictCacheUpdater(CacheUpdater):
 
245
    """Cache updater for dict-based caches."""
199
246
 
200
247
    def __init__(self, cache, rev):
201
248
        self.cache = cache
204
251
        self._commit = None
205
252
        self._entries = []
206
253
 
207
 
    def add_object(self, obj, ie):
 
254
    def add_object(self, obj, ie, path):
208
255
        if obj.type_name == "commit":
209
256
            self._commit = obj
210
257
            assert ie is None
211
258
            type_data = (self.revid, self._commit.tree)
 
259
            self.cache.idmap._by_revid[self.revid] = obj.id
212
260
        elif obj.type_name in ("blob", "tree"):
213
 
            if obj.type_name == "blob":
214
 
                revision = ie.revision
215
 
            else:
216
 
                revision = self.revid
217
 
            type_data = (ie.file_id, revision)
218
 
            self.cache.idmap._by_fileid.setdefault(type_data[1], {})[type_data[0]] = obj.id
 
261
            if ie is not None:
 
262
                if obj.type_name == "blob":
 
263
                    revision = ie.revision
 
264
                else:
 
265
                    revision = self.revid
 
266
                type_data = (ie.file_id, revision)
 
267
                self.cache.idmap._by_fileid.setdefault(type_data[1], {})[type_data[0]] =\
 
268
                    obj.id
219
269
        else:
220
270
            raise AssertionError
221
271
        self.cache.idmap._by_sha[obj.id] = (obj.type_name, type_data)
227
277
 
228
278
 
229
279
class DictGitShaMap(GitShaMap):
 
280
    """Git SHA map that uses a dictionary."""
230
281
 
231
282
    def __init__(self):
232
283
        self._by_sha = {}
233
284
        self._by_fileid = {}
 
285
        self._by_revid = {}
234
286
 
235
287
    def lookup_blob_id(self, fileid, revision):
236
288
        return self._by_fileid[revision][fileid]
239
291
        return self._by_sha[sha]
240
292
 
241
293
    def lookup_tree_id(self, fileid, revision):
242
 
        return self._base._by_fileid[revision][fileid]
 
294
        return self._by_fileid[revision][fileid]
 
295
 
 
296
    def lookup_commit(self, revid):
 
297
        return self._by_revid[revid]
243
298
 
244
299
    def revids(self):
245
300
        for key, (type, type_data) in self._by_sha.iteritems():
260
315
        self._trees = []
261
316
        self._blobs = []
262
317
 
263
 
    def add_object(self, obj, ie):
 
318
    def add_object(self, obj, ie, path):
264
319
        if obj.type_name == "commit":
265
320
            self._commit = obj
266
321
            assert ie is None
267
322
        elif obj.type_name == "tree":
268
 
            self._trees.append((obj.id, ie.file_id, self.revid))
 
323
            if ie is not None:
 
324
                self._trees.append((obj.id, ie.file_id, self.revid))
269
325
        elif obj.type_name == "blob":
270
 
            self._blobs.append((obj.id, ie.file_id, ie.revision))
 
326
            if ie is not None:
 
327
                self._blobs.append((obj.id, ie.file_id, ie.revision))
271
328
        else:
272
329
            raise AssertionError
273
330
 
303
360
 
304
361
 
305
362
class SqliteGitShaMap(GitShaMap):
 
363
    """Bazaar GIT Sha map that uses a sqlite database for storage."""
306
364
 
307
365
    def __init__(self, path=None):
308
366
        self.path = path
341
399
        return "%s(%r)" % (self.__class__.__name__, self.path)
342
400
    
343
401
    def lookup_commit(self, revid):
344
 
        row = self.db.execute("select sha1 from commits where revid = ?", (revid,)).fetchone()
 
402
        cursor = self.db.execute("select sha1 from commits where revid = ?", 
 
403
            (revid,))
 
404
        row = cursor.fetchone()
345
405
        if row is not None:
346
406
            return row[0]
347
407
        raise KeyError
356
416
        raise KeyError(fileid)
357
417
 
358
418
    def lookup_tree_id(self, fileid, revision):
359
 
        row = self.db.execute("select sha1 from trees where fileid = ? and revid = ?", (fileid, self.revid)).fetchone()
 
419
        row = self.db.execute("select sha1 from trees where fileid = ? and revid = ?", (fileid, revision)).fetchone()
360
420
        if row is not None:
361
421
            return row[0]
362
422
        raise KeyError(fileid)
391
451
 
392
452
 
393
453
class TdbCacheUpdater(CacheUpdater):
 
454
    """Cache updater for tdb-based caches."""
394
455
 
395
456
    def __init__(self, cache, rev):
396
457
        self.cache = cache
400
461
        self._commit = None
401
462
        self._entries = []
402
463
 
403
 
    def add_object(self, obj, ie):
 
464
    def add_object(self, obj, ie, path):
404
465
        sha = obj.sha().digest()
405
466
        if obj.type_name == "commit":
406
 
            self.db["commit\0" + self.revid] = "\0".join((obj.id, obj.tree))
 
467
            self.db["commit\0" + self.revid] = "\0".join((sha, obj.tree))
407
468
            type_data = (self.revid, obj.tree)
408
469
            self._commit = obj
409
470
            assert ie is None
410
471
        elif obj.type_name == "blob":
 
472
            if ie is None:
 
473
                return
411
474
            self.db["\0".join(("blob", ie.file_id, ie.revision))] = sha
412
475
            type_data = (ie.file_id, ie.revision)
413
476
        elif obj.type_name == "tree":
 
477
            if ie is None:
 
478
                return
414
479
            type_data = (ie.file_id, self.revid)
415
480
        else:
416
481
            raise AssertionError
417
 
        self.db["git\0" + sha] = "\0".join((obj.type_name,
418
 
            type_data[0], type_data[1]))
 
482
        self.db["git\0" + sha] = "\0".join((obj.type_name, ) + type_data)
419
483
 
420
484
    def finish(self):
421
485
        if self._commit is None:
426
490
TdbBzrGitCache = lambda p: BzrGitCache(TdbGitShaMap(p), None, TdbCacheUpdater)
427
491
 
428
492
class TdbGitCacheFormat(BzrGitCacheFormat):
 
493
    """Cache format for tdb-based caches."""
429
494
 
430
495
    def get_format_string(self):
431
496
        return 'bzr-git sha map version 3 using tdb\n'
529
594
                yield sha_to_hex(key[4:])
530
595
 
531
596
 
 
597
class VersionedFilesContentCache(ContentCache):
 
598
 
 
599
    def __init__(self, vf):
 
600
        self._vf = vf
 
601
 
 
602
    def add(self, obj):
 
603
        self._vf.insert_record_stream(
 
604
            [versionedfile.ChunkedContentFactory((obj.id,), [], None,
 
605
                obj.as_legacy_object_chunks())])
 
606
 
 
607
    def __getitem__(self, sha):
 
608
        stream = self._vf.get_record_stream([(sha,)], 'unordered', True)
 
609
        entry = stream.next() 
 
610
        if entry.storage_kind == 'absent':
 
611
            raise KeyError(sha)
 
612
        return ShaFile._parse_legacy_object(entry.get_bytes_as('fulltext'))
 
613
 
 
614
 
 
615
class GitObjectStoreContentCache(ContentCache):
 
616
 
 
617
    def __init__(self, store):
 
618
        self.store = store
 
619
 
 
620
    def add_multi(self, objs):
 
621
        self.store.add_objects(objs)
 
622
 
 
623
    def add(self, obj, path):
 
624
        self.store.add_object(obj)
 
625
 
 
626
    def __getitem__(self, sha):
 
627
        return self.store[sha]
 
628
 
 
629
 
 
630
class IndexCacheUpdater(CacheUpdater):
 
631
 
 
632
    def __init__(self, cache, rev):
 
633
        self.cache = cache
 
634
        self.revid = rev.revision_id
 
635
        self.parent_revids = rev.parent_ids
 
636
        self._commit = None
 
637
        self._entries = []
 
638
        self._cache_objs = set()
 
639
 
 
640
    def add_object(self, obj, ie, path):
 
641
        if obj.type_name == "commit":
 
642
            self._commit = obj
 
643
            assert ie is None
 
644
            self.cache.idmap._add_git_sha(obj.id, "commit",
 
645
                (self.revid, obj.tree))
 
646
            self.cache.idmap._add_node(("commit", self.revid, "X"),
 
647
                " ".join((obj.id, obj.tree)))
 
648
            self._cache_objs.add((obj, path))
 
649
        elif obj.type_name == "blob":
 
650
            self.cache.idmap._add_git_sha(obj.id, "blob",
 
651
                (ie.file_id, ie.revision))
 
652
            self.cache.idmap._add_node(("blob", ie.file_id, ie.revision), obj.id)
 
653
            if ie.kind == "symlink":
 
654
                self._cache_objs.add((obj, path))
 
655
        elif obj.type_name == "tree":
 
656
            self.cache.idmap._add_git_sha(obj.id, "tree",
 
657
                (ie.file_id, self.revid))
 
658
            self._cache_objs.add((obj, path))
 
659
        else:
 
660
            raise AssertionError
 
661
 
 
662
    def finish(self):
 
663
        self.cache.content_cache.add_multi(self._cache_objs)
 
664
        return self._commit
 
665
 
 
666
 
 
667
class IndexBzrGitCache(BzrGitCache):
 
668
 
 
669
    def __init__(self, transport=None):
 
670
        mapper = versionedfile.ConstantMapper("trees")
 
671
        shamap = IndexGitShaMap(transport.clone('index'))
 
672
        #trees_store = knit.make_file_factory(True, mapper)(transport)
 
673
        #content_cache = VersionedFilesContentCache(trees_store)
 
674
        from bzrlib.plugins.git.transportgit import TransportObjectStore
 
675
        store = TransportObjectStore(transport.clone('objects'))
 
676
        content_cache = GitObjectStoreContentCache(store)
 
677
        super(IndexBzrGitCache, self).__init__(shamap, content_cache,
 
678
                IndexCacheUpdater)
 
679
 
 
680
 
 
681
class IndexGitCacheFormat(BzrGitCacheFormat):
 
682
 
 
683
    def get_format_string(self):
 
684
        return 'bzr-git sha map with git object cache version 1\n'
 
685
 
 
686
    def initialize(self, transport):
 
687
        super(IndexGitCacheFormat, self).initialize(transport)
 
688
        transport.mkdir('index')
 
689
        transport.mkdir('objects')
 
690
        from bzrlib.plugins.git.transportgit import TransportObjectStore
 
691
        TransportObjectStore.init(transport.clone('objects'))
 
692
 
 
693
    def open(self, transport):
 
694
        return IndexBzrGitCache(transport)
 
695
 
 
696
 
 
697
class IndexGitShaMap(GitShaMap):
 
698
    """SHA Map that uses the Bazaar APIs to store a cache.
 
699
 
 
700
    BTree Index file with the following contents:
 
701
 
 
702
    ("git", <sha1>) -> "<type> <type-data1> <type-data2>"
 
703
    ("commit", <revid>) -> "<sha1> <tree-id>"
 
704
    ("blob", <fileid>, <revid>) -> <sha1>
 
705
 
 
706
    """
 
707
 
 
708
    def __init__(self, transport=None):
 
709
        if transport is None:
 
710
            self._transport = None
 
711
            self._index = _mod_index.InMemoryGraphIndex(0, key_elements=3)
 
712
            self._builder = self._index
 
713
        else:
 
714
            self._builder = None
 
715
            self._transport = transport
 
716
            self._index = _mod_index.CombinedGraphIndex([])
 
717
            for name in self._transport.list_dir("."):
 
718
                if not name.endswith(".rix"):
 
719
                    continue
 
720
                x = _mod_btree_index.BTreeGraphIndex(self._transport, name,
 
721
                    self._transport.stat(name).st_size)
 
722
                self._index.insert_index(0, x)
 
723
 
 
724
    @classmethod
 
725
    def from_repository(cls, repository):
 
726
        transport = getattr(repository, "_transport", None)
 
727
        if transport is not None:
 
728
            try:
 
729
                transport.mkdir('git')
 
730
            except bzrlib.errors.FileExists:
 
731
                pass
 
732
            return cls(transport.clone('git'))
 
733
        from bzrlib.transport import get_transport
 
734
        return cls(get_transport(get_cache_dir()))
 
735
 
 
736
    def __repr__(self):
 
737
        if self._transport is not None:
 
738
            return "%s(%r)" % (self.__class__.__name__, self._transport.base)
 
739
        else:
 
740
            return "%s()" % (self.__class__.__name__)
 
741
 
 
742
    def repack(self):
 
743
        assert self._builder is None
 
744
        self.start_write_group()
 
745
        for _, key, value in self._index.iter_all_entries():
 
746
            self._builder.add_node(key, value)
 
747
        to_remove = []
 
748
        for name in self._transport.list_dir('.'):
 
749
            if name.endswith('.rix'):
 
750
                to_remove.append(name)
 
751
        self.commit_write_group()
 
752
        del self._index.indices[1:]
 
753
        for name in to_remove:
 
754
            self._transport.rename(name, name + '.old')
 
755
 
 
756
    def start_write_group(self):
 
757
        assert self._builder is None
 
758
        self._builder = _mod_btree_index.BTreeBuilder(0, key_elements=3)
 
759
        self._name = osutils.sha()
 
760
 
 
761
    def commit_write_group(self):
 
762
        assert self._builder is not None
 
763
        stream = self._builder.finish()
 
764
        name = self._name.hexdigest() + ".rix"
 
765
        size = self._transport.put_file(name, stream)
 
766
        index = _mod_btree_index.BTreeGraphIndex(self._transport, name, size)
 
767
        self._index.insert_index(0, index)
 
768
        self._builder = None
 
769
        self._name = None
 
770
 
 
771
    def abort_write_group(self):
 
772
        assert self._builder is not None
 
773
        self._builder = None
 
774
        self._name = None
 
775
 
 
776
    def _add_node(self, key, value):
 
777
        try:
 
778
            self._builder.add_node(key, value)
 
779
        except bzrlib.errors.BadIndexDuplicateKey:
 
780
            # Multiple bzr objects can have the same contents
 
781
            return True
 
782
        else:
 
783
            return False
 
784
 
 
785
    def _get_entry(self, key):
 
786
        entries = self._index.iter_entries([key])
 
787
        try:
 
788
            return entries.next()[2]
 
789
        except StopIteration:
 
790
            if self._builder is None:
 
791
                raise KeyError
 
792
            entries = self._builder.iter_entries([key])
 
793
            try:
 
794
                return entries.next()[2]
 
795
            except StopIteration:
 
796
                raise KeyError
 
797
 
 
798
    def _iter_keys_prefix(self, prefix):
 
799
        for entry in self._index.iter_entries_prefix([prefix]):
 
800
            yield entry[1]
 
801
        if self._builder is not None:
 
802
            for entry in self._builder.iter_entries_prefix([prefix]):
 
803
                yield entry[1]
 
804
 
 
805
    def lookup_commit(self, revid):
 
806
        return self._get_entry(("commit", revid, "X"))[:40]
 
807
 
 
808
    def _add_git_sha(self, hexsha, type, type_data):
 
809
        if hexsha is not None:
 
810
            self._name.update(hexsha)
 
811
            self._add_node(("git", hexsha, "X"),
 
812
                " ".join((type, type_data[0], type_data[1])))
 
813
        else:
 
814
            # This object is not represented in Git - perhaps an empty
 
815
            # directory?
 
816
            self._name.update(type + " ".join(type_data))
 
817
 
 
818
    def lookup_blob_id(self, fileid, revision):
 
819
        return self._get_entry(("blob", fileid, revision))
 
820
 
 
821
    def lookup_git_sha(self, sha):
 
822
        if len(sha) == 20:
 
823
            sha = sha_to_hex(sha)
 
824
        data = self._get_entry(("git", sha, "X")).split(" ", 2)
 
825
        return (data[0], (data[1], data[2]))
 
826
 
 
827
    def revids(self):
 
828
        """List the revision ids known."""
 
829
        for key in self._iter_keys_prefix(("commit", None, None)):
 
830
            yield key[1]
 
831
 
 
832
    def missing_revisions(self, revids):
 
833
        """Return set of all the revisions that are not present."""
 
834
        missing_revids = set(revids)
 
835
        for _, key, value in self._index.iter_entries((
 
836
            ("commit", revid, "X") for revid in revids)):
 
837
            missing_revids.remove(key[1])
 
838
        return missing_revids
 
839
 
 
840
    def sha1s(self):
 
841
        """List the SHA1s."""
 
842
        for key in self._iter_keys_prefix(("git", None, None)):
 
843
            yield key[1]
 
844
 
 
845
 
532
846
formats = registry.Registry()
533
847
formats.register(TdbGitCacheFormat().get_format_string(),
534
848
    TdbGitCacheFormat())
535
849
formats.register(SqliteGitCacheFormat().get_format_string(),
536
850
    SqliteGitCacheFormat())
 
851
formats.register(IndexGitCacheFormat().get_format_string(),
 
852
    IndexGitCacheFormat())
 
853
# In the future, this will become the default:
 
854
# formats.register('default', IndexGitCacheFormat())
537
855
try:
538
856
    import tdb
539
857
except ImportError:
542
860
    formats.register('default', TdbGitCacheFormat())
543
861
 
544
862
 
 
863
 
545
864
def migrate_ancient_formats(repo_transport):
546
 
    if repo_transport.has("git.tdb"):
 
865
    # Prefer migrating git.db over git.tdb, since the latter may not 
 
866
    # be openable on some platforms.
 
867
    if repo_transport.has("git.db"):
 
868
        SqliteGitCacheFormat().initialize(repo_transport.clone("git"))
 
869
        repo_transport.rename("git.db", "git/idmap.db")
 
870
    elif repo_transport.has("git.tdb"):
547
871
        TdbGitCacheFormat().initialize(repo_transport.clone("git"))
548
872
        repo_transport.rename("git.tdb", "git/idmap.tdb")
549
 
    elif repo_transport.has("git.db"):
550
 
        SqliteGitCacheFormat().initialize(repo_transport.clone("git"))
551
 
        repo_transport.rename("git.db", "git/idmap.db")
 
873
 
 
874
 
 
875
def remove_readonly_transport_decorator(transport):
 
876
    if transport.is_readonly():
 
877
        return transport._decorated
 
878
    return transport
552
879
 
553
880
 
554
881
def from_repository(repository):
 
882
    """Open a cache file for a repository.
 
883
 
 
884
    If the repository is remote and there is no transport available from it
 
885
    this will use a local file in the users cache directory
 
886
    (typically ~/.cache/bazaar/git/)
 
887
 
 
888
    :param repository: A repository object
 
889
    """
555
890
    repo_transport = getattr(repository, "_transport", None)
556
891
    if repo_transport is not None:
557
892
        # Migrate older cache formats
 
893
        repo_transport = remove_readonly_transport_decorator(repo_transport)
558
894
        try:
559
895
            repo_transport.mkdir("git")
560
896
        except bzrlib.errors.FileExists: