/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: 2019-02-04 01:01:24 UTC
  • mto: This revision was merged to the branch mainline in revision 7268.
  • Revision ID: jelmer@jelmer.uk-20190204010124-ni0i4qc6f5tnbvux
Fix source tests.

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