/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 fetch return value for inter git fetching.

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: list with (type, type_data) tuples 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
            key = self.revid
 
276
            type_data = (self.revid, self._commit.tree, ie)
 
277
            self.cache.idmap._by_revid[self.revid] = obj.id
 
278
        elif obj.type_name in ("blob", "tree"):
 
279
            if ie is not None:
 
280
                if obj.type_name == "blob":
 
281
                    revision = ie.revision
 
282
                else:
 
283
                    revision = self.revid
 
284
                key = type_data = (ie.file_id, revision)
 
285
                self.cache.idmap._by_fileid.setdefault(type_data[1], {})[type_data[0]] = obj.id
 
286
        else:
 
287
            raise AssertionError
 
288
        entry = (obj.type_name, type_data)
 
289
        self.cache.idmap._by_sha.setdefault(obj.id, {})[key] = entry
 
290
 
 
291
    def finish(self):
 
292
        if self._commit is None:
 
293
            raise AssertionError("No commit object added")
 
294
        return self._commit
 
295
 
 
296
 
 
297
class DictGitShaMap(GitShaMap):
 
298
    """Git SHA map that uses a dictionary."""
 
299
 
 
300
    def __init__(self):
 
301
        self._by_sha = {}
 
302
        self._by_fileid = {}
 
303
        self._by_revid = {}
 
304
 
 
305
    def lookup_blob_id(self, fileid, revision):
 
306
        return self._by_fileid[revision][fileid]
 
307
 
 
308
    def lookup_git_sha(self, sha):
 
309
        for entry in self._by_sha[sha].itervalues():
 
310
            yield entry
 
311
 
 
312
    def lookup_tree_id(self, fileid, revision):
 
313
        return self._by_fileid[revision][fileid]
 
314
 
 
315
    def lookup_commit(self, revid):
 
316
        return self._by_revid[revid]
 
317
 
 
318
    def revids(self):
 
319
        for key, entries in self._by_sha.iteritems():
 
320
            for (type, type_data) in entries.values():
 
321
                if type == "commit":
 
322
                    yield type_data[0]
 
323
 
 
324
    def sha1s(self):
 
325
        return self._by_sha.iterkeys()
 
326
 
 
327
 
 
328
class SqliteCacheUpdater(CacheUpdater):
 
329
 
 
330
    def __init__(self, cache, rev):
 
331
        self.cache = cache
 
332
        self.db = self.cache.idmap.db
 
333
        self.revid = rev.revision_id
 
334
        self._commit = None
 
335
        self._trees = []
 
336
        self._blobs = []
 
337
 
 
338
    def add_object(self, obj, ie, path):
 
339
        if obj.type_name == "commit":
 
340
            self._commit = obj
 
341
            self._testament3_sha1 = ie["testament3-sha1"]
 
342
            assert type(ie) is dict
 
343
        elif obj.type_name == "tree":
 
344
            if ie is not None:
 
345
                self._trees.append((obj.id, ie.file_id, self.revid))
 
346
        elif obj.type_name == "blob":
 
347
            if ie is not None:
 
348
                self._blobs.append((obj.id, ie.file_id, ie.revision))
 
349
        else:
 
350
            raise AssertionError
 
351
 
 
352
    def finish(self):
 
353
        if self._commit is None:
 
354
            raise AssertionError("No commit object added")
 
355
        self.db.executemany(
 
356
            "replace into trees (sha1, fileid, revid) values (?, ?, ?)",
 
357
            self._trees)
 
358
        self.db.executemany(
 
359
            "replace into blobs (sha1, fileid, revid) values (?, ?, ?)",
 
360
            self._blobs)
 
361
        self.db.execute(
 
362
            "replace into commits (sha1, revid, tree_sha, testament3_sha1) values (?, ?, ?, ?)",
 
363
            (self._commit.id, self.revid, self._commit.tree, self._testament3_sha1))
 
364
        return self._commit
 
365
 
 
366
 
 
367
SqliteBzrGitCache = lambda p: BzrGitCache(SqliteGitShaMap(p), None, SqliteCacheUpdater)
 
368
 
 
369
 
 
370
class SqliteGitCacheFormat(BzrGitCacheFormat):
 
371
 
 
372
    def get_format_string(self):
 
