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

  • Committer: Jelmer Vernooij
  • Date: 2018-04-02 14:59:43 UTC
  • mto: (0.200.1913 work)
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@jelmer.uk-20180402145943-s5jmpbvvf1x42pao
Just don't touch the URL if it's already a valid URL.

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, "bazaar", "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), 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, content_cache, cache_updater_klass):
 
266
        self.idmap = idmap
 
267
        self.content_cache = content_cache
 
268
        self._cache_updater_klass = cache_updater_klass
 
269
 
 
270
    def get_updater(self, rev):
 
271
        """Update an object that implements the CacheUpdater interface for 
 
272
        updating this cache.
 
273
        """
 
274
        return self._cache_updater_klass(self, rev)
 
275
 
 
276
 
 
277
DictBzrGitCache = lambda: BzrGitCache(DictGitShaMap(), None, DictCacheUpdater)
 
278
 
 
279
 
 
280
class DictCacheUpdater(CacheUpdater):
 
281
    """Cache updater for dict-based caches."""
 
282
 
 
283
    def __init__(self, cache, rev):
 
284
        self.cache = cache
 
285
        self.revid = rev.revision_id
 
286
        self.parent_revids = rev.parent_ids
 
287
        self._commit = None
 
288
        self._entries = []
 
289
 
 
290
    def add_object(self, obj, bzr_key_data, path):
 
291
        if obj.type_name == "commit":
 
292
            self._commit = obj
 
293
            if type(bzr_key_data) is not dict:
 
294
                raise TypeError(bzr_key_data)
 
295
            key = self.revid
 
296
            type_data = (self.revid, self._commit.tree, bzr_key_data)
 
297
            self.cache.idmap._by_revid[self.revid] = obj.id
 
298
        elif obj.type_name in ("blob", "tree"):
 
299
            if bzr_key_data is not None:
 
300
                if obj.type_name == "blob":
 
301
                    revision = bzr_key_data[1]
 
302
                else:
 
303
                    revision = self.revid
 
304
                key = type_data = (bzr_key_data[0], revision)
 
305
                self.cache.idmap._by_fileid.setdefault(type_data[1], {})[type_data[0]] = obj.id
 
306
        else:
 
307
            raise AssertionError
 
308
        entry = (obj.type_name, type_data)
 
