/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/plugins/git/cache.py

  • Committer: Martin
  • Date: 2018-07-01 10:53:23 UTC
  • mto: This revision was merged to the branch mainline in revision 7016.
  • Revision ID: gzlist@googlemail.com-20180701105323-dawmz8ngtj3qzdo7
Tweak copyright headers

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 path not in mapdbs():
 
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.decode('ascii')
 
533
            sha = obj.sha().digest()
 
534
        if type_name == "commit":
 
535
            self.db[b"commit\0" + self.revid] = b"\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[b"\0".join((b"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 = b"\0".join((type_name.encode('ascii'), ) + type_data) + b"\n"
 
556
        key = b"git\0" + sha
 
557
        try:
 
558
            oldval = self.db[key]
 
559
        except KeyError:
 
560
            self.db[key] = entry
 
561
        else:
 
562
            if not oldval.endswith(b'\n'):
 
563
                self.db[key] = b"".join([oldval, b"\n", entry])
 
564
            else:
 
565
                self.db[key] = b"".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 path not in mapdbs():
 
618
                mapdbs()[path] = tdb.Tdb(path, self.TDB_HASH_SIZE, tdb.DEFAULT,
 
619
                                          os.O_RDWR|os.O_CREAT)
 
620
            self.db = mapdbs()[path]
 
621
        try:
 
622
            if int(self.db[b"version"]) not in (2, 3):
 
623
                trace.warning("SHA Map is incompatible (%s -> %d), rebuilding database.",
 
624
                              self.db[b"version"], self.TDB_MAP_VERSION)
 
625
                self.db.clear()
 
626
        except KeyError:
 
627
            pass
 
628
        self.db[b"version"] = b'%d' % self.TDB_MAP_VERSION
 
629
 
 
630
    def start_write_group(self):
 
631
        """Start writing changes."""
 
632
        self.db.transaction_start()
 
633
 
 
634
    def commit_write_group(self):
 
635
        """Commit any pending changes."""
 
636
        self.db.transaction_commit()
 
637
 
 
638
    def abort_write_group(self):
 
639
        """Abort any pending changes."""
 
640
        self.db.transaction_cancel()
 
641
 
 
642
    def __repr__(self):
 
643
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
644
 
 
645
    def lookup_commit(self, revid):
 
646
        try:
 
647
            return sha_to_hex(self.db[b"commit\0" + revid][:20])
 
648
        except KeyError:
 
649
            raise KeyError("No cache entry for %r" % revid)
 
650
 
 
651
    def lookup_blob_id(self, fileid, revision):
 
652
        return sha_to_hex(self.db[b"\0".join(("blob", fileid, revision))])
 
653
 
 
654
    def lookup_git_sha(self, sha):
 
655
        """Lookup a Git sha in the database.
 
656
 
 
657
        :param sha: Git object sha
 
658
        :return: (type, type_data) with type_data:
 
659
            commit: revid, tree sha
 
660
            blob: fileid, revid
 
661
            tree: fileid, revid
 
662
        """
 
663
        if len(sha) == 40:
 
664
            sha = hex_to_sha(sha)
 
665
        value = self.db[b"git\0" + sha]
 
666
        for data in value.splitlines():
 
667
            data = data.split(b"\0")
 
668
            type_name = data[0].decode('ascii')
 
669
            if type_name == "commit":
 
670
                if len(data) == 3:
 
671
                    yield (type_name, (data[1], data[2], {}))
 
672
                else:
 
673
                    yield (type_name, (data[1], data[2], {"testament3-sha1": data[3]}))
 
674
            elif type_name in ("tree", "blob"):
 
675
                yield (type_name, tuple(data[1:]))
 
676
            else:
 
677
                raise AssertionError("unknown type %r" % type_name)
 
678
 
 
679
    def missing_revisions(self, revids):
 
680
        ret = set()
 
681
        for revid in revids:
 
682
            if self.db.get(b"commit\0" + revid) is None:
 
683
                ret.add(revid)
 
684
        return ret
 
685
 
 
686
    def revids(self):
 
687
        """List the revision ids known."""
 
688
        for key in self.db.iterkeys():
 
689
            if key.startswith(b"commit\0"):
 
690
                yield key[7:]
 
691
 
 
692
    def sha1s(self):
 
693
        """List the SHA1s."""
 
694
        for key in self.db.iterkeys():
 
695
            if key.startswith(b"git\0"):
 