373
        return 'bzr-git sha map version 1 using sqlite\n'
 
374
 
 
375
    def open(self, transport):
 
376
        try:
 
377
            basepath = transport.local_abspath(".")
 
378
        except bzrlib.errors.NotLocalUrl:
 
379
            basepath = get_cache_dir()
 
380
        return SqliteBzrGitCache(os.path.join(basepath, "idmap.db"))
 
381
 
 
382
 
 
383
class SqliteGitShaMap(GitShaMap):
 
384
    """Bazaar GIT Sha map that uses a sqlite database for storage."""
 
385
 
 
386
    def __init__(self, path=None):
 
387
        self.path = path
 
388
        if path is None:
 
389
            self.db = sqlite3.connect(":memory:")
 
390
        else:
 
391
            if not mapdbs().has_key(path):
 
392
                mapdbs()[path] = sqlite3.connect(path)
 
393
            self.db = mapdbs()[path]
 
394
        self.db.text_factory = str
 
395
        self.db.executescript("""
 
396
        create table if not exists commits(
 
397
            sha1 text not null check(length(sha1) == 40),
 
398
            revid text not null,
 
399
            tree_sha text not null check(length(tree_sha) == 40)
 
400
        );
 
401
        create index if not exists commit_sha1 on commits(sha1);
 
402
        create unique index if not exists commit_revid on commits(revid);
 
403
        create table if not exists blobs(
 
404
            sha1 text not null check(length(sha1) == 40),
 
405
            fileid text not null,
 
406
            revid text not null
 
407
        );
 
408
        create index if not exists blobs_sha1 on blobs(sha1);
 
409
        create unique index if not exists blobs_fileid_revid on blobs(fileid, revid);
 
410
        create table if not exists trees(
 
411
            sha1 text unique not null check(length(sha1) == 40),
 
412
            fileid text not null,
 
413
            revid text not null
 
414
        );
 
415
        create unique index if not exists trees_sha1 on trees(sha1);
 
416
        create unique index if not exists trees_fileid_revid on trees(fileid, revid);
 
417
""")
 
418
        try:
 
419
            self.db.executescript(
 
420
                "ALTER TABLE commits ADD testament3_sha1 TEXT;")
 
421
        except sqlite3.OperationalError:
 
422
            pass # Column already exists.
 
423
 
 
424
    def __repr__(self):
 
425
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
426
 
 
427
    def lookup_commit(self, revid):
 
428
        cursor = self.db.execute("select sha1 from commits where revid = ?", 
 
429
            (revid,))
 
430
        row = cursor.fetchone()
 
431
        if row is not None:
 
432
            return row[0]
 
433
        raise KeyError
 
434
 
 
435
    def commit_write_group(self):
 
436
        self.db.commit()
 
437
 
 
438
    def lookup_blob_id(self, fileid, revision):
 
439
        row = self.db.execute("select sha1 from blobs where fileid = ? and revid = ?", (fileid, revision)).fetchone()
 
440
        if row is not None:
 
441
            return row[0]
 
442
        raise KeyError(fileid)
 
443
 
 
444
    def lookup_tree_id(self, fileid, revision):
 
445
        row = self.db.execute("select sha1 from trees where fileid = ? and revid = ?", (fileid, revision)).fetchone()
 
446
        if row is not None:
 
447
            return row[0]
 
448
        raise KeyError(fileid)
 
449
 
 
450
    def lookup_git_sha(self, sha):
 
451
        """Lookup a Git sha in the database.
 
452
 
 
453
        :param sha: Git object sha
 
454
        :return: (type, type_data) with type_data:
 
455
            commit: revid, tree sha, verifiers
 
456
            tree: fileid, revid
 
457
            blob: fileid, revid
 
458
        """
 
459
        found = False
 
460
        cursor = self.db.execute("select revid, tree_sha, testament3_sha1 from commits where sha1 = ?", (sha,))
 
461
        for row in cursor.fetchall():
 
462
            found = True
 
463
            yield ("commit", (row[0], row[1], {"testament3-sha1": row[2]}))
 
464
        cursor = self.db.execute("select fileid, revid from blobs where sha1 = ?", (sha,))
 
465
        for row in cursor.fetchall():
 
466
            found = True
 
467
            yield ("blob", row)
 
