/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

In .testr.conf; run all git-relevant tests.

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
class TdbGitCacheFormat(BzrGitCacheFormat):
 
517
    """Cache format for tdb-based caches."""
 
518
 
 
519
    def get_format_string(self):
 
520
        return 'bzr-git sha map version 3 using tdb\n'
 
521
 
 
522
    def open(self, transport):
 
523
        try:
 
524
            basepath = transport.local_abspath(".").encode(osutils._fs_enc)
 
525
        except bzrlib.errors.NotLocalUrl:
 
526
            basepath = get_cache_dir()
 
527
        assert isinstance(basepath, str)
 
528
        try:
 
529
            return TdbBzrGitCache(os.path.join(basepath, "idmap.tdb"))
 
530
        except ImportError:
 
531
            raise ImportError(
 
532
                "Unable to open existing bzr-git cache because 'tdb' is not "
 
533
                "installed.")
 
534
 
 
535
 
 
536
class TdbGitShaMap(GitShaMap):
 
537
    """SHA Map that uses a TDB database.
 
538
 
 
539
    Entries:
 
540
 
 
541
    "git <sha1>" -> "<type> <type-data1> <type-data2>"
 
542
    "commit revid" -> "<sha1> <tree-id>"
 
543
    "tree fileid revid" -> "<sha1>"
 
544
    "blob fileid revid" -> "<sha1>"
 
545
    """
 
546
 
 
547
    TDB_MAP_VERSION = 3
 
548
    TDB_HASH_SIZE = 50000
 
549
 
 
550
    def __init__(self, path=None):
 
551
        import tdb
 
552
        self.path = path
 
553
        if path is None:
 
554
            self.db = {}
 
555
        else:
 
556
            assert isinstance(path, str)
 
557
            if not mapdbs().has_key(path):
 
558
                mapdbs()[path] = tdb.Tdb(path, self.TDB_HASH_SIZE, tdb.DEFAULT,
 
559
                                          os.O_RDWR|os.O_CREAT)
 
560
            self.db = mapdbs()[path]
 
561
        try:
 
562
            if int(self.db["version"]) not in (2, 3):
 
563
                trace.warning("SHA Map is incompatible (%s -> %d), rebuilding database.",
 
564
                              self.db["version"], self.TDB_MAP_VERSION)
 
565
                self.db.clear()
 
566
        except KeyError:
 
567
            pass
 
568
        self.db["version"] = str(self.TDB_MAP_VERSION)
 
569
 
 
570
    def start_write_group(self):
 
571
        """Start writing changes."""
 
572
        self.db.transaction_start()
 
573
 
 
574
    def commit_write_group(self):
 
575
        """Commit any pending changes."""
 
576
        self.db.transaction_commit()
 
577
 
 
578
    def abort_write_group(self):
 
579
        """Abort any pending changes."""
 
580
        self.db.transaction_cancel()
 
581
 
 
582
    def __repr__(self):
 
583
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
584
 
 
585
    def lookup_commit(self, revid):
 
586
        return sha_to_hex(self.db["commit\0" + revid][:20])
 
587
 
 
588
    def lookup_blob_id(self, fileid, revision):
 
589
        return sha_to_hex(self.db["\0".join(("blob", fileid, revision))])
 
590
 
 
591
    def lookup_git_sha(self, sha):
 
592
        """Lookup a Git sha in the database.
 
593
 
 
594
        :param sha: Git object sha
 
595
        :return: (type, type_data) with type_data:
 
596
            commit: revid, tree sha
 
597
            blob: fileid, revid
 
598
            tree: fileid, revid
 
599
        """
 
600
        if len(sha) == 40:
 
601
            sha = hex_to_sha(sha)
 
602
        data = self.db["git\0" + sha].split("\0")
 
603
        if data[0] == "commit":
 
604
            if len(data) == 3:
 
605
                return (data[0], (data[1], data[2], {}))
 
606
            else:
 
607
                return (data[0], (data[1], data[2], {"testament3-sha1": data[3]}))
 
608
        else:
 
609
            return (data[0], tuple(data[1:]))
 
610
 
 
611
    def missing_revisions(self, revids):
 
612
        ret = set()
 
613
        for revid in revids:
 
614
            if self.db.get("commit\0" + revid) is None:
 
615
                ret.add(revid)
 
616
        return ret
 
617
 
 
618
    def revids(self):
 
619
        """List the revision ids known."""
 
620
        for key in self.db.iterkeys():
 
621
            if key.startswith("commit\0"):
 
622
                yield key[7:]
 
