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

  • Committer: Jelmer Vernooij
  • Date: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

Show diffs side-by-side

added added

removed removed

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