/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-03-04 07:14:58 UTC
  • mto: (7290.1.13 work)
  • mto: This revision was merged to the branch mainline in revision 7295.
  • Revision ID: jelmer@jelmer.uk-20190304071458-2zp29edsanbi1560
Fix tests on Python 3.

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(".")
 
617
        except bzr_errors.NotLocalUrl:
 
618
            basepath = get_cache_dir()
 
619
        try:
 
620
            return TdbBzrGitCache(os.path.join(basepath, "idmap.tdb"))
 
621
        except ImportError:
 
622
            raise ImportError(
 
623
                "Unable to open existing bzr-git cache because 'tdb' is not "
 
624
                "installed.")
 
625
 
 
626
 
 
627
class TdbGitShaMap(GitShaMap):
 
628
    """SHA Map that uses a TDB database.
 
629
 
 
630
    Entries:
 
631
 
 
632
    "git <sha1>" -> "<type> <type-data1> <type-data2>"
 
633
    "commit revid" -> "<sha1> <tree-id>"
 
634
    "tree fileid revid" -> "<sha1>"
 
635
    "blob fileid revid" -> "<sha1>"
 
636
    """
 
637
 
 
638
    TDB_MAP_VERSION = 3
 
639
    TDB_HASH_SIZE = 50000
 
640
 
 
641
    def __init__(self, path=None):
 
642
        import tdb
 
643
        self.path = path
 
644
        if path is None:
 
645
            self.db = {}
 
646
        else:
 
647
            if path not in mapdbs():
 
648
                mapdbs()[path] = tdb.Tdb(path, self.TDB_HASH_SIZE, tdb.DEFAULT,
 
649
                                         os.O_RDWR | os.O_CREAT)
 
650
            self.db = mapdbs()[path]
 
651
        try:
 
652
            if int(self.db[b"version"]) not in (2, 3):
 
653
                trace.warning(
 
654
                    "SHA Map is incompatible (%s -> %d), rebuilding database.",
 
655
                    self.db[b"version"], self.TDB_MAP_VERSION)
 
656
                self.db.clear()
 
657
        except KeyError:
 
658
            pass
 
659
        self.db[b"version"] = b'%d' % self.TDB_MAP_VERSION
 
660
 
 
661
    def start_write_group(self):
 
662
        """Start writing changes."""
 
663
        self.db.transaction_start()
 
664
 
 
665
    def commit_write_group(self):
 
666
        """Commit any pending changes."""
 
667
        self.db.transaction_commit()
 
668
 
 
669
    def abort_write_group(self):
 
670
        """Abort any pending changes."""
 
671
        self.db.transaction_cancel()
 
672
 
 
673
    def __repr__(self):
 
674
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
675
 
 
676
    def lookup_commit(self, revid):
 
677
        try:
 
678
            return sha_to_hex(self.db[b"commit\0" + revid][:20])
 
679
        except KeyError:
 
680
            raise KeyError("No cache entry for %r" % revid)
 
681
 
 
682
    def lookup_blob_id(self, fileid, revision):
 
683
        return sha_to_hex(self.db[b"\0".join((b"blob", fileid, revision))])
 
684
 
 
685
    def lookup_git_sha(self, sha):
 
686
        """Lookup a Git sha in the database.
 
687
 
 
688
        :param sha: Git object sha
 
689
        :return: (type, type_data) with type_data:
 
690
            commit: revid, tree sha
 
691
            blob: fileid, revid
 
692
            tree: fileid, revid
 
693
        """
 
694
        if len(sha) == 40:
 
695
            sha = hex_to_sha(sha)
 
696
        value = self.db[b"git\0" + sha]
 
697
        for data in value.splitlines():
 
698
            data = data.split(b"\0")
 
699
            type_name = data[0].decode('ascii')
 
700
            if type_name == "commit":
 
701
                if len(data) == 3:
 
702
                    yield (type_name, (data[1], data[2], {}))
 
703
                else:
 
704
                    yield (type_name, (data[1], data[2],
 
705
                                       {"testament3-sha1": data[3]}))
 
706
            elif type_name in ("tree", "blob"):
 
707
                yield (type_name, tuple(data[1:]))
 
708
            else:
 
709
                raise AssertionError("unknown type %r" % type_name)
 
710
 
 
711
    def missing_revisions(self, revids):
 
712
        ret = set()
 
713
        for revid in revids:
 
714
            if self.db.get(b"commit\0" + revid) is None:
 
715
                ret.add(revid)
 
716
        return ret
 
717
 
 
718
    def _keys(self):
 
719
        try:
 
720
            return self.db.keys()
 
