/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: Robert Collins
  • Date: 2010-05-06 11:08:10 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506110810-h3j07fh5gmw54s25
Cleaner matcher matching revised unlocking protocol.

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