309
        self.cache.idmap._by_sha.setdefault(obj.id, {})[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 obj.type_name == "commit":
 
360
            self._commit = obj
 
361
            if type(bzr_key_data) is not dict:
 
362
                raise TypeError(bzr_key_data)
 
363
            self._testament3_sha1 = bzr_key_data.get("testament3-sha1")
 
364
        elif obj.type_name == "tree":
 
365
            if bzr_key_data is not None:
 
366
                self._trees.append((obj.id, bzr_key_data[0], self.revid))
 
367
        elif obj.type_name == "blob":
 
368
            if bzr_key_data is not None:
 
369
                self._blobs.append((obj.id, bzr_key_data[0], bzr_key_data[1]))
 
370
        else:
 
371
            raise AssertionError
 
372
 
 
373
    def finish(self):
 
374
        if self._commit is None:
 
375
            raise AssertionError("No commit object added")
 
376
        self.db.executemany(
 
377
            "replace into trees (sha1, fileid, revid) values (?, ?, ?)",
 
378
            self._trees)
 
379
        self.db.executemany(
 
380
            "replace into blobs (sha1, fileid, revid) values (?, ?, ?)",
 
381
            self._blobs)
 
382
        self.db.execute(
 
383
            "replace into commits (sha1, revid, tree_sha, testament3_sha1) values (?, ?, ?, ?)",
 
384
            (self._commit.id, self.revid, self._commit.tree, self._testament3_sha1))
 
385
        return self._commit
 
386
 
 
387
 
 
388
SqliteBzrGitCache = lambda p: BzrGitCache(SqliteGitShaMap(p), None, SqliteCacheUpdater)
 
389
 
 
390
 
 
391
class SqliteGitCacheFormat(BzrGitCacheFormat):
 
392
 
 
393
    def get_format_string(self):
 
394
        return 'bzr-git sha map version 1 using sqlite\n'
 
395
 
 
396
    def open(self, transport):
 
397
        try:
 
398
            basepath = transport.local_abspath(".")
 
399
        except bzr_errors.NotLocalUrl:
 
400
            basepath = get_cache_dir()
 
401
        return SqliteBzrGitCache(os.path.join(basepath, "idmap.db"))
 
402
 
 
403
 
 
404
class SqliteGitShaMap(GitShaMap):
 
405
    """Bazaar GIT Sha map that uses a sqlite database for storage."""
 
406
 
 
407
    def __init__(self, path=None):
 
408
        self.path = path
 
409
        if path is None:
 
410
            self.db = sqlite3.connect(":memory:")
 
411
        else:
 
412
            if not mapdbs().has_key(path):
 
413
                mapdbs()[path] = sqlite3.connect(path)
 
414
            self.db = mapdbs()[path]
 
415
        self.db.text_factory = str
 
416
        self.db.executescript("""
 
417
        create table if not exists commits(
 
418
            sha1 text not null check(length(sha1) == 40),
 
419
            revid text not null,
 
420
            tree_sha text not null check(length(tree_sha) == 40)
 
421
        );
 
422
        create index if not exists commit_sha1 on commits(sha1);
 
423
        create unique index if not exists commit_revid on commits(revid);
 
424
        create table if not exists blobs(
 
425
            sha1 text not null check(length(sha1) == 40),
 
426
            fileid text not null,
 
427
            revid text not null
 
428
        );
 
429
        create index if not exists blobs_sha1 on blobs(sha1);
 
430
        create unique index if not exists blobs_fileid_revid on blobs(fileid, revid);
 
431
        create table if not exists trees(
 
432
            sha1 text unique not null check(length(sha1) == 40),
 
433
            fileid text not null,
 
434
            revid text not null
 
435
        );
 
436
        create unique index if not exists trees_sha1 on trees(sha1);
 
437
        create unique index if not exists trees_fileid_revid on trees(fileid, revid);
 
438
""")
 
439
        try:
 
440
            self.db.executescript(
 
441
                "ALTER TABLE commits ADD testament3_sha1 TEXT;")
 
442
        except sqlite3.OperationalError:
 
443
            pass # Column already exists.
 
444
 
 
445
    def __repr__(self):
 
446
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
447
 
 
448
    def lookup_commit(self, revid):
 
449
        cursor = self.db.execute("select sha1 from commits where revid = ?", 
 
450
            (revid,))
 
451
        row = cursor.fetchone()
 
452
        if row is not None:
 
453
            return row[0]
 
454
        raise KeyError
 
455
 
 
456
    def commit_write_group(self):
 
457
        self.db.commit()
 
458
 
 
459
    def lookup_blob_id(self, fileid, revision):
 
460
        row = self.db.execute("select sha1 from blobs where fileid = ? and revid = ?", (fileid, revision)).fetchone()
 
461
        if row is not None:
 
462
            return row[0]
 
463
        raise KeyError(fileid)
 
464
 
 
465
    def lookup_tree_id(self, fileid, revision):
 
466
        row = self.db.execute("select sha1 from trees where fileid = ? and revid = ?", (fileid, revision)).fetchone()
 
467
        if row is not None:
 
468
            return row[0]
 
469
        raise KeyError(fileid)
 
470
 
 
471
    def lookup_git_sha(self, sha):
 
472
        """Lookup a Git sha in the database.
 
473
 
 
474
        :param sha: Git object sha
 
475
        :return: (type, type_data) with type_data:
 
476
            commit: revid, tree sha, verifiers
 
477
            tree: fileid, revid
 
478
            blob: fileid, revid
 
479
        """
 
480
        found = False
 
481
        cursor = self.db.execute("select revid, tree_sha, testament3_sha1 from commits where sha1 = ?", (sha,))
 
482
        for row in cursor.fetchall():
 
483
            found = True
 
484
            if row[2] is not None:
 
485
                verifiers = {"testament3-sha1": row[2]}
 
486
            else:
 
487
                verifiers = {}
 
488
            yield ("commit", (row[0], row[1], verifiers))
 
489
        cursor = self.db.execute("select fileid, revid from blobs where sha1 = ?", (sha,))
 
490
        for row in cursor.fetchall():
 
491
            found = True
 
492
            yield ("blob", row)
 
493
        cursor = self.db.execute("select fileid, revid from trees where sha1 = ?", (sha,))
 
494
        for row in cursor.fetchall():
 
495
            found = True
 
496
            yield ("tree", row)
 
497
        if not found:
 
498
            raise KeyError(sha)
 
499
 
 
500
    def revids(self):
 
501
        """List the revision ids known."""
 
502
        return (row for (row,) in self.db.execute("select revid from commits"))
 
503
 
 
504
    def sha1s(self):
 
505
        """List the SHA1s."""
 
506
        for table in ("blobs", "commits", "trees"):
 
507
            for (sha,) in self.db.execute("select sha1 from %s" % table):
 
508
                yield sha
 
509
 
 
510
 
 
511
class TdbCacheUpdater(CacheUpdater):
 
512
    """Cache updater for tdb-based caches."""
 
513
 
 
514
    def __init__(self, cache, rev):
 
515
        self.cache = cache
 
516
        self.db = cache.idmap.db
 
517
        self.revid = rev.revision_id
 
518
        self.parent_revids = rev.parent_ids
 
519
        self._commit = None
 
520
        self._entries = []
 
521
 
 
522
    def add_object(self, obj, bzr_key_data, path):
 
523
        sha = obj.sha().digest()
 
524
        if obj.type_name == "commit":
 
525
            self.db["commit\0" + self.revid] = "\0".join((sha, obj.tree))
 
526
            if type(bzr_key_data) is not dict:
 
527
                raise TypeError(bzr_key_data)
 
528
            type_data = (self.revid, obj.tree)
 
529
            try:
 
530
                type_data += (bzr_key_data["testament3-sha1"],)
 
531
            except KeyError:
 
532
                pass
 
533
            self._commit = obj
 
534
        elif obj.type_name == "blob":
 
535
            if bzr_key_data is None:
 
536
                return
 
537
            self.db["\0".join(("blob", bzr_key_data[0], bzr_key_data[1]))] = sha
 
538
            type_data = bzr_key_data
 
539
        elif obj.type_name == "tree":
 
540
            if bzr_key_data is None:
 
541
                return
 
542
            (file_id, ) = bzr_key_data
 
543
            type_data = (file_id, self.revid)
 
544
        else:
 
545
            raise AssertionError
 
546
        entry = "\0".join((obj.type_name, ) + type_data) + "\n"
 
547
        key = "git\0" + sha
 
548
        try:
 
549
            oldval = self.db[key]
 
550
        except KeyError:
 
551
            self.db[key] = entry
 
552
        else:
 
553
            if oldval[-1] != "\n":
 
554
                self.db[key] = "".join([oldval, "\n", entry])
 
555
            else:
 
556
                self.db[key] = "".join([oldval, entry])
 
557
 
 
558
    def finish(self):
 
559
        if self._commit is None:
 
560
            raise AssertionError("No commit object added")
 
561
        return self._commit
 
562
 
 
563
 
 
564
TdbBzrGitCache = lambda p: BzrGitCache(TdbGitShaMap(p), None, TdbCacheUpdater)
 
565
 
 
566
 
 
567
class TdbGitCacheFormat(BzrGitCacheFormat):
 
568
    """Cache format for tdb-based caches."""
 
569
 
 
570
    def get_format_string(self):
 
571
        return 'bzr-git sha map version 3 using tdb\n'
 
572
 
 
573
    def open(self, transport):
 
574
        try:
 
575
            basepath = transport.local_abspath(".").encode(osutils._fs_enc)
 
576
        except bzr_errors.NotLocalUrl:
 
577
            basepath = get_cache_dir()
 
578
        if type(basepath) is not str:
 
579
            raise TypeError(basepath)
 
580
        try:
 
581
            return TdbBzrGitCache(os.path.join(basepath, "idmap.tdb"))
 
582
        except ImportError:
 
583
            raise ImportError(
 
584
                "Unable to open existing bzr-git cache because 'tdb' is not "
 
585
                "installed.")
 
586
 
 
587
 
 
588
class TdbGitShaMap(GitShaMap):
 
589
    """SHA Map that uses a TDB database.
 
590
 
 
591
    Entries:
 
592
 
 
593
    "git <sha1>" -> "<type> <type-data1> <type-data2>"
 
594
    "commit revid" -> "<sha1> <tree-id>"
 
595
    "tree fileid revid" -> "<sha1>"
 
596
    "blob fileid revid" -> "<sha1>"
 
597
    """
 
598
 
 
599
    TDB_MAP_VERSION = 3
 
600
    TDB_HASH_SIZE = 50000
 
601
 
 
602
    def __init__(self, path=None):
 
603
        import tdb
 
604
        self.path = path
 
605
        if path is None:
 
606
            self.db = {}
 
607
        else:
 
608
            if type(path) is not str:
 
609
                raise TypeError(path)
 
610
            if not mapdbs().has_key(path):
 
611
                mapdbs()[path] = tdb.Tdb(path, self.TDB_HASH_SIZE, tdb.DEFAULT,
 
612
                                          os.O_RDWR|os.O_CREAT)
 
613
            self.db = mapdbs()[path]
 
614
        try:
 
615
            if int(self.db["version"]) not in (2, 3):
 
616
                trace.warning("SHA Map is incompatible (%s -> %d), rebuilding database.",
 
617
                              self.db["version"], self.TDB_MAP_VERSION)
 
618
                self.db.clear()
 
619
        except KeyError:
 
620
            pass
 
621
        self.db["version"] = str(self.TDB_MAP_VERSION)
 
622
 
 
623
    def start_write_group(self):
 
624
        """Start writing changes."""
 
625
        self.db.transaction_start()
 
626
 
 
627
    def commit_write_group(self):
 
628
        """Commit any pending changes."""
 
629
        self.db.transaction_commit()
 
630
 
 
631
    def abort_write_group(self):
 
632
        """Abort any pending changes."""
 
633
        self.db.transaction_cancel()
 
634
 
 
635
    def __repr__(self):
 
636
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
637
 
 
638
    def lookup_commit(self, revid):
 
639
        try:
 
640
            return sha_to_hex(self.db["commit\0" + revid][:20])
 
641
        except KeyError:
 
642
            raise KeyError("No cache entry for %r" % revid)
 
643
 
 
644
    def lookup_blob_id(self, fileid, revision):
 
645
        return sha_to_hex(self.db["\0".join(("blob", fileid, revision))])
 
646
 
 
647
    def lookup_git_sha(self, sha):
 
648
        """Lookup a Git sha in the database.
 
649
 
 
650
        :param sha: Git object sha
 
651
        :return: (type, type_data) with type_data:
 
652
            commit: revid, tree sha
 
653
            blob: fileid, revid
 
654
            tree: fileid, revid
 
655
        """
 
656
        if len(sha) == 40:
 
657
            sha = hex_to_sha(sha)
 
658
        value = self.db["git\0" + sha]
 
659
        for data in value.splitlines():
 
660
            data = data.split("\0")
 
661
            if data[0] == "commit":
 
662
                if len(data) == 3:
 
663
                    yield (data[0], (data[1], data[2], {}))
 
664
                else:
 
665
                    yield (data[0], (data[1], data[2], {"testament3-sha1": data[3]}))
 
666
            elif data[0] in ("tree", "blob"):
 
667
                yield (data[0], tuple(data[1:]))
 
668
            else:
 
669
                raise AssertionError("unknown type %r" % data[0])
 
670
 
 
671
    def missing_revisions(self, revids):
 
672
        ret = set()
 
673
        for revid in revids:
 
674
            if self.db.get("commit\0" + revid) is None:
 
675
                ret.add(revid)
 
676
        return ret
 
677
 
 
678
    def revids(self):
 
679
        """List the revision ids known."""
 
680
        for key in self.db.iterkeys():
 
681
            if key.startswith("commit\0"):
 
682
                yield key[7:]
 
683
 
 
684
    def sha1s(self):
 
685
        """List the SHA1s."""
 
686
        for key in self.db.iterkeys():
 
687
            if key.startswith("git\0"):
 
688
                yield sha_to_hex(key[4:])
 
689
 
 
690
 
 
691
class VersionedFilesContentCache(ContentCache):
 
692
 
 
693
    def __init__(self, vf):
 
694
        self._vf = vf
 
695
 
 
696
    def add(self, obj):
 
697
        self._vf.insert_record_stream(
 
698
            [versionedfile.ChunkedContentFactory((obj.id,), [], None,
 
699
                obj.as_legacy_object_chunks())])
 
700
 
 
701
    def __getitem__(self, sha):
 
702
        stream = self._vf.get_record_stream([(sha,)], 'unordered', True)
 
703
        entry = stream.next() 
 
704
        if entry.storage_kind == 'absent':
 
705
            raise KeyError(sha)
 
706
        return ShaFile._parse_legacy_object(entry.get_bytes_as('fulltext'))
 
707
 
 
708
 
 
709
class GitObjectStoreContentCache(ContentCache):
 
710
 
 
711
    def __init__(self, store):
 
712
        self.store = store
 
713
 
 
714
    def add_multi(self, objs):
 
715
        self.store.add_objects(objs)
 
716
 
 
717
    def add(self, obj, path):
 
718
        self.store.add_object(obj)
 
719
 
 
720
    def __getitem__(self, sha):
 
721
        return self.store[sha]
 
722
 
 
723
 
 
724
class IndexCacheUpdater(CacheUpdater):
 
725
 
 
726
    def __init__(self, cache, rev):
 
727
        self.cache = cache
 
728
        self.revid = rev.revision_id
 
729
        self.parent_revids = rev.parent_ids
 
730
        self._commit = None
 
731
        self._entries = []
 
732
        self._cache_objs = set()
 
733
 
 
734
    def add_object(self, obj, bzr_key_data, path):
 
735
        if obj.type_name == "commit":
 
736
            self._commit = obj
 
737
            if type(bzr_key_data) is not dict:
 
738
                raise TypeError(bzr_key_data)
 
739
            self.cache.idmap._add_git_sha(obj.id, "commit",
 
740
                (self.revid, obj.tree, bzr_key_data))
 
741
            self.cache.idmap._add_node(("commit", self.revid, "X"),
 
742
                " ".join((obj.id, obj.tree)))
 
743
            self._cache_objs.add((obj, path))
 
744
        elif obj.type_name == "blob":
 
745
            self.cache.idmap._add_git_sha(obj.id, "blob", bzr_key_data)
 
746
            self.cache.idmap._add_node(("blob", bzr_key_data[0],
 
747
                bzr_key_data[1]), obj.id)
 
748
        elif obj.type_name == "tree":
 
749
            self.cache.idmap._add_git_sha(obj.id, "tree",
 
750
                (bzr_key_data[0], self.revid))
 
751
            self._cache_objs.add((obj, path))
 
752
        else:
 
753
            raise AssertionError
 
754
 
 
755
    def finish(self):
 
756
        self.cache.content_cache.add_multi(self._cache_objs)
 
757
        return self._commit
 
758
 
 
759
 
 
760
class IndexBzrGitCache(BzrGitCache):
 
761
 
 
762
    def __init__(self, transport=None):
 
763
        mapper = versionedfile.ConstantMapper("trees")
 
764
        shamap = IndexGitShaMap(transport.clone('index'))
 
765
        #trees_store = knit.make_file_factory(True, mapper)(transport)
 
766
        #content_cache = VersionedFilesContentCache(trees_store)
 
767
        from .transportgit import TransportObjectStore
 
768
        store = TransportObjectStore(transport.clone('objects'))
 
769
        content_cache = GitObjectStoreContentCache(store)
 
770
        super(IndexBzrGitCache, self).__init__(shamap, content_cache,
 
771
                IndexCacheUpdater)
 
772
 
 
773
 
 
774
class IndexGitCacheFormat(BzrGitCacheFormat):
 
775
 
 
776
    def get_format_string(self):
 
777
        return 'bzr-git sha map with git object cache version 1\n'
 
778
 
 
779
    def initialize(self, transport):
 
780
        super(IndexGitCacheFormat, self).initialize(transport)
 
781
        transport.mkdir('index')
 
782
        transport.mkdir('objects')
 
783
        from .transportgit import TransportObjectStore
 
784
        TransportObjectStore.init(transport.clone('objects'))
 
785
 
 
786
    def open(self, transport):
 
787
        return IndexBzrGitCache(transport)
 
788
 
 
789
 
 
790
class IndexGitShaMap(GitShaMap):
 
791
    """SHA Map that uses the Bazaar APIs to store a cache.
 
792
 
 
793
    BTree Index file with the following contents:
 
794
 
 
795
    ("git", <sha1>) -> "<type> <type-data1> <type-data2>"
 
796
    ("commit", <revid>) -> "<sha1> <tree-id>"
 
797
    ("blob", <fileid>, <revid>) -> <sha1>
 
798
 
 
799
    """
 
800
 
 
801
    def __init__(self, transport=None):
 
802
        if transport is None:
 
803
            self._transport = None
 
804
            self._index = _mod_index.InMemoryGraphIndex(0, key_elements=3)
 
805
            self._builder = self._index
 
806
        else:
 
807
            self._builder = None
 
808
            self._transport = transport
 
809
            self._index = _mod_index.CombinedGraphIndex([])
 
810
            for name in self._transport.list_dir("."):
 
811
                if not name.endswith(".rix"):
 
812
                    continue
 
813
                x = _mod_btree_index.BTreeGraphIndex(self._transport, name,
 
814
                    self._transport.stat(name).st_size)
 
815
                self._index.insert_index(0, x)
 
816
 
 
817
    @classmethod
 
818
    def from_repository(cls, repository):
 
819
        transport = getattr(repository, "_transport", None)
 
820
        if transport is not None:
 
821
            try:
 
822
                transport.mkdir('git')
 
823
            except bzr_errors.FileExists:
 
824
                pass
 
825
            return cls(transport.clone('git'))
 
826
        from ...transport import get_transport
 
827
        return cls(get_transport(get_cache_dir()))
 
828
 
 
829
    def __repr__(self):
 
830
        if self._transport is not None:
 
831
            return "%s(%r)" % (self.__class__.__name__, self._transport.base)
 
832
        else:
 
833
            return "%s()" % (self.__class__.__name__)
 
834
 
 
835
    def repack(self):
 
836
        if self._builder is not None:
 
837
            raise errors.BzrError('builder already open')
 
838
        self.start_write_group()
 
839
        for _, key, value in self._index.iter_all_entries():
 
840
            self._builder.add_node(key, value)
 
841
        to_remove = []
 
842
        for name in self._transport.list_dir('.'):
 
843
            if name.endswith('.rix'):
 
844
                to_remove.append(name)
 
845
        self.commit_write_group()
 
846
        del self._index.indices[1:]
 
847
        for name in to_remove:
 
848
            self._transport.rename(name, name + '.old')
 
849
 
 
850
    def start_write_group(self):
 
851
        if self._builder is not None:
 
852
            raise errors.BzrError('builder already open')
 
853
        self._builder = _mod_btree_index.BTreeBuilder(0, key_elements=3)
 
854
        self._name = osutils.sha()
 
855
 
 
856
    def commit_write_group(self):
 
857
        if self._builder is None:
 
858
            raise errors.BzrError('builder not open')
 
859
        stream = self._builder.finish()
 
860
        name = self._name.hexdigest() + ".rix"
 
861
        size = self._transport.put_file(name, stream)
 
862
        index = _mod_btree_index.BTreeGraphIndex(self._transport, name, size)
 
863
        self._index.insert_index(0, index)
 
864
        self._builder = None
 
865
        self._name = None
 
866
 
 
867
    def abort_write_group(self):
 
868
        if self._builder is None:
 
869
            raise errors.BzrError('builder not open')
 
870
        self._builder = None
 
871
        self._name = None
 
872
 
 
873
    def _add_node(self, key, value):
 
874
        try:
 
875
            self._builder.add_node(key, value)
 
876
        except bzr_errors.BadIndexDuplicateKey:
 
877
            # Multiple bzr objects can have the same contents
 
878
            return True
 
879
        else:
 
880
            return False
 
881
 
 
882
    def _get_entry(self, key):
 
883
        entries = self._index.iter_entries([key])
 
884
        try:
 
885
            return entries.next()[2]
 
886
        except StopIteration:
 
887
            if self._builder is None:
 
888
                raise KeyError
 
889
            entries = self._builder.iter_entries([key])
 
890
            try:
 
891
                return entries.next()[2]
 
892
            except StopIteration:
 
893
                raise KeyError
 
894
 
 
895
    def _iter_entries_prefix(self, prefix):
 
896
        for entry in self._index.iter_entries_prefix([prefix]):
 
897
            yield (entry[1], entry[2])
 
898
        if self._builder is not None:
 
899
            for entry in self._builder.iter_entries_prefix([prefix]):
 
900
                yield (entry[1], entry[2])
 
901
 
 
902
    def lookup_commit(self, revid):
 
903
        return self._get_entry(("commit", revid, "X"))[:40]
 
904
 
 
905
    def _add_git_sha(self, hexsha, type, type_data):
 
906
        if hexsha is not None:
 
907
            self._name.update(hexsha)
 
908
            if type == "commit":
 
909
                td = (type_data[0], type_data[1])
 
910
                try:
 
911
                    td += (type_data[2]["testament3-sha1"],)
 
912
                except KeyError:
 
913
                    pass
 
914
            else:
 
915
                td = type_data
 
916
            self._add_node(("git", hexsha, "X"), " ".join((type,) + td))
 
917
        else:
 
918
            # This object is not represented in Git - perhaps an empty
 
919
            # directory?
 
920
            self._name.update(type + " ".join(type_data))
 
921
 
 
922
    def lookup_blob_id(self, fileid, revision):
 
923
        return self._get_entry(("blob", fileid, revision))
 
924
 
 
925
    def lookup_git_sha(self, sha):
 
926
        if len(sha) == 20:
 
927
            sha = sha_to_hex(sha)
 
928
        found = False
 
929
        for key, value in self._iter_entries_prefix(("git", sha, None)):
 
930
            found = True
 
931
            data = value.split(" ", 3)
 
932
            if data[0] == "commit":
 
933
                if data[3]:
 
934
                    verifiers = {"testament3-sha1": data[3]}
 
935
                else:
 
936
                    verifiers = {}
 
937
                yield ("commit", (data[1], data[2], verifiers))
 
938
            else:
 
939
                yield (data[0], tuple(data[1:]))
 
940
        if not found:
 
941
            raise KeyError(sha)
 
942
 
 
943
    def revids(self):
 
944
        """List the revision ids known."""
 
945
        for key, value in self._iter_entries_prefix(("commit", None, None)):
 
946
            yield key[1]
 
947
 
 
948
    def missing_revisions(self, revids):
 
949
        """Return set of all the revisions that are not present."""
 
950
        missing_revids = set(revids)
 
951
        for _, key, value in self._index.iter_entries((
 
952
            ("commit", revid, "X") for revid in revids)):
 
953
            missing_revids.remove(key[1])
 
954
        return missing_revids
 
955
 
 
956
    def sha1s(self):
 
957
        """List the SHA1s."""
 
958
        for key, value in self._iter_entries_prefix(("git", None, None)):
 
959
            yield key[1]
 
960
 
 
961
 
 
962
formats = registry.Registry()
 
963
formats.register(TdbGitCacheFormat().get_format_string(),
 
964
    TdbGitCacheFormat())
 
965
formats.register(SqliteGitCacheFormat().get_format_string(),
 
966
    SqliteGitCacheFormat())
 
967
formats.register(IndexGitCacheFormat().get_format_string(),
 
968
    IndexGitCacheFormat())
 
969
# In the future, this will become the default:
 
970
# formats.register('default', IndexGitCacheFormat())
 
971
try:
 
972
    import tdb
 
973
except ImportError:
 
974
    formats.register('default', SqliteGitCacheFormat())
 
975
else:
 
976
    formats.register('default', TdbGitCacheFormat())
 
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)