/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to breezy/git/cache.py

  • Committer: Jelmer Vernooij
  • Date: 2018-02-18 21:42:57 UTC
  • mto: This revision was merged to the branch mainline in revision 6859.
  • Revision ID: jelmer@jelmer.uk-20180218214257-jpevutp1wa30tz3v
Update TODO to reference Breezy, not Bazaar.

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