/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-06-16 15:54:33 UTC
  • mto: This revision was merged to the branch mainline in revision 7347.
  • Revision ID: jelmer@jelmer.uk-20190616155433-bkjxtcejurscyjbu
Use assertIn.

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