721
        except AttributeError:  # python < 3
 
722
            return self.db.iterkeys()
 
723
 
 
724
    def revids(self):
 
725
        """List the revision ids known."""
 
726
        for key in self._keys():
 
727
            if key.startswith(b"commit\0"):
 
728
                yield key[7:]
 
729
 
 
730
    def sha1s(self):
 
731
        """List the SHA1s."""
 
732
        for key in self._keys():
 
733
            if key.startswith(b"git\0"):
 
734
                yield sha_to_hex(key[4:])
 
735
 
 
736
 
 
737
class VersionedFilesContentCache(ContentCache):
 
738
 
 
739
    def __init__(self, vf):
 
740
        self._vf = vf
 
741
 
 
742
    def add(self, obj):
 
743
        self._vf.insert_record_stream(
 
744
            [versionedfile.ChunkedContentFactory(
 
745
                (obj.id,), [], None, obj.as_legacy_object_chunks())])
 
746
 
 
747
    def __getitem__(self, sha):
 
748
        stream = self._vf.get_record_stream([(sha,)], 'unordered', True)
 
749
        entry = next(stream)
 
750
        if entry.storage_kind == 'absent':
 
751
            raise KeyError(sha)
 
752
        return ShaFile._parse_legacy_object(entry.get_bytes_as('fulltext'))
 
753
 
 
754
 
 
755
class IndexCacheUpdater(CacheUpdater):
 
756
 
 
757
    def __init__(self, cache, rev):
 
758
        self.cache = cache
 
759
        self.revid = rev.revision_id
 
760
        self.parent_revids = rev.parent_ids
 
761
        self._commit = None
 
762
        self._entries = []
 
763
 
 
764
    def add_object(self, obj, bzr_key_data, path):
 
765
        if isinstance(obj, tuple):
 
766
            (type_name, hexsha) = obj
 
767
        else:
 
768
            type_name = obj.type_name.decode('ascii')
 
769
            hexsha = obj.id
 
770
        if type_name == "commit":
 
771
            self._commit = obj
 
772
            if type(bzr_key_data) is not dict:
 
773
                raise TypeError(bzr_key_data)
 
774
            self.cache.idmap._add_git_sha(hexsha, b"commit",
 
775
                                          (self.revid, obj.tree, bzr_key_data))
 
776
            self.cache.idmap._add_node((b"commit", self.revid, b"X"),
 
777
                                       b" ".join((hexsha, obj.tree)))
 
778
        elif type_name == "blob":
 
779
            self.cache.idmap._add_git_sha(hexsha, b"blob", bzr_key_data)
 
780
            self.cache.idmap._add_node((b"blob", bzr_key_data[0],
 
781
                                        bzr_key_data[1]), hexsha)
 
782
        elif type_name == "tree":
 
783
            self.cache.idmap._add_git_sha(hexsha, b"tree", bzr_key_data)
 
784
        else:
 
785
            raise AssertionError
 
786
 
 
787
    def finish(self):
 
788
        return self._commit
 
789
 
 
790
 
 
791
class IndexBzrGitCache(BzrGitCache):
 
792
 
 
793
    def __init__(self, transport=None):
 
794
        shamap = IndexGitShaMap(transport.clone('index'))
 
795
        super(IndexBzrGitCache, self).__init__(shamap, IndexCacheUpdater)
 
796
 
 
797
 
 
798
class IndexGitCacheFormat(BzrGitCacheFormat):
 
799
 
 
800
    def get_format_string(self):
 
801
        return b'bzr-git sha map with git object cache version 1\n'
 
802
 
 
803
    def initialize(self, transport):
 
804
        super(IndexGitCacheFormat, self).initialize(transport)
 
805
        transport.mkdir('index')
 
806
        transport.mkdir('objects')
 
807
        from .transportgit import TransportObjectStore
 
808
        TransportObjectStore.init(transport.clone('objects'))
 
809
 
 
810
    def open(self, transport):
 
811
        return IndexBzrGitCache(transport)
 
812
 
 
813
 
 
814
class IndexGitShaMap(GitShaMap):
 
815
    """SHA Map that uses the Bazaar APIs to store a cache.
 
816
 
 
817
    BTree Index file with the following contents:
 
818
 
 
819
    ("git", <sha1>, "X") -> "<type> <type-data1> <type-data2>"
 
820
    ("commit", <revid>, "X") -> "<sha1> <tree-id>"
 
821
    ("blob", <fileid>, <revid>) -> <sha1>
 
822
 
 
823
    """
 
824
 
 
825
    def __init__(self, transport=None):
 
826
        self._name = None
 
827
        if transport is None:
 