696
                yield sha_to_hex(key[4:])
 
697
 
 
698
 
 
699
class VersionedFilesContentCache(ContentCache):
 
700
 
 
701
    def __init__(self, vf):
 
702
        self._vf = vf
 
703
 
 
704
    def add(self, obj):
 
705
        self._vf.insert_record_stream(
 
706
            [versionedfile.ChunkedContentFactory((obj.id,), [], None,
 
707
                obj.as_legacy_object_chunks())])
 
708
 
 
709
    def __getitem__(self, sha):
 
710
        stream = self._vf.get_record_stream([(sha,)], 'unordered', True)
 
711
        entry = next(stream)
 
712
        if entry.storage_kind == 'absent':
 
713
            raise KeyError(sha)
 
714
        return ShaFile._parse_legacy_object(entry.get_bytes_as('fulltext'))
 
715
 
 
716
 
 
717
class IndexCacheUpdater(CacheUpdater):
 
718
 
 
719
    def __init__(self, cache, rev):
 
720
        self.cache = cache
 
721
        self.revid = rev.revision_id
 
722
        self.parent_revids = rev.parent_ids
 
723
        self._commit = None
 
724
        self._entries = []
 
725
 
 
726
    def add_object(self, obj, bzr_key_data, path):
 
727
        if isinstance(obj, tuple):
 
728
            (type_name, hexsha) = obj
 
729
        else:
 
730
            type_name = obj.type_name
 
731
            hexsha = obj.id
 
732
        if type_name == "commit":
 
733
            self._commit = obj
 
734
            if type(bzr_key_data) is not dict:
 
735
                raise TypeError(bzr_key_data)
 
736
            self.cache.idmap._add_git_sha(hexsha, "commit",
 
737
                (self.revid, obj.tree, bzr_key_data))
 
738
            self.cache.idmap._add_node(("commit", self.revid, "X"),
 
739
                " ".join((hexsha, obj.tree)))
 
740
        elif type_name == "blob":
 
741
            self.cache.idmap._add_git_sha(hexsha, "blob", bzr_key_data)
 
742
            self.cache.idmap._add_node(("blob", bzr_key_data[0],
 
743
                bzr_key_data[1]), hexsha)
 
744
        elif type_name == "tree":
 
745
            self.cache.idmap._add_git_sha(hexsha, "tree", bzr_key_data)
 
746
        else:
 
747
            raise AssertionError
 
748
 
 
749
    def finish(self):
 
750
        return self._commit
 
751
 
 
752
 
 
753
class IndexBzrGitCache(BzrGitCache):
 
754
 
 
755
    def __init__(self, transport=None):
 
756
        mapper = versionedfile.ConstantMapper("trees")
 
757
        shamap = IndexGitShaMap(transport.clone('index'))
 
758
        from .transportgit import TransportObjectStore
 
759
        super(IndexBzrGitCache, self).__init__(shamap, IndexCacheUpdater)
 
760
 
 
761
 
 
762
class IndexGitCacheFormat(BzrGitCacheFormat):
 
763
 
 
764
    def get_format_string(self):
 
765
        return b'bzr-git sha map with git object cache version 1\n'
 
766
 
 
767
    def initialize(self, transport):
 
768
        super(IndexGitCacheFormat, self).initialize(transport)
 
769
        transport.mkdir('index')
 
770
        transport.mkdir('objects')
 
771
        from .transportgit import TransportObjectStore
 
772
        TransportObjectStore.init(transport.clone('objects'))
 
773
 
 
774
    def open(self, transport):
 
775
        return IndexBzrGitCache(transport)
 
776
 
 
777
 
 
778
class IndexGitShaMap(GitShaMap):
 
779
    """SHA Map that uses the Bazaar APIs to store a cache.
 
780
 
 
781
    BTree Index file with the following contents:
 
782
 
 
783
    ("git", <sha1>, "X") -> "<type> <type-data1> <type-data2>"
 
784
    ("commit", <revid>, "X") -> "<sha1> <tree-id>"
 
785
    ("blob", <fileid>, <revid>) -> <sha1>
 
786
 
 
787
    """
 
788
 
 
789
    def __init__(self, transport=None):
 
790
        self._name = None
 
791
        if transport is None:
 
792
            self._transport = None
 
793
            self._index = _mod_index.InMemoryGraphIndex(0, key_elements=3)
 
794
            self._builder = self._index
 
795
        else:
 
796
            self._builder = None
 
