/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

Fix imports of TestLoader on newer versions of bzr.

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,
 
35
    registry,
28
36
    trace,
 
37
    versionedfile,
29
38
    )
30
 
from bzrlib.errors import (
31
 
    NoSuchRevision,
 
39
from bzrlib.transport import (
 
40
    get_transport,
32
41
    )
33
42
 
34
43
 
45
54
    return ret
46
55
 
47
56
 
 
57
def get_remote_cache_transport():
 
58
    """Retrieve the transport to use when accessing (unwritable) remote 
 
59
    repositories.
 
60
    """
 
61
    return get_transport(get_cache_dir())
 
62
 
 
63
 
48
64
def check_pysqlite_version(sqlite3):
49
65
    """Check that sqlite library is compatible.
50
66
 
51
67
    """
52
 
    if (sqlite3.sqlite_version_info[0] < 3 or 
53
 
            (sqlite3.sqlite_version_info[0] == 3 and 
 
68
    if (sqlite3.sqlite_version_info[0] < 3 or
 
69
            (sqlite3.sqlite_version_info[0] == 3 and
54
70
             sqlite3.sqlite_version_info[1] < 3)):
55
 
        warning('Needs at least sqlite 3.3.x')
 
71
        trace.warning('Needs at least sqlite 3.3.x')
56
72
        raise bzrlib.errors.BzrError("incompatible sqlite library")
57
73
 
58
74
try:
59
75
    try:
60
76
        import sqlite3
61
77
        check_pysqlite_version(sqlite3)
62
 
    except (ImportError, bzrlib.errors.BzrError), e: 
 
78
    except (ImportError, bzrlib.errors.BzrError), e:
63
79
        from pysqlite2 import dbapi2 as sqlite3
64
80
        check_pysqlite_version(sqlite3)
65
81
except:
66
 
    warning('Needs at least Python2.5 or Python2.4 with the pysqlite2 '
 
82
    trace.warning('Needs at least Python2.5 or Python2.4 with the pysqlite2 '
67
83
            'module')
68
84
    raise bzrlib.errors.BzrError("missing sqlite library")
69
85
 
81
97
class GitShaMap(object):
82
98
    """Git<->Bzr revision id mapping database."""
83
99
 
84
 
    def add_entry(self, sha, type, type_data):
85
 
        """Add a new entry to the database.
86
 
        """
87
 
        raise NotImplementedError(self.add_entry)
88
 
 
89
 
    def add_entries(self, entries):
90
 
        """Add multiple new entries to the database.
91
 
        """
92
 
        for e in entries:
93
 
            self.add_entry(*e)
94
 
 
95
 
    def lookup_tree(self, fileid, revid):
96
 
        """Lookup the SHA of a git tree."""
97
 
        raise NotImplementedError(self.lookup_tree)
98
 
 
99
 
    def lookup_blob(self, fileid, revid):
100
 
        """Lookup a blob by the fileid it has in a bzr revision."""
101
 
        raise NotImplementedError(self.lookup_blob)
102
 
 
103
100
    def lookup_git_sha(self, sha):
104
101
        """Lookup a Git sha in the database.
105
 
 
106
102
        :param sha: Git object sha
107
103
        :return: (type, type_data) with type_data:
108
104
            revision: revid, tree sha
109
105
        """
110
106
        raise NotImplementedError(self.lookup_git_sha)
111
107
 
 
108
    def lookup_blob_id(self, file_id, revision):
 
109
        """Retrieve a Git blob SHA by file id.
 
110
 
 
111
        :param file_id: File id of the file/symlink
 
112
        :param revision: revision in which the file was last changed.
 
113
        """
 
114
        raise NotImplementedError(self.lookup_blob_id)
 
115
 
 
116
    def lookup_tree_id(self, file_id, revision):
 
117
        """Retrieve a Git tree SHA by file id.
 
118
        """
 
119
        raise NotImplementedError(self.lookup_tree_id)
 
120
 
112
121
    def revids(self):
113
122
        """List the revision ids known."""
114
123
        raise NotImplementedError(self.revids)
115
124
 
116
 
    def sha1s(Self):
 
125
    def missing_revisions(self, revids):
 
126
        """Return set of all the revisions that are not present."""
 
127
        present_revids = set(self.revids())
 
128
        if not isinstance(revids, set):
 
129
            revids = set(revids)
 
130
        return revids - present_revids
 
131
 
 
132
    def sha1s(self):
117
133
        """List the SHA1s."""
118
134
        raise NotImplementedError(self.sha1s)
119
135
 
120
 
    def commit(self):
 
136
    def start_write_group(self):
 
137
        """Start writing changes."""
 
138
 
 
139
    def commit_write_group(self):
121
140
        """Commit any pending changes."""
122
141
 
 
142
    def abort_write_group(self):
 
143
        """Abort any pending changes."""
 
144
 
 
145
 
 
146
class ContentCache(object):
 
147
    """Object that can cache Git objects."""
 
148
 
 
149
    def add(self, object):
 
150
        """Add an object."""
 
151
        raise NotImplementedError(self.add)
 
152
 
 
153
    def add_multi(self, objects):
 
154
        """Add multiple objects."""
 
155
        for obj in objects:
 
156
            self.add(obj)
 
157
 
 
158
    def __getitem__(self, sha):
 
159
        """Retrieve an item, by SHA."""
 
160
        raise NotImplementedError(self.__getitem__)
 
161
 
 
162
 
 
163
class BzrGitCacheFormat(object):
 
164
    """Bazaar-Git Cache Format."""
 
165
 
 
166
    def get_format_string(self):
 
167
        """Return a single-line unique format string for this cache format."""
 
168
        raise NotImplementedError(self.get_format_string)
 
169
 
 
170
    def open(self, transport):
 
171
        """Open this format on a transport."""
 
172
        raise NotImplementedError(self.open)
 
173
 
 
174
    def initialize(self, transport):
 
175
        """Create a new instance of this cache format at transport."""
 
176
        transport.put_bytes('format', self.get_format_string())
 
177
 
 
178
    @classmethod
 
179
    def from_transport(self, transport):
 
180
        """Open a cache file present on a transport, or initialize one.
 
181
 
 
182
        :param transport: Transport to use
 
183
        :return: A BzrGitCache instance
 
184
        """
 
185
        try:
 
186
            format_name = transport.get_bytes('format')
 
187
            format = formats.get(format_name)
 
188
        except bzrlib.errors.NoSuchFile:
 
189
            format = formats.get('default')
 
190
            format.initialize(transport)
 
191
        return format.open(transport)
 
192
 
 
193
    @classmethod
 
194
    def from_repository(cls, repository):
 
195
        """Open a cache file for a repository.
 
196
 
 
197
        This will use the repository's transport to store the cache file, or
 
198
        use the users global cache directory if the repository has no 
 
199
        transport associated with it.
 
200
 
 
201
        :param repository: Repository to open the cache for
 
202
        :return: A `BzrGitCache`
 
203
        """
 
204
        repo_transport = getattr(repository, "_transport", None)
 
205
        if repo_transport is not None:
 
206
            # Even if we don't write to this repo, we should be able 
 
207
            # to update its cache.
 
208
            repo_transport = remove_readonly_transport_decorator(repo_transport)
 
209
            try:
 
210
                repo_transport.mkdir('git')
 
211
            except bzrlib.errors.FileExists:
 
212
                pass
 
213
            transport = repo_transport.clone('git')
 
214
        else:
 
215
            transport = get_remote_cache_transport()
 
216
        return cls.from_transport(transport)
 
217
 
 
218
 
 
219
class CacheUpdater(object):
 
220
    """Base class for objects that can update a bzr-git cache."""
 
221
 
 
222
    def add_object(self, obj, ie, path):
 
223
        raise NotImplementedError(self.add_object)
 
224
 
 
225
    def finish(self):
 
226
        raise NotImplementedError(self.finish)
 
227
 
 
228
 
 
229
class BzrGitCache(object):
 
230
    """Caching backend."""
 
231
 
 
232
    def __init__(self, idmap, content_cache, cache_updater_klass):
 
233
        self.idmap = idmap
 
234
        self.content_cache = content_cache
 
235
        self._cache_updater_klass = cache_updater_klass
 
236
 
 
237
    def get_updater(self, rev):
 
238
        """Update an object that implements the CacheUpdater interface for 
 
239
        updating this cache.
 
240
        """
 
241
        return self._cache_updater_klass(self, rev)
 
242
 
 
243
 
 
244
DictBzrGitCache = lambda: BzrGitCache(DictGitShaMap(), None, DictCacheUpdater)
 
245
 
 
246
 
 
247
class DictCacheUpdater(CacheUpdater):
 
248
    """Cache updater for dict-based caches."""
 
249
 
 
250
    def __init__(self, cache, rev):
 
251
        self.cache = cache
 
252
        self.revid = rev.revision_id
 
253
        self.parent_revids = rev.parent_ids
 
254
        self._commit = None
 
255
        self._entries = []
 
256
 
 
257
    def add_object(self, obj, ie, path):
 
258
        if obj.type_name == "commit":
 
259
            self._commit = obj
 
260
            assert ie is None
 
261
            type_data = (self.revid, self._commit.tree)
 
262
            self.cache.idmap._by_revid[self.revid] = obj.id
 
263
        elif obj.type_name in ("blob", "tree"):
 
264
            if ie is not None:
 
265
                if obj.type_name == "blob":
 
266
                    revision = ie.revision
 
267
                else:
 
268
                    revision = self.revid
 
269
                type_data = (ie.file_id, revision)
 
270
                self.cache.idmap._by_fileid.setdefault(type_data[1], {})[type_data[0]] =\
 
271
                    obj.id
 
272
        else:
 
273
            raise AssertionError
 
274
        self.cache.idmap._by_sha[obj.id] = (obj.type_name, type_data)
 
275
 
 
276
    def finish(self):
 
277
        if self._commit is None:
 
278
            raise AssertionError("No commit object added")
 
279
        return self._commit
 
280
 
123
281
 
124
282
class DictGitShaMap(GitShaMap):
 
283
    """Git SHA map that uses a dictionary."""
125
284
 
126
285
    def __init__(self):
127
 
        self.dict = {}
 
286
        self._by_sha = {}
 
287
        self._by_fileid = {}
 
288
        self._by_revid = {}
128
289
 
129
 
    def add_entry(self, sha, type, type_data):
130
 
        self.dict[sha] = (type, type_data)
 
290
    def lookup_blob_id(self, fileid, revision):
 
291
        return self._by_fileid[revision][fileid]
131
292
 
132
293
    def lookup_git_sha(self, sha):
133
 
        return self.dict[sha]
134
 
 
135
 
    def lookup_tree(self, fileid, revid):
136
 
        for k, v in self.dict.iteritems():
137
 
            if v == ("tree", (fileid, revid)):
138
 
                return k
139
 
        raise KeyError((fileid, revid))
140
 
 
141
 
    def lookup_blob(self, fileid, revid):
142
 
        for k, v in self.dict.iteritems():
143
 
            if v == ("blob", (fileid, revid)):
144
 
                return k
145
 
        raise KeyError((fileid, revid))
 
294
        return self._by_sha[sha]
 
295
 
 
296
    def lookup_tree_id(self, fileid, revision):
 
297
        return self._by_fileid[revision][fileid]
 
298
 
 
299
    def lookup_commit(self, revid):
 
300
        return self._by_revid[revid]
146
301
 
147
302
    def revids(self):
148
 
        for key, (type, type_data) in self.dict.iteritems():
 
303
        for key, (type, type_data) in self._by_sha.iteritems():
149
304
            if type == "commit":
150
305
                yield type_data[0]
151
306
 
152
307
    def sha1s(self):
153
 
        return self.dict.iterkeys()
 
308
        return self._by_sha.iterkeys()
 
309
 
 
310
 
 
311
class SqliteCacheUpdater(CacheUpdater):
 
312
 
 
313
    def __init__(self, cache, rev):
 
314
        self.cache = cache
 
315
        self.db = self.cache.idmap.db
 
316
        self.revid = rev.revision_id
 
317
        self._commit = None
 
318
        self._trees = []
 
319
        self._blobs = []
 
320
 
 
321
    def add_object(self, obj, ie, path):
 
322
        if obj.type_name == "commit":
 
323
            self._commit = obj
 
324
            assert ie is None
 
325
        elif obj.type_name == "tree":
 
326
            if ie is not None:
 
327
                self._trees.append((obj.id, ie.file_id, self.revid))
 
328
        elif obj.type_name == "blob":
 
329
            if ie is not None:
 
330
                self._blobs.append((obj.id, ie.file_id, ie.revision))
 
331
        else:
 
332
            raise AssertionError
 
333
 
 
334
    def finish(self):
 
335
        if self._commit is None:
 
336
            raise AssertionError("No commit object added")
 
337
        self.db.executemany(
 
338
            "replace into trees (sha1, fileid, revid) values (?, ?, ?)",
 
339
            self._trees)
 
340
        self.db.executemany(
 
341
            "replace into blobs (sha1, fileid, revid) values (?, ?, ?)",
 
342
            self._blobs)
 
343
        self.db.execute(
 
344
            "replace into commits (sha1, revid, tree_sha) values (?, ?, ?)",
 
345
            (self._commit.id, self.revid, self._commit.tree))
 
346
        return self._commit
 
347
 
 
348
 
 
349
SqliteBzrGitCache = lambda p: BzrGitCache(SqliteGitShaMap(p), None, SqliteCacheUpdater)
 
350
 
 
351
 
 
352
class SqliteGitCacheFormat(BzrGitCacheFormat):
 
353
 
 
354
    def get_format_string(self):
 
355
        return 'bzr-git sha map version 1 using sqlite\n'
 
356
 
 
357
    def open(self, transport):
 
358
        try:
 
359
            basepath = transport.local_abspath(".")
 
360
        except bzrlib.errors.NotLocalUrl:
 
361
            basepath = get_cache_dir()
 
362
        return SqliteBzrGitCache(os.path.join(basepath, "idmap.db"))
154
363
 
155
364
 
156
365
class SqliteGitShaMap(GitShaMap):
 
366
    """Bazaar GIT Sha map that uses a sqlite database for storage."""
157
367
 
158
368
    def __init__(self, path=None):
159
369
        self.path = path
162
372
        else:
163
373
            if not mapdbs().has_key(path):
164
374
                mapdbs()[path] = sqlite3.connect(path)
165
 
            self.db = mapdbs()[path]    
 
375
            self.db = mapdbs()[path]
 
376
        self.db.text_factory = str
166
377
        self.db.executescript("""
167
 
        create table if not exists commits(sha1 text, revid text, tree_sha text);
 
378
        create table if not exists commits(
 
379
            sha1 text not null check(length(sha1) == 40),
 
380
            revid text not null,
 
381
            tree_sha text not null check(length(tree_sha) == 40)
 
382
        );
168
383
        create index if not exists commit_sha1 on commits(sha1);
169
384
        create unique index if not exists commit_revid on commits(revid);
170
 
        create table if not exists blobs(sha1 text, fileid text, revid text);
 
385
        create table if not exists blobs(
 
386
            sha1 text not null check(length(sha1) == 40),
 
387
            fileid text not null,
 
388
            revid text not null
 
389
        );
171
390
        create index if not exists blobs_sha1 on blobs(sha1);
172
391
        create unique index if not exists blobs_fileid_revid on blobs(fileid, revid);
173
 
        create table if not exists trees(sha1 text, fileid text, revid text);
174
 
        create index if not exists trees_sha1 on trees(sha1);
 
392
        create table if not exists trees(
 
393
            sha1 text unique not null check(length(sha1) == 40),
 
394
            fileid text not null,
 
395
            revid text not null
 
396
        );
 
397
        create unique index if not exists trees_sha1 on trees(sha1);
175
398
        create unique index if not exists trees_fileid_revid on trees(fileid, revid);
176
399
""")
177
400
 
178
 
    @classmethod
179
 
    def from_repository(cls, repository):
180
 
        try:
181
 
            transport = getattr(repository, "_transport", None)
182
 
            if transport is not None:
183
 
                return cls(os.path.join(transport.local_abspath("."), "git.db"))
184
 
        except bzrlib.errors.NotLocalUrl:
185
 
            pass
186
 
        return cls(os.path.join(get_cache_dir(), "remote.db"))
187
 
 
 
401
    def __repr__(self):
 
402
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
403
    
188
404
    def lookup_commit(self, revid):
189
 
        row = self.db.execute("select sha1 from commits where revid = ?", (revid,)).fetchone()
 
405
        cursor = self.db.execute("select sha1 from commits where revid = ?", 
 
406
            (revid,))
 
407
        row = cursor.fetchone()
190
408
        if row is not None:
191
 
            return row[0].encode("utf-8")
 
409
            return row[0]
192
410
        raise KeyError
193
411
 
194
 
    def commit(self):
 
412
    def commit_write_group(self):
195
413
        self.db.commit()
196
414
 
197
 
    def add_entries(self, entries):
198
 
        trees = []
199
 
        blobs = []
200
 
        for sha, type, type_data in entries:
201
 
            assert isinstance(type_data[0], str)
202
 
            assert isinstance(type_data[1], str)
203
 
            entry = (sha.decode("utf-8"), type_data[0].decode("utf-8"), 
204
 
                     type_data[1].decode("utf-8"))
205
 
            if type == "tree":
206
 
                trees.append(entry)
207
 
            elif type == "blob":
208
 
                blobs.append(entry)
209
 
            else:
210
 
                raise AssertionError
211
 
        if trees:
212
 
            self.db.executemany("replace into trees (sha1, fileid, revid) values (?, ?, ?)", trees)
213
 
        if blobs:
214
 
            self.db.executemany("replace into blobs (sha1, fileid, revid) values (?, ?, ?)", blobs)
215
 
 
216
 
 
217
 
    def add_entry(self, sha, type, type_data):
218
 
        """Add a new entry to the database.
219
 
        """
220
 
        assert isinstance(type_data, tuple)
221
 
        assert isinstance(sha, str), "type was %r" % sha
222
 
        if type == "commit":
223
 
            self.db.execute("replace into commits (sha1, revid, tree_sha) values (?, ?, ?)", (sha, type_data[0], type_data[1]))
224
 
        elif type in ("blob", "tree"):
225
 
            self.db.execute("replace into %ss (sha1, fileid, revid) values (?, ?, ?)" % type, (sha, type_data[0], type_data[1]))
226
 
        else:
227
 
            raise AssertionError("Unknown type %s" % type)
228
 
 
229
 
    def lookup_tree(self, fileid, revid):
230
 
        row = self.db.execute("select sha1 from trees where fileid = ? and revid = ?", (fileid,revid)).fetchone()
231
 
        if row is None:
232
 
            raise KeyError((fileid, revid))
233
 
        return row[0].encode("utf-8")
234
 
 
235
 
    def lookup_blob(self, fileid, revid):
236
 
        row = self.db.execute("select sha1 from blobs where fileid = ? and revid = ?", (fileid, revid)).fetchone()
237
 
        if row is None:
238
 
            raise KeyError((fileid, revid))
239
 
        return row[0].encode("utf-8")
 
415
    def lookup_blob_id(self, fileid, revision):
 
416
        row = self.db.execute("select sha1 from blobs where fileid = ? and revid = ?", (fileid, revision)).fetchone()
 
417
        if row is not None:
 
418
            return row[0]
 
419
        raise KeyError(fileid)
 
420
 
 
421
    def lookup_tree_id(self, fileid, revision):
 
422
        row = self.db.execute("select sha1 from trees where fileid = ? and revid = ?", (fileid, revision)).fetchone()
 
423
        if row is not None:
 
424
            return row[0]
 
425
        raise KeyError(fileid)
240
426
 
241
427
    def lookup_git_sha(self, sha):
242
428
        """Lookup a Git sha in the database.
245
431
        :return: (type, type_data) with type_data:
246
432
            revision: revid, tree sha
247
433
        """
248
 
        def format(type, row):
249
 
            return (type, (row[0].encode("utf-8"), row[1].encode("utf-8")))
250
434
        row = self.db.execute("select revid, tree_sha from commits where sha1 = ?", (sha,)).fetchone()
251
435
        if row is not None:
252
 
            return format("commit", row)
 
436
            return ("commit", row)
253
437
        row = self.db.execute("select fileid, revid from blobs where sha1 = ?", (sha,)).fetchone()
254
438
        if row is not None:
255
 
            return format("blob", row)
 
439
            return ("blob", row)
256
440
        row = self.db.execute("select fileid, revid from trees where sha1 = ?", (sha,)).fetchone()
257
441
        if row is not None:
258
 
            return format("tree", row)
 
442
            return ("tree", row)
259
443
        raise KeyError(sha)
260
444
 
261
445
    def revids(self):
262
446
        """List the revision ids known."""
263
 
        for row in self.db.execute("select revid from commits").fetchall():
264
 
            yield row[0].encode("utf-8")
 
447
        return (row for (row,) in self.db.execute("select revid from commits"))
265
448
 
266
449
    def sha1s(self):
267
450
        """List the SHA1s."""
268
451
        for table in ("blobs", "commits", "trees"):
269
 
            for row in self.db.execute("select sha1 from %s" % table).fetchall():
270
 
                yield row[0].encode("utf-8")
271
 
 
272
 
 
273
 
TDB_MAP_VERSION = 2
274
 
TDB_HASH_SIZE = 10000
 
452
            for (sha,) in self.db.execute("select sha1 from %s" % table):
 
453
                yield sha
 
454
 
 
455
 
 
456
class TdbCacheUpdater(CacheUpdater):
 
457
    """Cache updater for tdb-based caches."""
 
458
 
 
459
    def __init__(self, cache, rev):
 
460
        self.cache = cache
 
461
        self.db = cache.idmap.db
 
462
        self.revid = rev.revision_id
 
463
        self.parent_revids = rev.parent_ids
 
464
        self._commit = None
 
465
        self._entries = []
 
466
 
 
467
    def add_object(self, obj, ie, path):
 
468
        sha = obj.sha().digest()
 
469
        if obj.type_name == "commit":
 
470
            self.db["commit\0" + self.revid] = "\0".join((sha, obj.tree))
 
471
            type_data = (self.revid, obj.tree)
 
472
            self._commit = obj
 
473
            assert ie is None
 
474
        elif obj.type_name == "blob":
 
475
            if ie is None:
 
476
                return
 
477
            self.db["\0".join(("blob", ie.file_id, ie.revision))] = sha
 
478
            type_data = (ie.file_id, ie.revision)
 
479
        elif obj.type_name == "tree":
 
480
            if ie is None:
 
481
                return
 
482
            type_data = (ie.file_id, self.revid)
 
483
        else:
 
484
            raise AssertionError
 
485
        self.db["git\0" + sha] = "\0".join((obj.type_name, ) + type_data)
 
486
 
 
487
    def finish(self):
 
488
        if self._commit is None:
 
489
            raise AssertionError("No commit object added")
 
490
        return self._commit
 
491
 
 
492
 
 
493
TdbBzrGitCache = lambda p: BzrGitCache(TdbGitShaMap(p), None, TdbCacheUpdater)
 
494
 
 
495
class TdbGitCacheFormat(BzrGitCacheFormat):
 
496
    """Cache format for tdb-based caches."""
 
497
 
 
498
    def get_format_string(self):
 
499
        return 'bzr-git sha map version 3 using tdb\n'
 
500
 
 
501
    def open(self, transport):
 
502
        try:
 
503
            basepath = transport.local_abspath(".")
 
504
        except bzrlib.errors.NotLocalUrl:
 
505
            basepath = get_cache_dir()
 
506
        try:
 
507
            return TdbBzrGitCache(os.path.join(basepath, "idmap.tdb"))
 
508
        except ImportError:
 
509
            raise ImportError(
 
510
                "Unable to open existing bzr-git cache because 'tdb' is not "
 
511
                "installed.")
275
512
 
276
513
 
277
514
class TdbGitShaMap(GitShaMap):
285
522
    "blob fileid revid" -> "<sha1>"
286
523
    """
287
524
 
 
525
    TDB_MAP_VERSION = 3
 
526
    TDB_HASH_SIZE = 50000
 
527
 
288
528
    def __init__(self, path=None):
289
529
        import tdb
290
530
        self.path = path
292
532
            self.db = {}
293
533
        else:
294
534
            if not mapdbs().has_key(path):
295
 
                mapdbs()[path] = tdb.Tdb(path, TDB_HASH_SIZE, tdb.DEFAULT, 
 
535
                mapdbs()[path] = tdb.Tdb(path, self.TDB_HASH_SIZE, tdb.DEFAULT,
296
536
                                          os.O_RDWR|os.O_CREAT)
297
 
            self.db = mapdbs()[path]    
298
 
        if not "version" in self.db:
299
 
            self.db["version"] = str(TDB_MAP_VERSION)
300
 
        else:
301
 
            if int(self.db["version"]) != TDB_MAP_VERSION:
 
537
            self.db = mapdbs()[path]
 
538
        try:
 
539
            if int(self.db["version"]) not in (2, 3):
302
540
                trace.warning("SHA Map is incompatible (%s -> %d), rebuilding database.",
303
 
                              self.db["version"], TDB_MAP_VERSION)
 
541
                              self.db["version"], self.TDB_MAP_VERSION)
304
542
                self.db.clear()
305
 
            self.db["version"] = str(TDB_MAP_VERSION)
306
 
 
307
 
    @classmethod
308
 
    def from_repository(cls, repository):
309
 
        try:
310
 
            transport = getattr(repository, "_transport", None)
311
 
            if transport is not None:
312
 
                return cls(os.path.join(transport.local_abspath("."), "git.tdb"))
313
 
        except bzrlib.errors.NotLocalUrl:
 
543
        except KeyError:
314
544
            pass
315
 
        return cls(os.path.join(get_cache_dir(), "remote.tdb"))
 
545
        self.db["version"] = str(self.TDB_MAP_VERSION)
 
546
 
 
547
    def start_write_group(self):
 
548
        """Start writing changes."""
 
549
        self.db.transaction_start()
 
550
 
 
551
    def commit_write_group(self):
 
552
        """Commit any pending changes."""
 
553
        self.db.transaction_commit()
 
554
 
 
555
    def abort_write_group(self):
 
556
        """Abort any pending changes."""
 
557
        self.db.transaction_cancel()
 
558
 
 
559
    def __repr__(self):
 
560
        return "%s(%r)" % (self.__class__.__name__, self.path)
316
561
 
317
562
    def lookup_commit(self, revid):
318
563
        return sha_to_hex(self.db["commit\0" + revid][:20])
319
564
 
320
 
    def commit(self):
321
 
        pass
322
 
 
323
 
    def add_entry(self, sha, type, type_data):
324
 
        """Add a new entry to the database.
325
 
        """
326
 
        self.db["git\0" + hex_to_sha(sha)] = "\0".join((type, type_data[0], type_data[1]))
327
 
        if type == "commit":
328
 
            self.db["commit\0" + type_data[0]] = "\0".join((hex_to_sha(sha), type_data[1]))
329
 
        else:
330
 
            self.db["\0".join((type, type_data[0], type_data[1]))] = hex_to_sha(sha)
331
 
 
332
 
    def lookup_tree(self, fileid, revid):
333
 
        return sha_to_hex(self.db["\0".join(("tree", fileid, revid))])
334
 
 
335
 
    def lookup_blob(self, fileid, revid):
336
 
        return sha_to_hex(self.db["\0".join(("blob", fileid, revid))])
337
 
 
 
565
    def lookup_blob_id(self, fileid, revision):
 
566
        return sha_to_hex(self.db["\0".join(("blob", fileid, revision))])
 
567
                
338
568
    def lookup_git_sha(self, sha):
339
569
        """Lookup a Git sha in the database.
340
570
 
342
572
        :return: (type, type_data) with type_data:
343
573
            revision: revid, tree sha
344
574
        """
345
 
        data = self.db["git\0" + hex_to_sha(sha)].split("\0")
 
575
        if len(sha) == 40:
 
576
            sha = hex_to_sha(sha)
 
577
        data = self.db["git\0" + sha].split("\0")
346
578
        return (data[0], (data[1], data[2]))
347
579
 
 
580
    def missing_revisions(self, revids):
 
581
        ret = set()
 
582
        for revid in revids:
 
583
            if self.db.get("commit\0" + revid) is None:
 
584
                ret.add(revid)
 
585
        return ret
 
586
 
348
587
    def revids(self):
349
588
        """List the revision ids known."""
350
589
        for key in self.db.iterkeys():
356
595
        for key in self.db.iterkeys():
357
596
            if key.startswith("git\0"):
358
597
                yield sha_to_hex(key[4:])
 
598
 
 
599
 
 
600
class VersionedFilesContentCache(ContentCache):
 
601
 
 
602
    def __init__(self, vf):
 
603
        self._vf = vf
 
604
 
 
605
    def add(self, obj):
 
606
        self._vf.insert_record_stream(
 
607
            [versionedfile.ChunkedContentFactory((obj.id,), [], None,
 
608
                obj.as_legacy_object_chunks())])
 
609
 
 
610
    def __getitem__(self, sha):
 
611
        stream = self._vf.get_record_stream([(sha,)], 'unordered', True)
 
612
        entry = stream.next() 
 
613
        if entry.storage_kind == 'absent':
 
614
            raise KeyError(sha)
 
615
        return ShaFile._parse_legacy_object(entry.get_bytes_as('fulltext'))
 
616
 
 
617
 
 
618
class GitObjectStoreContentCache(ContentCache):
 
619
 
 
620
    def __init__(self, store):
 
621
        self.store = store
 
622
 
 
623
    def add_multi(self, objs):
 
624
        self.store.add_objects(objs)
 
625
 
 
626
    def add(self, obj, path):
 
627
        self.store.add_object(obj)
 
628
 
 
629
    def __getitem__(self, sha):
 
630
        return self.store[sha]
 
631
 
 
632
 
 
633
class IndexCacheUpdater(CacheUpdater):
 
634
 
 
635
    def __init__(self, cache, rev):
 
636
        self.cache = cache
 
637
        self.revid = rev.revision_id
 
638
        self.parent_revids = rev.parent_ids
 
639
        self._commit = None
 
640
        self._entries = []
 
641
        self._cache_objs = set()
 
642
 
 
643
    def add_object(self, obj, ie, path):
 
644
        if obj.type_name == "commit":
 
645
            self._commit = obj
 
646
            assert ie is None
 
647
            self.cache.idmap._add_git_sha(obj.id, "commit",
 
648
                (self.revid, obj.tree))
 
649
            self.cache.idmap._add_node(("commit", self.revid, "X"),
 
650
                " ".join((obj.id, obj.tree)))
 
651
            self._cache_objs.add((obj, path))
 
652
        elif obj.type_name == "blob":
 
653
            self.cache.idmap._add_git_sha(obj.id, "blob",
 
654
                (ie.file_id, ie.revision))
 
655
            self.cache.idmap._add_node(("blob", ie.file_id, ie.revision), obj.id)
 
656
            if ie.kind == "symlink":
 
657
                self._cache_objs.add((obj, path))
 
658
        elif obj.type_name == "tree":
 
659
            self.cache.idmap._add_git_sha(obj.id, "tree",
 
660
                (ie.file_id, self.revid))
 
661
            self._cache_objs.add((obj, path))
 
662
        else:
 
663
            raise AssertionError
 
664
 
 
665
    def finish(self):
 
666
        self.cache.content_cache.add_multi(self._cache_objs)
 
667
        return self._commit
 
668
 
 
669
 
 
670
class IndexBzrGitCache(BzrGitCache):
 
671
 
 
672
    def __init__(self, transport=None):
 
673
        mapper = versionedfile.ConstantMapper("trees")
 
674
        shamap = IndexGitShaMap(transport.clone('index'))
 
675
        #trees_store = knit.make_file_factory(True, mapper)(transport)
 
676
        #content_cache = VersionedFilesContentCache(trees_store)
 
677
        from bzrlib.plugins.git.transportgit import TransportObjectStore
 
678
        store = TransportObjectStore(transport.clone('objects'))
 
679
        content_cache = GitObjectStoreContentCache(store)
 
680
        super(IndexBzrGitCache, self).__init__(shamap, content_cache,
 
681
                IndexCacheUpdater)
 
682
 
 
683
 
 
684
class IndexGitCacheFormat(BzrGitCacheFormat):
 
685
 
 
686
    def get_format_string(self):
 
687
        return 'bzr-git sha map with git object cache version 1\n'
 
688
 
 
689
    def initialize(self, transport):
 
690
        super(IndexGitCacheFormat, self).initialize(transport)
 
691
        transport.mkdir('index')
 
692
        transport.mkdir('objects')
 
693
        from bzrlib.plugins.git.transportgit import TransportObjectStore
 
694
        TransportObjectStore.init(transport.clone('objects'))
 
695
 
 
696
    def open(self, transport):
 
697
        return IndexBzrGitCache(transport)
 
698
 
 
699
 
 
700
class IndexGitShaMap(GitShaMap):
 
701
    """SHA Map that uses the Bazaar APIs to store a cache.
 
702
 
 
703
    BTree Index file with the following contents:
 
704
 
 
705
    ("git", <sha1>) -> "<type> <type-data1> <type-data2>"
 
706
    ("commit", <revid>) -> "<sha1> <tree-id>"
 
707
    ("blob", <fileid>, <revid>) -> <sha1>
 
708
 
 
709
    """
 
710
 
 
711
    def __init__(self, transport=None):
 
712
        if transport is None:
 
713
            self._transport = None
 
714
            self._index = _mod_index.InMemoryGraphIndex(0, key_elements=3)
 
715
            self._builder = self._index
 
716
        else:
 
717
            self._builder = None
 
718
            self._transport = transport
 
719
            self._index = _mod_index.CombinedGraphIndex([])
 
720
            for name in self._transport.list_dir("."):
 
721
                if not name.endswith(".rix"):
 
722
                    continue
 
723
                x = _mod_btree_index.BTreeGraphIndex(self._transport, name,
 
724
                    self._transport.stat(name).st_size)
 
725
                self._index.insert_index(0, x)
 
726
 
 
727
    @classmethod
 
728
    def from_repository(cls, repository):
 
729
        transport = getattr(repository, "_transport", None)
 
730
        if transport is not None:
 
731
            try:
 
732
                transport.mkdir('git')
 
733
            except bzrlib.errors.FileExists:
 
734
                pass
 
735
            return cls(transport.clone('git'))
 
736
        from bzrlib.transport import get_transport
 
737
        return cls(get_transport(get_cache_dir()))
 
738
 
 
739
    def __repr__(self):
 
740
        if self._transport is not None:
 
741
            return "%s(%r)" % (self.__class__.__name__, self._transport.base)
 
742
        else:
 
743
            return "%s()" % (self.__class__.__name__)
 
744
 
 
745
    def repack(self):
 
746
        assert self._builder is None
 
747
        self.start_write_group()
 
748
        for _, key, value in self._index.iter_all_entries():
 
749
            self._builder.add_node(key, value)
 
750
        to_remove = []
 
751
        for name in self._transport.list_dir('.'):
 
752
            if name.endswith('.rix'):
 
753
                to_remove.append(name)
 
754
        self.commit_write_group()
 
755
        del self._index.indices[1:]
 
756
        for name in to_remove:
 
757
            self._transport.rename(name, name + '.old')
 
758
 
 
759
    def start_write_group(self):
 
760
        assert self._builder is None
 
761
        self._builder = _mod_btree_index.BTreeBuilder(0, key_elements=3)
 
762
        self._name = osutils.sha()
 
763
 
 
764
    def commit_write_group(self):
 
765
        assert self._builder is not None
 
766
        stream = self._builder.finish()
 
767
        name = self._name.hexdigest() + ".rix"
 
768
        size = self._transport.put_file(name, stream)
 
769
        index = _mod_btree_index.BTreeGraphIndex(self._transport, name, size)
 
770
        self._index.insert_index(0, index)
 
771
        self._builder = None
 
772
        self._name = None
 
773
 
 
774
    def abort_write_group(self):
 
775
        assert self._builder is not None
 
776
        self._builder = None
 
777
        self._name = None
 
778
 
 
779
    def _add_node(self, key, value):
 
780
        try:
 
781
            self._builder.add_node(key, value)
 
782
        except bzrlib.errors.BadIndexDuplicateKey:
 
783
            # Multiple bzr objects can have the same contents
 
784
            return True
 
785
        else:
 
786
            return False
 
787
 
 
788
    def _get_entry(self, key):
 
789
        entries = self._index.iter_entries([key])
 
790
        try:
 
791
            return entries.next()[2]
 
792
        except StopIteration:
 
793
            if self._builder is None:
 
794
                raise KeyError
 
795
            entries = self._builder.iter_entries([key])
 
796
            try:
 
797
                return entries.next()[2]
 
798
            except StopIteration:
 
799
                raise KeyError
 
800
 
 
801
    def _iter_keys_prefix(self, prefix):
 
802
        for entry in self._index.iter_entries_prefix([prefix]):
 
803
            yield entry[1]
 
804
        if self._builder is not None:
 
805
            for entry in self._builder.iter_entries_prefix([prefix]):
 
806
                yield entry[1]
 
807
 
 
808
    def lookup_commit(self, revid):
 
809
        return self._get_entry(("commit", revid, "X"))[:40]
 
810
 
 
811
    def _add_git_sha(self, hexsha, type, type_data):
 
812
        if hexsha is not None:
 
813
            self._name.update(hexsha)
 
814
            self._add_node(("git", hexsha, "X"),
 
815
                " ".join((type, type_data[0], type_data[1])))
 
816
        else:
 
817
            # This object is not represented in Git - perhaps an empty
 
818
            # directory?
 
819
            self._name.update(type + " ".join(type_data))
 
820
 
 
821
    def lookup_blob_id(self, fileid, revision):
 
822
        return self._get_entry(("blob", fileid, revision))
 
823
 
 
824
    def lookup_git_sha(self, sha):
 
825
        if len(sha) == 20:
 
826
            sha = sha_to_hex(sha)
 
827
        data = self._get_entry(("git", sha, "X")).split(" ", 2)
 
828
        return (data[0], (data[1], data[2]))
 
829
 
 
830
    def revids(self):
 
831
        """List the revision ids known."""
 
832
        for key in self._iter_keys_prefix(("commit", None, None)):
 
833
            yield key[1]
 
834
 
 
835
    def missing_revisions(self, revids):
 
836
        """Return set of all the revisions that are not present."""
 
837
        missing_revids = set(revids)
 
838
        for _, key, value in self._index.iter_entries((
 
839
            ("commit", revid, "X") for revid in revids)):
 
840
            missing_revids.remove(key[1])
 
841
        return missing_revids
 
842
 
 
843
    def sha1s(self):
 
844
        """List the SHA1s."""
 
845
        for key in self._iter_keys_prefix(("git", None, None)):
 
846
            yield key[1]
 
847
 
 
848
 
 
849
formats = registry.Registry()
 
850
formats.register(TdbGitCacheFormat().get_format_string(),
 
851
    TdbGitCacheFormat())
 
852
formats.register(SqliteGitCacheFormat().get_format_string(),
 
853
    SqliteGitCacheFormat())
 
854
formats.register(IndexGitCacheFormat().get_format_string(),
 
855
    IndexGitCacheFormat())
 
856
# In the future, this will become the default:
 
857
# formats.register('default', IndexGitCacheFormat())
 
858
try:
 
859
    import tdb
 
860
except ImportError:
 
861
    formats.register('default', SqliteGitCacheFormat())
 
862
else:
 
863
    formats.register('default', TdbGitCacheFormat())
 
864
 
 
865
 
 
866
 
 
867
def migrate_ancient_formats(repo_transport):
 
868
    # Prefer migrating git.db over git.tdb, since the latter may not 
 
869
    # be openable on some platforms.
 
870
    if repo_transport.has("git.db"):
 
871
        SqliteGitCacheFormat().initialize(repo_transport.clone("git"))
 
872
        repo_transport.rename("git.db", "git/idmap.db")
 
873
    elif repo_transport.has("git.tdb"):
 
874
        TdbGitCacheFormat().initialize(repo_transport.clone("git"))
 
875
        repo_transport.rename("git.tdb", "git/idmap.tdb")
 
876
 
 
877
 
 
878
def remove_readonly_transport_decorator(transport):
 
879
    if transport.is_readonly():
 
880
        return transport._decorated
 
881
    return transport
 
882
 
 
883
 
 
884
def from_repository(repository):
 
885
    """Open a cache file for a repository.
 
886
 
 
887
    If the repository is remote and there is no transport available from it
 
888
    this will use a local file in the users cache directory
 
889
    (typically ~/.cache/bazaar/git/)
 
890
 
 
891
    :param repository: A repository object
 
892
    """
 
893
    repo_transport = getattr(repository, "_transport", None)
 
894
    if repo_transport is not None:
 
895
        # Migrate older cache formats
 
896
        repo_transport = remove_readonly_transport_decorator(repo_transport)
 
897
        try:
 
898
            repo_transport.mkdir("git")
 
899
        except bzrlib.errors.FileExists:
 
900
            pass
 
901
        else:
 
902
            migrate_ancient_formats(repo_transport)
 
903
    return BzrGitCacheFormat.from_repository(repository)