/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 teardown of object store tests.

Show diffs side-by-side

added added

removed removed

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