468
        cursor = self.db.execute("select fileid, revid from trees where sha1 = ?", (sha,))
 
469
        for row in cursor.fetchall():
 
470
            found = True
 
471
            yield ("tree", row)
 
472
        if not found:
 
473
            raise KeyError(sha)
 
474
 
 
475
    def revids(self):
 
476
        """List the revision ids known."""
 
477
        return (row for (row,) in self.db.execute("select revid from commits"))
 
478
 
 
479
    def sha1s(self):
 
480
        """List the SHA1s."""
 
481
        for table in ("blobs", "commits", "trees"):
 
482
            for (sha,) in self.db.execute("select sha1 from %s" % table):
 
483
                yield sha
 
484
 
 
485
 
 
486
class TdbCacheUpdater(CacheUpdater):
 
487
    """Cache updater for tdb-based caches."""
 
488
 
 
489
    def __init__(self, cache, rev):
 
490
        self.cache = cache
 
491
        self.db = cache.idmap.db
 
492
        self.revid = rev.revision_id
 
493
        self.parent_revids = rev.parent_ids
 
494
        self._commit = None
 
495
        self._entries = []
 
496
 
 
497
    def add_object(self, obj, ie, path):
 
498
        sha = obj.sha().digest()
 
499
        if obj.type_name == "commit":
 
500
            self.db["commit\0" + self.revid] = "\0".join((sha, obj.tree))
 
501
            assert type(ie) is dict, "was %r" % ie
 
502
            type_data = (self.revid, obj.tree, ie["testament3-sha1"])
 
503
            self._commit = obj
 
504
        elif obj.type_name == "blob":
 
505
            if ie is None:
 
506
                return
 
507
            self.db["\0".join(("blob", ie.file_id, ie.revision))] = sha
 
508
            type_data = (ie.file_id, ie.revision)
 
509
        elif obj.type_name == "tree":
 
510
            if ie is None:
 
511
                return
 
512
            type_data = (ie.file_id, self.revid)
 
513
        else:
 
514
            raise AssertionError
 
515
        entry = "\0".join((obj.type_name, ) + type_data) + "\n"
 
516
        key = "git\0" + sha
 
517
        try:
 
518
            oldval = self.db[key]
 
519
        except KeyError:
 
520
            self.db[key] = entry
 
521
        else:
 
522
            if oldval[-1] != "\n":
 
523
                self.db[key] = "".join([oldval, "\n", entry])
 
524
            else:
 
525
                self.db[key] = "".join([oldval, entry])
 
526
 
 
527
    def finish(self):
 
528
        if self._commit is None:
 
529
            raise AssertionError("No commit object added")
 
530
        return self._commit
 
531
 
 
532
 
 
533
TdbBzrGitCache = lambda p: BzrGitCache(TdbGitShaMap(p), None, TdbCacheUpdater)
 
534
 
 
535
 
 
536
class TdbGitCacheFormat(BzrGitCacheFormat):
 
537
    """Cache format for tdb-based caches."""
 
538
 
 
539
    def get_format_string(self):
 
540
        return 'bzr-git sha map version 3 using tdb\n'
 
541
 
 
542
    def open(self, transport):
 
543
        try:
 
544
            basepath = transport.local_abspath(".").encode(osutils._fs_enc)
 
545
        except bzrlib.errors.NotLocalUrl:
 
546
            basepath = get_cache_dir()
 
547
        assert isinstance(basepath, str)
 
548
        try:
 
549
            return TdbBzrGitCache(os.path.join(basepath, "idmap.tdb"))
 
550
        except ImportError:
 
551
            raise ImportError(
 
552
                "Unable to open existing bzr-git cache because 'tdb' is not "
 
553
                "installed.")
 
554
 
 
555
 
 
556
class TdbGitShaMap(GitShaMap):
 
557
    """SHA Map that uses a TDB database.
 
558
 
 
559
    Entries:
 
560
 
 
561
    "git <sha1>" -> "<type> <type-data1> <type-data2>"
 
562
    "commit revid" -> "<sha1> <tree-id>"
 
563
    "tree fileid revid" -> "<sha1>"
 
564
    "blob fileid revid" -> "<sha1>"
 
565
    """
 
566
 
 
567
    TDB_MAP_VERSION = 3
 
568
    TDB_HASH_SIZE = 50000
 
