/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

Tags: bzr-git-0.6.5
ReleaseĀ 0.6.6.

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