797
            self._transport = transport
 
798
            self._index = _mod_index.CombinedGraphIndex([])
 
799
            for name in self._transport.list_dir("."):
 
800
                if not name.endswith(".rix"):
 
801
                    continue
 
802
                x = _mod_btree_index.BTreeGraphIndex(self._transport, name,
 
803
                    self._transport.stat(name).st_size)
 
804
                self._index.insert_index(0, x)
 
805
 
 
806
    @classmethod
 
807
    def from_repository(cls, repository):
 
808
        transport = getattr(repository, "_transport", None)
 
809
        if transport is not None:
 
810
            try:
 
811
                transport.mkdir('git')
 
812
            except bzr_errors.FileExists:
 
813
                pass
 
814
            return cls(transport.clone('git'))
 
815
        from ...transport import get_transport
 
816
        return cls(get_transport(get_cache_dir()))
 
817
 
 
818
    def __repr__(self):
 
819
        if self._transport is not None:
 
820
            return "%s(%r)" % (self.__class__.__name__, self._transport.base)
 
821
        else:
 
822
            return "%s()" % (self.__class__.__name__)
 
823
 
 
824
    def repack(self):
 
825
        if self._builder is not None:
 
826
            raise errors.BzrError('builder already open')
 
827
        self.start_write_group()
 
828
        self._builder.add_nodes(
 
829
            ((key, value) for (_, key, value) in
 
830
                self._index.iter_all_entries()))
 
831
        to_remove = []
 
832
        for name in self._transport.list_dir('.'):
 
833
            if name.endswith('.rix'):
 
834
                to_remove.append(name)
 
835
        self.commit_write_group()
 
836
        del self._index.indices[1:]
 
837
        for name in to_remove:
 
838
            self._transport.rename(name, name + '.old')
 
839
 
 
840
    def start_write_group(self):
 
841
        if self._builder is not None:
 
842
            raise errors.BzrError('builder already open')
 
843
        self._builder = _mod_btree_index.BTreeBuilder(0, key_elements=3)
 
844
        self._name = osutils.sha()
 
845
 
 
846
    def commit_write_group(self):
 
847
        if self._builder is None:
 
848
            raise errors.BzrError('builder not open')
 
849
        stream = self._builder.finish()
 
850
        name = self._name.hexdigest() + ".rix"
 
851
        size = self._transport.put_file(name, stream)
 
852
        index = _mod_btree_index.BTreeGraphIndex(self._transport, name, size)
 
853
        self._index.insert_index(0, index)
 
854
        self._builder = None
 
855
        self._name = None
 
856
 
 
857
    def abort_write_group(self):
 
858
        if self._builder is None:
 
859
            raise errors.BzrError('builder not open')
 
860
        self._builder = None
 
861
        self._name = None
 
862
 
 
863
    def _add_node(self, key, value):
 
864
        try:
 
865
            self._get_entry(key)
 
866
        except KeyError:
 
867
            self._builder.add_node(key, value)
 
868
            return False
 
869
        else:
 
870
            return True
 
871
 
 
872
    def _get_entry(self, key):
 
873
        entries = self._index.iter_entries([key])
 
874
        try:
 
875
            return next(entries)[2]
 
876
        except StopIteration:
 
877
            if self._builder is None:
 
878
                raise KeyError
 
879
            entries = self._builder.iter_entries([key])
 
880
            try:
 
881
                return next(entries)[2]
 
882
            except StopIteration:
 
883
                raise KeyError
 
884
 
 
885
    def _iter_entries_prefix(self, prefix):
 
886
        for entry in self._index.iter_entries_prefix([prefix]):
 
887
            yield (entry[1], entry[2])
 
888
        if self._builder is not None:
 
889
            for entry in self._builder.iter_entries_prefix([prefix]):
 
890
                yield (entry[1], entry[2])
 
891
 
 
892
    def lookup_commit(self, revid):
 
893
        return self._get_entry(("commit", revid, "X"))[:40]
 
894
 
 
895
    def _add_git_sha(self, hexsha, type, type_data):
 
896
        if hexsha is not None:
 
897
            self._name.update(hexsha)
 
898
            if type == "commit":
 
899
                td = (type_data[0], type_data[1])
 
900
                try:
 
901
                    td += (type_data[2]["testament3-sha1"],)
 
902
                except KeyError:
 
903
                    pass
 
904
            else:
 
905
                td = type_data
 