828
            self._transport = None
 
829
            self._index = _mod_index.InMemoryGraphIndex(0, key_elements=3)
 
830
            self._builder = self._index
 
831
        else:
 
832
            self._builder = None
 
833
            self._transport = transport
 
834
            self._index = _mod_index.CombinedGraphIndex([])
 
835
            for name in self._transport.list_dir("."):
 
836
                if not name.endswith(".rix"):
 
837
                    continue
 
838
                x = _mod_btree_index.BTreeGraphIndex(
 
839
                    self._transport, name, self._transport.stat(name).st_size)
 
840
                self._index.insert_index(0, x)
 
841
 
 
842
    @classmethod
 
843
    def from_repository(cls, repository):
 
844
        transport = getattr(repository, "_transport", None)
 
845
        if transport is not None:
 
846
            try:
 
847
                transport.mkdir('git')
 
848
            except bzr_errors.FileExists:
 
849
                pass
 
850
            return cls(transport.clone('git'))
 
851
        from ..transport import get_transport
 
852
        return cls(get_transport(get_cache_dir()))
 
853
 
 
854
    def __repr__(self):
 
855
        if self._transport is not None:
 
856
            return "%s(%r)" % (self.__class__.__name__, self._transport.base)
 
857
        else:
 
858
            return "%s()" % (self.__class__.__name__)
 
859
 
 
860
    def repack(self):
 
861
        if self._builder is not None:
 
862
            raise bzr_errors.BzrError('builder already open')
 
863
        self.start_write_group()
 
864
        self._builder.add_nodes(
 
865
            ((key, value) for (_, key, value) in
 
866
                self._index.iter_all_entries()))
 
867
        to_remove = []
 
868
        for name in self._transport.list_dir('.'):
 
869
            if name.endswith('.rix'):
 
870
                to_remove.append(name)
 
871
        self.commit_write_group()
 
872
        del self._index.indices[1:]
 
873
        for name in to_remove:
 
874
            self._transport.rename(name, name + '.old')
 
875
 
 
876
    def start_write_group(self):
 
877
        if self._builder is not None:
 
878
            raise bzr_errors.BzrError('builder already open')
 
879
        self._builder = _mod_btree_index.BTreeBuilder(0, key_elements=3)
 
880
        self._name = osutils.sha()
 
881
 
 
882
    def commit_write_group(self):
 
883
        if self._builder is None:
 
884
            raise bzr_errors.BzrError('builder not open')
 
885
        stream = self._builder.finish()
 
886
        name = self._name.hexdigest() + ".rix"
 
887
        size = self._transport.put_file(name, stream)
 
888
        index = _mod_btree_index.BTreeGraphIndex(self._transport, name, size)
 
889
        self._index.insert_index(0, index)
 
890
        self._builder = None
 
891
        self._name = None
 
892
 
 
893
    def abort_write_group(self):
 
894
        if self._builder is None:
 
895
            raise bzr_errors.BzrError('builder not open')
 
896
        self._builder = None
 
897
        self._name = None
 
898
 
 
899
    def _add_node(self, key, value):
 
900
        try:
 
901
            self._get_entry(key)
 
902
        except KeyError:
 
903
            self._builder.add_node(key, value)
 
904
            return False
 
905
        else:
 
906
            return True
 
907
 
 
908
    def _get_entry(self, key):
 
909
        entries = self._index.iter_entries([key])
 
910
        try:
 
911
            return next(entries)[2]
 
912
        except StopIteration:
 
913
            if self._builder is None:
 
914
                raise KeyError
 
915
            entries = self._builder.iter_entries([key])
 
916
            try:
 
917
                return next(entries)[2]
 
918
            except StopIteration:
 
919
                raise KeyError
 
920
 
 
921
    def _iter_entries_prefix(self, prefix):
 
922
        for entry in self._index.iter_entries_prefix([prefix]):
 
923
            yield (entry[1], entry[2])
 
924
        if self._builder is not None:
 
925
            for entry in self._builder.iter_entries_prefix([prefix]):
 
926
                yield (entry[1], entry[2])
 
927
 
 
928
    def lookup_commit(self, revid):
 
929
        return self._get_entry((b"commit", revid, b"X"))[:40]
 
930
 
 
931
    def _add_git_sha(self, hexsha, type, type_data):
 
932
        if hexsha is not None:
 
933
            self._name.update(hexsha)
 
934
            if type == b"commit":
 
935
                td = (type_data[0], type_data[1])
 
936
                try:
 
937
                    td += (type_data[2]["testament3-sha1"],)
 
938
                except KeyError:
 
939
                    pass
 
940
            else:
 