569
 
 
570
    def __init__(self, path=None):
 
571
        import tdb
 
572
        self.path = path
 
573
        if path is None:
 
574
            self.db = {}
 
575
        else:
 
576
            assert isinstance(path, str)
 
577
            if not mapdbs().has_key(path):
 
578
                mapdbs()[path] = tdb.Tdb(path, self.TDB_HASH_SIZE, tdb.DEFAULT,
 
579
                                          os.O_RDWR|os.O_CREAT)
 
580
            self.db = mapdbs()[path]
 
581
        try:
 
582
            if int(self.db["version"]) not in (2, 3):
 
583
                trace.warning("SHA Map is incompatible (%s -> %d), rebuilding database.",
 
584
                              self.db["version"], self.TDB_MAP_VERSION)
 
585
                self.db.clear()
 
586
        except KeyError:
 
587
            pass
 
588
        self.db["version"] = str(self.TDB_MAP_VERSION)
 
589
 
 
590
    def start_write_group(self):
 
591
        """Start writing changes."""
 
592
        self.db.transaction_start()
 
593
 
 
594
    def commit_write_group(self):
 
595
        """Commit any pending changes."""
 
596
        self.db.transaction_commit()
 
597
 
 
598
    def abort_write_group(self):
 
599
        """Abort any pending changes."""
 
600
        self.db.transaction_cancel()
 
601
 
 
602
    def __repr__(self):
 
603
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
604
 
 
605
    def lookup_commit(self, revid):
 
606
        return sha_to_hex(self.db["commit\0" + revid][:20])
 
607
 
 
608
    def lookup_blob_id(self, fileid, revision):
 
609
        return sha_to_hex(self.db["\0".join(("blob", fileid, revision))])
 
610
 
 
611
    def lookup_git_sha(self, sha):
 
612
        """Lookup a Git sha in the database.
 
613
 
 
614
        :param sha: Git object sha
 
615
        :return: (type, type_data) with type_data:
 
616
            commit: revid, tree sha
 
617
            blob: fileid, revid
 
618
            tree: fileid, revid
 
619
        """
 
620
        if len(sha) == 40:
 
621
            sha = hex_to_sha(sha)
 
622
        value = self.db["git\0" + sha]
 
623
        for data in value.splitlines():
 
624
            data = data.split("\0")
 
625
            if data[0] == "commit":
 
626
                if len(data) == 3:
 
627
                    yield (data[0], (data[1], data[2], {}))
 
628
                else:
 
629
                    yield (data[0], (data[1], data[2], {"testament3-sha1": data[3]}))
 
630
            elif data[0] in ("tree", "blob"):
 
631
                yield (data[0], tuple(data[1:]))
 
632
            else:
 
633
                raise AssertionError("unknown type %r" % data[0])
 
634
 
 
635
    def missing_revisions(self, revids):
 
636
        ret = set()
 
637
        for revid in revids:
 
638
            if self.db.get("commit\0" + revid) is None:
 
639
                ret.add(revid)
 
640
        return ret
 
641
 
 
642
    def revids(self):
 
643
        """List the revision ids known."""
 
644
        for key in self.db.iterkeys():
 
645
            if key.startswith("commit\0"):
 
646
                yield key[7:]
 
647
 
 
648
    def sha1s(self):
 
649
        """List the SHA1s."""
 
650
        for key in self.db.iterkeys():
 
651
            if key.startswith("git\0"):
 
652
                yield sha_to_hex(key[4:])
 
653
 
 
654
 
 
655
class VersionedFilesContentCache(ContentCache):
 
656
 
 
657
    def __init__(self, vf):
 
658
        self._vf = vf
 
659
 
 
660
    def add(self, obj):
 
661
        self._vf.insert_record_stream(
 
662
            [versionedfile.ChunkedContentFactory((obj.id,), [], None,
 
663
                obj.as_legacy_object_chunks())])
 
664
 
 
665
    def __getitem__(self, sha):
 
666
        stream = self._vf.get_record_stream([(sha,)], 'unordered', True)
 
667
        entry = stream.next() 
 
668
        if entry.storage_kind == 'absent':
 
669
            raise KeyError(sha)
 
670
        return ShaFile._parse_legacy_object(entry.get_bytes_as('fulltext'))
 
