/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/git/cache.py

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

Show diffs side-by-side

added added

removed removed

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