/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

Require dulwich 0.8.1.

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