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