/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: Marius Kruger
  • Date: 2010-07-10 21:28:56 UTC
  • mto: (5384.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 5385.
  • Revision ID: marius.kruger@enerweb.co.za-20100710212856-uq4ji3go0u5se7hx
* Update documentation
* add NEWS

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