/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-11-16 10:50:21 UTC
  • mfrom: (7164 work)
  • mto: This revision was merged to the branch mainline in revision 7165.
  • Revision ID: jelmer@jelmer.uk-20181116105021-xl419v2rh4aus1au
Merge trunk.

Show diffs side-by-side

added added

removed removed

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