/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: 2018-11-11 04:08:32 UTC
  • mto: (7143.16.20 even-more-cleanups)
  • mto: This revision was merged to the branch mainline in revision 7175.
  • Revision ID: jelmer@jelmer.uk-20181111040832-nsljjynzzwmznf3h
Run autopep8.

Show diffs side-by-side

added added

removed removed

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