906
            self._add_node(("git", hexsha, "X"), " ".join((type,) + td))
 
907
        else:
 
908
            # This object is not represented in Git - perhaps an empty
 
909
            # directory?
 
910
            self._name.update(type + " ".join(type_data))
 
911
 
 
912
    def lookup_blob_id(self, fileid, revision):
 
913
        return self._get_entry(("blob", fileid, revision))
 
914
 
 
915
    def lookup_git_sha(self, sha):
 
916
        if len(sha) == 20:
 
917
            sha = sha_to_hex(sha)
 
918
        value = self._get_entry(("git", sha, "X"))
 
919
        data = value.split(" ", 3)
 
920
        if data[0] == "commit":
 
921
            try:
 
922
                if data[3]:
 
923
                    verifiers = {"testament3-sha1": data[3]}
 
924
                else:
 
925
                    verifiers = {}
 
926
            except IndexError:
 
927
                verifiers = {}
 
928
            yield ("commit", (data[1], data[2], verifiers))
 
929
        else:
 
930
            yield (data[0], tuple(data[1:]))
 
931
 
 
932
    def revids(self):
 
933
        """List the revision ids known."""
 
934
        for key, value in self._iter_entries_prefix(("commit", None, None)):
 
935
            yield key[1]
 
936
 
 
937
    def missing_revisions(self, revids):
 
938
        """Return set of all the revisions that are not present."""
 
939
        missing_revids = set(revids)
 
940
        for _, key, value in self._index.iter_entries((
 
941
            ("commit", revid, "X") for revid in revids)):
 
942
            missing_revids.remove(key[1])
 
943
        return missing_revids
 
944
 
 
945
    def sha1s(self):
 
946
        """List the SHA1s."""
 
947
        for key, value in self._iter_entries_prefix(("git", None, None)):
 
948
            yield key[1]
 
949
 
 
950
 
 
951
formats = registry.Registry()
 
952
formats.register(TdbGitCacheFormat().get_format_string(),
 
953
    TdbGitCacheFormat())
 
954
formats.register(SqliteGitCacheFormat().get_format_string(),
 
955
    SqliteGitCacheFormat())
 
956
formats.register(IndexGitCacheFormat().get_format_string(),
 
957
    IndexGitCacheFormat())
 
958
# In the future, this will become the default:
 
959
formats.register('default', IndexGitCacheFormat())
 
960
 
 
961
 
 
962
 
 
963
def migrate_ancient_formats(repo_transport):
 
964
    # Migrate older cache formats
 
965
    repo_transport = remove_readonly_transport_decorator(repo_transport)
 
966
    has_sqlite = repo_transport.has("git.db")
 
967
    has_tdb = repo_transport.has("git.tdb")
 
968
    if not has_sqlite or has_tdb:
 
969
        return
 
970
    try:
 
971
        repo_transport.mkdir("git")
 
972
    except bzr_errors.FileExists:
 
973
        return
 
974
    # Prefer migrating git.db over git.tdb, since the latter may not 
 
975
    # be openable on some platforms.
 
976
    if has_sqlite:
 
977
        SqliteGitCacheFormat().initialize(repo_transport.clone("git"))
 
978
        repo_transport.rename("git.db", "git/idmap.db")
 
979
    elif has_tdb:
 
980
        TdbGitCacheFormat().initialize(repo_transport.clone("git"))
 
981
        repo_transport.rename("git.tdb", "git/idmap.tdb")
 
982
 
 
983
 
 
984
def remove_readonly_transport_decorator(transport):
 
985
    if transport.is_readonly():
 
986
        try:
 
987
            return transport._decorated
 
988
        except AttributeError:
 
989
            raise bzr_errors.ReadOnlyError(transport)
 
990
    return transport
 
991
 
 
992
 
 
993
def from_repository(repository):
 
994
    """Open a cache file for a repository.
 
995
 
 
996
    If the repository is remote and there is no transport available from it
 
997
    this will use a local file in the users cache directory
 
998
    (typically ~/.cache/bazaar/git/)
 
999
 
 
1000
    :param repository: A repository object
 
1001
    """
 
1002
    repo_transport = getattr(repository, "_transport", None)
 
1003
    if repo_transport is not None:
 
1004
        try:
 
1005
            migrate_ancient_formats(repo_transport)
 
1006
        except bzr_errors.ReadOnlyError:
 
1007
            pass # Not much we can do
 
1008
    return BzrGitCacheFormat.from_repository(repository)