671
 
 
672
 
 
673
class GitObjectStoreContentCache(ContentCache):
 
674
 
 
675
    def __init__(self, store):
 
676
        self.store = store
 
677
 
 
678
    def add_multi(self, objs):
 
679
        self.store.add_objects(objs)
 
680
 
 
681
    def add(self, obj, path):
 
682
        self.store.add_object(obj)
 
683
 
 
684
    def __getitem__(self, sha):
 
685
        return self.store[sha]
 
686
 
 
687
 
 
688
class IndexCacheUpdater(CacheUpdater):
 
689
 
 
690
    def __init__(self, cache, rev):
 
691
        self.cache = cache
 
692
        self.revid = rev.revision_id
 
693
        self.parent_revids = rev.parent_ids
 
694
        self._commit = None
 
695
        self._entries = []
 
696
        self._cache_objs = set()
 
697
 
 
698
    def add_object(self, obj, ie, path):
 
699
        if obj.type_name == "commit":
 
700
            self._commit = obj
 
701
            assert type(ie) is dict
 
702
            self.cache.idmap._add_git_sha(obj.id, "commit",
 
703
                (self.revid, obj.tree, ie))
 
704
            self.cache.idmap._add_node(("commit", self.revid, "X"),
 
705
                " ".join((obj.id, obj.tree)))
 
706
            self._cache_objs.add((obj, path))
 
707
        elif obj.type_name == "blob":
 
708
            self.cache.idmap._add_git_sha(obj.id, "blob",
 
709
                (ie.file_id, ie.revision))
 
710
            self.cache.idmap._add_node(("blob", ie.file_id, ie.revision), obj.id)
 
711
            if ie.kind == "symlink":
 
712
                self._cache_objs.add((obj, path))
 
713
        elif obj.type_name == "tree":
 
714
            self.cache.idmap._add_git_sha(obj.id, "tree",
 
715
                (ie.file_id, self.revid))
 
716
            self._cache_objs.add((obj, path))
 
717
        else:
 
718
            raise AssertionError
 
719
 
 
720
    def finish(self):
 
721
        self.cache.content_cache.add_multi(self._cache_objs)
 
722
        return self._commit
 
723
 
 
724
 
 
725
class IndexBzrGitCache(BzrGitCache):
 
726
 
 
727
    def __init__(self, transport=None):
 
728
        mapper = versionedfile.ConstantMapper("trees")
 
729
        shamap = IndexGitShaMap(transport.clone('index'))
 
730
        #trees_store = knit.make_file_factory(True, mapper)(transport)
 
731
        #content_cache = VersionedFilesContentCache(trees_store)
 
732
        from bzrlib.plugins.git.transportgit import TransportObjectStore
 
733
        store = TransportObjectStore(transport.clone('objects'))
 
734
        content_cache = GitObjectStoreContentCache(store)
 
735
        super(IndexBzrGitCache, self).__init__(shamap, content_cache,
 
736
                IndexCacheUpdater)
 
737
 
 
738
 
 
739
class IndexGitCacheFormat(BzrGitCacheFormat):
 
740
 
 
741
    def get_format_string(self):
 
742
        return 'bzr-git sha map with git object cache version 1\n'
 
743
 
 
744
    def initialize(self, transport):
 
745
        super(IndexGitCacheFormat, self).initialize(transport)
 
746
        transport.mkdir('index')
 
747
        transport.mkdir('objects')
 
748
        from bzrlib.plugins.git.transportgit import TransportObjectStore
 
749
        TransportObjectStore.init(transport.clone('objects'))
 
750
 
 
751
    def open(self, transport):
 
752
        return IndexBzrGitCache(transport)
 
753
 
 
754
 
 
755
class IndexGitShaMap(GitShaMap):
 
756
    """SHA Map that uses the Bazaar APIs to store a cache.
 
757
 
 
758
    BTree Index file with the following contents:
 
759
 
 
760
    ("git", <sha1>) -> "<type> <type-data1> <type-data2>"
 
761
    ("commit", <revid>) -> "<sha1> <tree-id>"
 
762
    ("blob", <fileid>, <revid>) -> <sha1>
 
763
 
 
764
    """
 
765
 
 
766
    def __init__(self, transport=None):
 
767
        if transport is None:
 