623
 
 
624
    def sha1s(self):
 
625
        """List the SHA1s."""
 
626
        for key in self.db.iterkeys():
 
627
            if key.startswith("git\0"):
 
628
                yield sha_to_hex(key[4:])
 
629
 
 
630
 
 
631
class VersionedFilesContentCache(ContentCache):
 
632
 
 
633
    def __init__(self, vf):
 
634
        self._vf = vf
 
635
 
 
636
    def add(self, obj):
 
637
        self._vf.insert_record_stream(
 
638
            [versionedfile.ChunkedContentFactory((obj.id,), [], None,
 
639
                obj.as_legacy_object_chunks())])
 
640
 
 
641
    def __getitem__(self, sha):
 
642
        stream = self._vf.get_record_stream([(sha,)], 'unordered', True)
 
643
        entry = stream.next() 
 
644
        if entry.storage_kind == 'absent':
 
645
            raise KeyError(sha)
 
646
        return ShaFile._parse_legacy_object(entry.get_bytes_as('fulltext'))
 
647
 
 
648
 
 
649
class GitObjectStoreContentCache(ContentCache):
 
650
 
 
651
    def __init__(self, store):
 
652
        self.store = store
 
653
 
 
654
    def add_multi(self, objs):
 
655
        self.store.add_objects(objs)
 
656
 
 
657
    def add(self, obj, path):
 
658
        self.store.add_object(obj)
 
659
 
 
660
    def __getitem__(self, sha):
 
661
        return self.store[sha]
 
662
 
 
663
 
 
664
class IndexCacheUpdater(CacheUpdater):
 
665
 
 
666
    def __init__(self, cache, rev):
 
667
        self.cache = cache
 
668
        self.revid = rev.revision_id
 
669
        self.parent_revids = rev.parent_ids
 
670
        self._commit = None
 
671
        self._entries = []
 
672
        self._cache_objs = set()
 
673
 
 
674
    def add_object(self, obj, ie, path):
 
675
        if obj.type_name == "commit":
 
676
            self._commit = obj
 
677
            assert type(ie) is dict
 
678
            self.cache.idmap._add_git_sha(obj.id, "commit",
 
679
                (self.revid, obj.tree, ie))
 
680
            self.cache.idmap._add_node(("commit", self.revid, "X"),
 
681
                " ".join((obj.id, obj.tree)))
 
682
            self._cache_objs.add((obj, path))
 
683
        elif obj.type_name == "blob":
 
684
            self.cache.idmap._add_git_sha(obj.id, "blob",
 
685
                (ie.file_id, ie.revision))
 
686
            self.cache.idmap._add_node(("blob", ie.file_id, ie.revision), obj.id)
 
687
            if ie.kind == "symlink":
 
688
                self._cache_objs.add((obj, path))
 
689
        elif obj.type_name == "tree":
 
690
            self.cache.idmap._add_git_sha(obj.id, "tree",
 
691
                (ie.file_id, self.revid))
 
692
            self._cache_objs.add((obj, path))
 
693
        else:
 
694
            raise AssertionError
 
695
 
 
696
    def finish(self):
 
697
        self.cache.content_cache.add_multi(self._cache_objs)
 
698
        return self._commit
 
699
 
 
700
 
 
701
class IndexBzrGitCache(BzrGitCache):
 
702
 
 
703
    def __init__(self, transport=None):
 
704
        mapper = versionedfile.ConstantMapper("trees")
 
705
        shamap = IndexGitShaMap(transport.clone('index'))
 
706
        #trees_store = knit.make_file_factory(True, mapper)(transport)
 
707
        #content_cache = VersionedFilesContentCache(trees_store)
 
708
        from bzrlib.plugins.git.transportgit import TransportObjectStore
 
709
        store = TransportObjectStore(transport.clone('objects'))
 
710
        content_cache = GitObjectStoreContentCache(store)
 
711
        super(IndexBzrGitCache, self).__init__(shamap, content_cache,
 
712
                IndexCacheUpdater)
 
713
 
 
714
 
 
715
class IndexGitCacheFormat(BzrGitCacheFormat):
 
716
 
 
717
    def get_format_string(self):
 
718
        return 'bzr-git sha map with git object cache version 1\n'
 
719
 
 
720
    def initialize(self, transport):
 
721
        super(IndexGitCacheFormat, self).initialize(transport)
 
722
        transport.mkdir('index')
 
723
        transport.mkdir('objects')
 
724
        from bzrlib.plugins.git.transportgit import TransportObjectStore
 