941
                td = type_data
 
942
            self._add_node((b"git", hexsha, b"X"), b" ".join((type,) + td))
 
943
        else:
 
944
            # This object is not represented in Git - perhaps an empty
 
945
            # directory?
 
946
            self._name.update(type + b" ".join(type_data))
 
947
 
 
948
    def lookup_blob_id(self, fileid, revision):
 
949
        return self._get_entry((b"blob", fileid, revision))
 
950
 
 
951
    def lookup_git_sha(self, sha):
 
952
        if len(sha) == 20:
 
953
            sha = sha_to_hex(sha)
 
954
        value = self._get_entry((b"git", sha, b"X"))
 
955
        data = value.split(b" ", 3)
 
956
        if data[0] == b"commit":
 
957
            try:
 
958
                if data[3]:
 
959
                    verifiers = {"testament3-sha1": data[3]}
 
960
                else:
 
961
                    verifiers = {}
 
962
            except IndexError:
 
963
                verifiers = {}
 
964
            yield ("commit", (data[1], data[2], verifiers))
 
965
        else:
 
966
            yield (data[0].decode('ascii'), tuple(data[1:]))
 
967
 
 
968
    def revids(self):
 
969
        """List the revision ids known."""
 
970
        for key, value in self._iter_entries_prefix((b"commit", None, None)):
 
971
            yield key[1]
 
972
 
 
973
    def missing_revisions(self, revids):
 
974
        """Return set of all the revisions that are not present."""
 
975
        missing_revids = set(revids)
 
976
        for _, key, value in self._index.iter_entries((
 
977
                (b"commit", revid, b"X") for revid in revids)):
 
978
            missing_revids.remove(key[1])
 
979
        return missing_revids
 
980
 
 
981
    def sha1s(self):
 
982
        """List the SHA1s."""
 
983
        for key, value in self._iter_entries_prefix((b"git", None, None)):
 
984
            yield key[1]
 
985
 
 
986
 
 
987
formats = registry.Registry()
 
988
formats.register(TdbGitCacheFormat().get_format_string(),
 
989
                 TdbGitCacheFormat())
 
990
formats.register(SqliteGitCacheFormat().get_format_string(),
 
991
                 SqliteGitCacheFormat())
 
992
formats.register(IndexGitCacheFormat().get_format_string(),
 
993
                 IndexGitCacheFormat())
 
994
# In the future, this will become the default:
 
995
formats.register('default', IndexGitCacheFormat())
 
996
 
 
997
 
 
998
def migrate_ancient_formats(repo_transport):
 
999
    # Migrate older cache formats
 
1000
    repo_transport = remove_readonly_transport_decorator(repo_transport)
 
1001
    has_sqlite = repo_transport.has("git.db")
 
1002
    has_tdb = repo_transport.has("git.tdb")
 
1003
    if not has_sqlite or has_tdb:
 
1004
        return
 
1005
    try:
 
1006
        repo_transport.mkdir("git")
 
1007
    except bzr_errors.FileExists:
 
1008
        return
 
1009
    # Prefer migrating git.db over git.tdb, since the latter may not
 
1010
    # be openable on some platforms.
 
1011
    if has_sqlite:
 
1012
        SqliteGitCacheFormat().initialize(repo_transport.clone("git"))
 
1013
        repo_transport.rename("git.db", "git/idmap.db")
 
1014
    elif has_tdb:
 
1015
        TdbGitCacheFormat().initialize(repo_transport.clone("git"))
 
1016
        repo_transport.rename("git.tdb", "git/idmap.tdb")
 
1017
 
 
1018
 
 
1019
def remove_readonly_transport_decorator(transport):
 
1020
    if transport.is_readonly():
 
1021
        try:
 
1022
            return transport._decorated
 
1023
        except AttributeError:
 
1024
            raise bzr_errors.ReadOnlyError(transport)
 
1025
    return transport
 
1026
 
 
1027
 
 
1028
def from_repository(repository):
 
1029
    """Open a cache file for a repository.
 
1030
 
 
1031
    If the repository is remote and there is no transport available from it
 
1032
    this will use a local file in the users cache directory
 
1033
    (typically ~/.cache/bazaar/git/)
 
1034
 
 
1035
    :param repository: A repository object
 
1036
    """
 
1037
    repo_transport = getattr(repository, "_transport", None)
 
1038
    if repo_transport is not None:
 
1039
        try:
 
1040
            migrate_ancient_formats(repo_transport)
 
1041
        except bzr_errors.ReadOnlyError:
 
1042
            pass  # Not much we can do
 
1043
    return BzrGitCacheFormat.from_repository(repository)