/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/git/cache.py

  • Committer: Jelmer Vernooij
  • Date: 2020-04-05 19:11:34 UTC
  • mto: (7490.7.16 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200405191134-0aebh8ikiwygxma5
Populate the .gitignore file.

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