725
        TransportObjectStore.init(transport.clone('objects'))
 
726
 
 
727
    def open(self, transport):
 
728
        return IndexBzrGitCache(transport)
 
729
 
 
730
 
 
731
class IndexGitShaMap(GitShaMap):
 
732
    """SHA Map that uses the Bazaar APIs to store a cache.
 
733
 
 
734
    BTree Index file with the following contents:
 
735
 
 
736
    ("git", <sha1>) -> "<type> <type-data1> <type-data2>"
 
737
    ("commit", <revid>) -> "<sha1> <tree-id>"
 
738
    ("blob", <fileid>, <revid>) -> <sha1>
 
739
 
 
740
    """
 
741
 
 
742
    def __init__(self, transport=None):
 
743
        if transport is None:
 
744
            self._transport = None
 
745
            self._index = _mod_index.InMemoryGraphIndex(0, key_elements=3)
 
746
            self._builder = self._index
 
747
        else:
 
748
            self._builder = None
 
749
            self._transport = transport
 
750
            self._index = _mod_index.CombinedGraphIndex([])
 
751
            for name in self._transport.list_dir("."):
 
752
                if not name.endswith(".rix"):
 
753
                    continue
 
754
                x = _mod_btree_index.BTreeGraphIndex(self._transport, name,
 
755
                    self._transport.stat(name).st_size)
 
756
                self._index.insert_index(0, x)
 
757
 
 
758
    @classmethod
 
759
    def from_repository(cls, repository):
 
760
        transport = getattr(repository, "_transport", None)
 
761
        if transport is not None:
 
762
            try:
 
763
                transport.mkdir('git')
 
764
            except bzrlib.errors.FileExists:
 
765
                pass
 
766
            return cls(transport.clone('git'))
 
767
        from bzrlib.transport import get_transport
 
768
        return cls(get_transport(get_cache_dir()))
 
769
 
 
770
    def __repr__(self):
 
771
        if self._transport is not None:
 
772
            return "%s(%r)" % (self.__class__.__name__, self._transport.base)
 
773
        else:
 
774
            return "%s()" % (self.__class__.__name__)
 
775
 
 
776
    def repack(self):
 
777
        assert self._builder is None
 
778
        self.start_write_group()
 
779
        for _, key, value in self._index.iter_all_entries():
 
780
            self._builder.add_node(key, value)
 
781
        to_remove = []
 
782
        for name in self._transport.list_dir('.'):
 
783
            if name.endswith('.rix'):
 
784
                to_remove.append(name)
 
785
        self.commit_write_group()
 
786
        del self._index.indices[1:]
 
787
        for name in to_remove:
 
788
            self._transport.rename(name, name + '.old')
 
789
 
 
790
    def start_write_group(self):
 
791
        assert self._builder is None
 
792
        self._builder = _mod_btree_index.BTreeBuilder(0, key_elements=3)
 
793
        self._name = osutils.sha()
 
794
 
 
795
    def commit_write_group(self):
 
796
        assert self._builder is not None
 
797
        stream = self._builder.finish()
 
798
        name = self._name.hexdigest() + ".rix"
 
799
        size = self._transport.put_file(name, stream)
 
800
        index = _mod_btree_index.BTreeGraphIndex(self._transport, name, size)
 
801
        self._index.insert_index(0, index)
 
802
        self._builder = None
 
803
        self._name = None
 
804
 
 
805
    def abort_write_group(self):
 
806
        assert self._builder is not None
 
807
        self._builder = None
 
808
        self._name = None
 
809
 
 
810
    def _add_node(self, key, value):
 
811
        try:
 
812
            self._builder.add_node(key, value)
 
813
        except bzrlib.errors.BadIndexDuplicateKey:
 
814
            # Multiple bzr objects can have the same contents
 
815
            return True
 
816
        else:
 
817
            return False
 
818
 
 
819
    def _get_entry(self, key):
 
820
        entries = self._index.iter_entries([key])
 
821
        try:
 
822
            return entries.next()[2]
 
823
        except StopIteration:
 
824
            if self._builder is None:
 
825
                raise KeyError
 
826
            entries = self._builder.iter_entries([key])
 
827
            try:
 
828
                return entries.next()[2]
 
829
            except StopIteration:
 
830
                raise KeyError
 
831
 
 
832
    def _iter_keys_prefix(self, prefix):
 
833
        for entry in self._index.iter_entries_prefix([prefix]):
 
834
            yield entry[1]
 
835
        if self._builder is not None:
 
836
            for entry in self._builder.iter_entries_prefix([prefix]):
 
