/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: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

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)