/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

More work on roundtrip push support.

Show diffs side-by-side

added added

removed removed

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