768
            self._transport = None
 
769
            self._index = _mod_index.InMemoryGraphIndex(0, key_elements=3)
 
770
            self._builder = self._index
 
771
        else:
 
772
            self._builder = None
 
773
            self._transport = transport
 
774
            self._index = _mod_index.CombinedGraphIndex([])
 
775
            for name in self._transport.list_dir("."):
 
776
                if not name.endswith(".rix"):
 
777
                    continue
 
778
                x = _mod_btree_index.BTreeGraphIndex(self._transport, name,
 
779
                    self._transport.stat(name).st_size)
 
780
                self._index.insert_index(0, x)
 
781
 
 
782
    @classmethod
 
783
    def from_repository(cls, repository):
 
784
        transport = getattr(repository, "_transport", None)
 
785
        if transport is not None:
 
786
            try:
 
787
                transport.mkdir('git')
 
788
            except bzrlib.errors.FileExists:
 
789
                pass
 
790
            return cls(transport.clone('git'))
 
791
        from bzrlib.transport import get_transport
 
792
        return cls(get_transport(get_cache_dir()))
 
793
 
 
794
    def __repr__(self):
 
795
        if self._transport is not None:
 
796
            return "%s(%r)" % (self.__class__.__name__, self._transport.base)
 
797
        else:
 
798
            return "%s()" % (self.__class__.__name__)
 
799
 
 
800
    def repack(self):
 
801
        assert self._builder is None
 
802
        self.start_write_group()
 
803
        for _, key, value in self._index.iter_all_entries():
 
804
            self._builder.add_node(key, value)
 
805
        to_remove = []
 
806
        for name in self._transport.list_dir('.'):
 
807
            if name.endswith('.rix'):
 
808
                to_remove.append(name)
 
809
        self.commit_write_group()
 
810
        del self._index.indices[1:]
 
811
        for name in to_remove:
 
812
            self._transport.rename(name, name + '.old')
 
813
 
 
814
    def start_write_group(self):
 
815
        assert self._builder is None
 
816
        self._builder = _mod_btree_index.BTreeBuilder(0, key_elements=3)
 
817
        self._name = osutils.sha()
 
818
 
 
819
    def commit_write_group(self):
 
820
        assert self._builder is not None
 
821
        stream = self._builder.finish()
 
822
        name = self._name.hexdigest() + ".rix"
 
823
        size = self._transport.put_file(name, stream)
 
824
        index = _mod_btree_index.BTreeGraphIndex(self._transport, name, size)
 
825
        self._index.insert_index(0, index)
 
826
        self._builder = None
 
827
        self._name = None
 
828
 
 
829
    def abort_write_group(self):
 
830
        assert self._builder is not None
 
831
        self._builder = None
 
832
        self._name = None
 
833
 
 
834
    def _add_node(self, key, value):
 
835
        try:
 
836
            self._builder.add_node(key, value)
 
837
        except bzrlib.errors.BadIndexDuplicateKey:
 
838
            # Multiple bzr objects can have the same contents
 
839
            return True
 
840
        else:
 
841
            return False
 
842
 
 
843
    def _get_entry(self, key):
 
844
        entries = self._index.iter_entries([key])
 
845
        try:
 
846
            return entries.next()[2]
 
847
        except StopIteration:
 
848
            if self._builder is None:
 
849
                raise KeyError
 
850
            entries = self._builder.iter_entries([key])
 
851
            try:
 
852
                return entries.next()[2]
 
853
            except StopIteration:
 
854
                raise KeyError
 
855
 
 
856
    def _iter_entries_prefix(self, prefix):
 
857
        for entry in self._index.iter_entries_prefix([prefix]):
 
858
            yield (entry[1], entry[2])
 
859
        if self._builder is not None:
 
860
            for entry in self._builder.iter_entries_prefix([prefix]):
 
861
                yield (entry[1], entry[2])
 
862
 
 
863
    def lookup_commit(self, revid):
 
864
        return self._get_entry(("commit", revid, "X"))[:40]
 
865
 
 
866
    def _add_git_sha(self, hexsha, type, type_data):
 
867
        if hexsha is not None:
 
868
            self._name.update(hexsha)
 
869
            if type == "commit":
 
870
                td = (type_data[0], type_data[1], type_data[2]["testament3-sha1"])
 
