/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-06-14 17:59:16 UTC
  • mto: This revision was merged to the branch mainline in revision 7065.
  • Revision ID: jelmer@jelmer.uk-20180614175916-a2e2xh5k533guq1x
Move breezy.plugins.git to breezy.git.

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