/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

Raise SettingFileIdUnsupported

Show diffs side-by-side

added added

removed removed

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