837
                yield entry[1]
 
838
 
 
839
    def lookup_commit(self, revid):
 
840
        return self._get_entry(("commit", revid, "X"))[:40]
 
841
 
 
842
    def _add_git_sha(self, hexsha, type, type_data):
 
843
        if hexsha is not None:
 
844
            self._name.update(hexsha)
 
845
            if type == "commit":
 
846
                td = (type_data[0], type_data[1], type_data[2]["testament3-sha1"])
 
847
            else:
 
848
                td = type_data
 
849
            self._add_node(("git", hexsha, "X"), " ".join((type,) + td))
 
850
        else:
 
851
            # This object is not represented in Git - perhaps an empty
 
852
            # directory?
 
853
            self._name.update(type + " ".join(type_data))
 
854
 
 
855
    def lookup_blob_id(self, fileid, revision):
 
856
        return self._get_entry(("blob", fileid, revision))
 
857
 
 
858
    def lookup_git_sha(self, sha):
 
859
        if len(sha) == 20:
 
860
            sha = sha_to_hex(sha)
 
861
        data = self._get_entry(("git", sha, "X")).split(" ", 3)
 
862
        if data[0] == "commit":
 
863
            return ("commit", (data[1], data[2], {"testament3-sha1": data[3]}))
 
864
        else:
 
865
            return (data[0], tuple(data[1:]))
 
866
 
 
867
    def revids(self):
 
868
        """List the revision ids known."""
 
869
        for key in self._iter_keys_prefix(("commit", None, None)):
 
870
            yield key[1]
 
871
 
 
872
    def missing_revisions(self, revids):
 
873
        """Return set of all the revisions that are not present."""
 
874
        missing_revids = set(revids)
 
875
        for _, key, value in self._index.iter_entries((
 
876
            ("commit", revid, "X") for revid in revids)):
 
877
            missing_revids.remove(key[1])
 
878
        return missing_revids
 
879
 
 
880
    def sha1s(self):
 
881
        """List the SHA1s."""
 
882
        for key in self._iter_keys_prefix(("git", None, None)):
 
883
            yield key[1]
 
884
 
 
885
 
 
886
formats = registry.Registry()
 
887
formats.register(TdbGitCacheFormat().get_format_string(),
 
888
    TdbGitCacheFormat())
 
889
formats.register(SqliteGitCacheFormat().get_format_string(),
 
890
    SqliteGitCacheFormat())
 
891
formats.register(IndexGitCacheFormat().get_format_string(),
 
892
    IndexGitCacheFormat())
 
893
# In the future, this will become the default:
 
894
# formats.register('default', IndexGitCacheFormat())
 
895
try:
 
896
    import tdb
 
897
except ImportError:
 
898
    formats.register('default', SqliteGitCacheFormat())
 
899
else:
 
900
    formats.register('default', TdbGitCacheFormat())
 
901
 
 
902
 
 
903
 
 
904
def migrate_ancient_formats(repo_transport):
 
905
    # Prefer migrating git.db over git.tdb, since the latter may not 
 
906
    # be openable on some platforms.
 
907
    if repo_transport.has("git.db"):
 
908
        SqliteGitCacheFormat().initialize(repo_transport.clone("git"))
 
909
        repo_transport.rename("git.db", "git/idmap.db")
 
910
    elif repo_transport.has("git.tdb"):
 
911
        TdbGitCacheFormat().initialize(repo_transport.clone("git"))
 
912
        repo_transport.rename("git.tdb", "git/idmap.tdb")
 
913
 
 
914
 
 
915
def remove_readonly_transport_decorator(transport):
 
916
    if transport.is_readonly():
 
917
        return transport._decorated
 
918
    return transport
 
919
 
 
920
 
 
921
def from_repository(repository):
 
922
    """Open a cache file for a repository.
 
923
 
 
924
    If the repository is remote and there is no transport available from it
 
925
    this will use a local file in the users cache directory
 
926
    (typically ~/.cache/bazaar/git/)
 
927
 
 
928
    :param repository: A repository object
 
929
    """
 
930
    repo_transport = getattr(repository, "_transport", None)
 
931
    if repo_transport is not None:
 
932
        # Migrate older cache formats
 
933
        repo_transport = remove_readonly_transport_decorator(repo_transport)
 
934
        try:
 
935
            repo_transport.mkdir("git")
 
936
        except bzrlib.errors.FileExists:
 
937
            pass
 
938
        else:
 
939
            migrate_ancient_formats(repo_transport)
 
940
    return BzrGitCacheFormat.from_repository(repository)