871
            else:
 
872
                td = type_data
 
873
            self._add_node(("git", hexsha, "X"), " ".join((type,) + td))
 
874
        else:
 
875
            # This object is not represented in Git - perhaps an empty
 
876
            # directory?
 
877
            self._name.update(type + " ".join(type_data))
 
878
 
 
879
    def lookup_blob_id(self, fileid, revision):
 
880
        return self._get_entry(("blob", fileid, revision))
 
881
 
 
882
    def lookup_git_sha(self, sha):
 
883
        if len(sha) == 20:
 
884
            sha = sha_to_hex(sha)
 
885
        found = False
 
886
        for key, value in self._iter_entries_prefix(("git", sha, None)):
 
887
            found = True
 
888
            data = value.split(" ", 3)
 
889
            if data[0] == "commit":
 
890
                yield ("commit", (data[1], data[2], {"testament3-sha1": data[3]}))
 
891
            else:
 
892
                yield (data[0], tuple(data[1:]))
 
893
        if not found:
 
894
            raise KeyError(sha)
 
895
 
 
896
    def revids(self):
 
897
        """List the revision ids known."""
 
898
        for key, value in self._iter_entries_prefix(("commit", None, None)):
 
899
            yield key[1]
 
900
 
 
901
    def missing_revisions(self, revids):
 
902
        """Return set of all the revisions that are not present."""
 
903
        missing_revids = set(revids)
 
904
        for _, key, value in self._index.iter_entries((
 
905
            ("commit", revid, "X") for revid in revids)):
 
906
            missing_revids.remove(key[1])
 
907
        return missing_revids
 
908
 
 
909
    def sha1s(self):
 
910
        """List the SHA1s."""
 
911
        for key, value in self._iter_entries_prefix(("git", None, None)):
 
912
            yield key[1]
 
913
 
 
914
 
 
915
formats = registry.Registry()
 
916
formats.register(TdbGitCacheFormat().get_format_string(),
 
917
    TdbGitCacheFormat())
 
918
formats.register(SqliteGitCacheFormat().get_format_string(),
 
919
    SqliteGitCacheFormat())
 
920
formats.register(IndexGitCacheFormat().get_format_string(),
 
921
    IndexGitCacheFormat())
 
922
# In the future, this will become the default:
 
923
# formats.register('default', IndexGitCacheFormat())
 
924
try:
 
925
    import tdb
 
926
except ImportError:
 
927
    formats.register('default', SqliteGitCacheFormat())
 
928
else:
 
929
    formats.register('default', TdbGitCacheFormat())
 
930
 
 
931
 
 
932
 
 
933
def migrate_ancient_formats(repo_transport):
 
934
    # Prefer migrating git.db over git.tdb, since the latter may not 
 
935
    # be openable on some platforms.
 
936
    if repo_transport.has("git.db"):
 
937
        SqliteGitCacheFormat().initialize(repo_transport.clone("git"))
 
938
        repo_transport.rename("git.db", "git/idmap.db")
 
939
    elif repo_transport.has("git.tdb"):
 
940
        TdbGitCacheFormat().initialize(repo_transport.clone("git"))
 
941
        repo_transport.rename("git.tdb", "git/idmap.tdb")
 
942
 
 
943
 
 
944
def remove_readonly_transport_decorator(transport):
 
945
    if transport.is_readonly():
 
946
        return transport._decorated
 
947
    return transport
 
948
 
 
949
 
 
950
def from_repository(repository):
 
951
    """Open a cache file for a repository.
 
952
 
 
953
    If the repository is remote and there is no transport available from it
 
954
    this will use a local file in the users cache directory
 
955
    (typically ~/.cache/bazaar/git/)
 
956
 
 
957
    :param repository: A repository object
 
958
    """
 
959
    repo_transport = getattr(repository, "_transport", None)
 
960
    if repo_transport is not None:
 
961
        # Migrate older cache formats
 
962
        repo_transport = remove_readonly_transport_decorator(repo_transport)
 
963
        try:
 
964
            repo_transport.mkdir("git")
 
965
        except bzrlib.errors.FileExists:
 
966
            pass
 
967
        else:
 
968
            migrate_ancient_formats(repo_transport)
 
969
    return BzrGitCacheFormat.from_repository(repository)