1
 
# Copyright (C) 2009 Jelmer Vernooij <jelmer@samba.org>
 
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.
 
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.
 
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
 
17
 
"""Map from Git sha's to Bazaar objects."""
 
19
 
from dulwich.objects import (
 
26
 
from dulwich.objects import (
 
32
 
    btree_index as _mod_btree_index,
 
39
 
from bzrlib.transport import (
 
46
 
        from xdg.BaseDirectory import xdg_cache_home
 
48
 
        from bzrlib.config import config_dir
 
49
 
        ret = os.path.join(config_dir(), "git")
 
51
 
        ret = os.path.join(xdg_cache_home, "bazaar", "git")
 
52
 
    if not os.path.isdir(ret):
 
57
 
def get_remote_cache_transport():
 
58
 
    """Retrieve the transport to use when accessing (unwritable) remote 
 
61
 
    return get_transport(get_cache_dir())
 
64
 
def check_pysqlite_version(sqlite3):
 
65
 
    """Check that sqlite library is compatible.
 
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")
 
77
 
        check_pysqlite_version(sqlite3)
 
78
 
    except (ImportError, bzrlib.errors.BzrError), e:
 
79
 
        from pysqlite2 import dbapi2 as sqlite3
 
80
 
        check_pysqlite_version(sqlite3)
 
82
 
    trace.warning('Needs at least Python2.5 or Python2.4 with the pysqlite2 '
 
84
 
    raise bzrlib.errors.BzrError("missing sqlite library")
 
87
 
_mapdbs = threading.local()
 
89
 
    """Get a cache for this thread's db connections."""
 
92
 
    except AttributeError:
 
97
 
class GitShaMap(object):
 
98
 
    """Git<->Bzr revision id mapping database."""
 
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
 
108
 
        raise NotImplementedError(self.lookup_git_sha)
 
110
 
    def lookup_blob_id(self, file_id, revision):
 
111
 
        """Retrieve a Git blob SHA by file id.
 
113
 
        :param file_id: File id of the file/symlink
 
114
 
        :param revision: revision in which the file was last changed.
 
116
 
        raise NotImplementedError(self.lookup_blob_id)
 
118
 
    def lookup_tree_id(self, file_id, revision):
 
119
 
        """Retrieve a Git tree SHA by file id.
 
121
 
        raise NotImplementedError(self.lookup_tree_id)
 
124
 
        """List the revision ids known."""
 
125
 
        raise NotImplementedError(self.revids)
 
127
 
    def missing_revisions(self, revids):
 
128
 
        """Return set of all the revisions that are not present."""
 
129
 
        present_revids = set(self.revids())
 
130
 
        if not isinstance(revids, set):
 
132
 
        return revids - present_revids
 
135
 
        """List the SHA1s."""
 
136
 
        raise NotImplementedError(self.sha1s)
 
138
 
    def start_write_group(self):
 
139
 
        """Start writing changes."""
 
141
 
    def commit_write_group(self):
 
142
 
        """Commit any pending changes."""
 
144
 
    def abort_write_group(self):
 
145
 
        """Abort any pending changes."""
 
148
 
class ContentCache(object):
 
149
 
    """Object that can cache Git objects."""
 
151
 
    def add(self, object):
 
153
 
        raise NotImplementedError(self.add)
 
155
 
    def add_multi(self, objects):
 
156
 
        """Add multiple objects."""
 
160
 
    def __getitem__(self, sha):
 
161
 
        """Retrieve an item, by SHA."""
 
162
 
        raise NotImplementedError(self.__getitem__)
 
165
 
class BzrGitCacheFormat(object):
 
166
 
    """Bazaar-Git Cache Format."""
 
168
 
    def get_format_string(self):
 
169
 
        """Return a single-line unique format string for this cache format."""
 
170
 
        raise NotImplementedError(self.get_format_string)
 
172
 
    def open(self, transport):
 
173
 
        """Open this format on a transport."""
 
174
 
        raise NotImplementedError(self.open)
 
176
 
    def initialize(self, transport):
 
177
 
        """Create a new instance of this cache format at transport."""
 
178
 
        transport.put_bytes('format', self.get_format_string())
 
181
 
    def from_transport(self, transport):
 
182
 
        """Open a cache file present on a transport, or initialize one.
 
184
 
        :param transport: Transport to use
 
185
 
        :return: A BzrGitCache instance
 
188
 
            format_name = transport.get_bytes('format')
 
189
 
            format = formats.get(format_name)
 
190
 
        except bzrlib.errors.NoSuchFile:
 
191
 
            format = formats.get('default')
 
192
 
            format.initialize(transport)
 
193
 
        return format.open(transport)
 
196
 
    def from_repository(cls, repository):
 
197
 
        """Open a cache file for a repository.
 
199
 
        This will use the repository's transport to store the cache file, or
 
200
 
        use the users global cache directory if the repository has no 
 
201
 
        transport associated with it.
 
203
 
        :param repository: Repository to open the cache for
 
204
 
        :return: A `BzrGitCache`
 
206
 
        repo_transport = getattr(repository, "_transport", None)
 
207
 
        if repo_transport is not None:
 
208
 
            # Even if we don't write to this repo, we should be able 
 
209
 
            # to update its cache.
 
210
 
            repo_transport = remove_readonly_transport_decorator(repo_transport)
 
212
 
                repo_transport.mkdir('git')
 
213
 
            except bzrlib.errors.FileExists:
 
215
 
            transport = repo_transport.clone('git')
 
217
 
            transport = get_remote_cache_transport()
 
218
 
        return cls.from_transport(transport)
 
221
 
class CacheUpdater(object):
 
222
 
    """Base class for objects that can update a bzr-git cache."""
 
224
 
    def add_object(self, obj, ie, path):
 
227
 
        :param obj: Object type ("commit", "blob" or "tree")
 
228
 
        :param ie: Inventory entry (for blob/tree) or testament_sha in case
 
230
 
        :param path: Path of the object (optional)
 
232
 
        raise NotImplementedError(self.add_object)
 
235
 
        raise NotImplementedError(self.finish)
 
238
 
class BzrGitCache(object):
 
239
 
    """Caching backend."""
 
241
 
    def __init__(self, idmap, content_cache, cache_updater_klass):
 
243
 
        self.content_cache = content_cache
 
244
 
        self._cache_updater_klass = cache_updater_klass
 
246
 
    def get_updater(self, rev):
 
247
 
        """Update an object that implements the CacheUpdater interface for 
 
250
 
        return self._cache_updater_klass(self, rev)
 
253
 
DictBzrGitCache = lambda: BzrGitCache(DictGitShaMap(), None, DictCacheUpdater)
 
256
 
class DictCacheUpdater(CacheUpdater):
 
257
 
    """Cache updater for dict-based caches."""
 
259
 
    def __init__(self, cache, rev):
 
261
 
        self.revid = rev.revision_id
 
262
 
        self.parent_revids = rev.parent_ids
 
266
 
    def add_object(self, obj, ie, path):
 
267
 
        if obj.type_name == "commit":
 
269
 
            assert type(ie) is dict
 
270
 
            type_data = (self.revid, self._commit.tree, ie)
 
271
 
            self.cache.idmap._by_revid[self.revid] = obj.id
 
272
 
        elif obj.type_name in ("blob", "tree"):
 
274
 
                if obj.type_name == "blob":
 
275
 
                    revision = ie.revision
 
277
 
                    revision = self.revid
 
278
 
                type_data = (ie.file_id, revision)
 
279
 
                self.cache.idmap._by_fileid.setdefault(type_data[1], {})[type_data[0]] = obj.id
 
282
 
        self.cache.idmap._by_sha[obj.id] = (obj.type_name, type_data)
 
285
 
        if self._commit is None:
 
286
 
            raise AssertionError("No commit object added")
 
290
 
class DictGitShaMap(GitShaMap):
 
291
 
    """Git SHA map that uses a dictionary."""
 
298
 
    def lookup_blob_id(self, fileid, revision):
 
299
 
        return self._by_fileid[revision][fileid]
 
301
 
    def lookup_git_sha(self, sha):
 
302
 
        return self._by_sha[sha]
 
304
 
    def lookup_tree_id(self, fileid, revision):
 
305
 
        return self._by_fileid[revision][fileid]
 
307
 
    def lookup_commit(self, revid):
 
308
 
        return self._by_revid[revid]
 
311
 
        for key, (type, type_data) in self._by_sha.iteritems():
 
316
 
        return self._by_sha.iterkeys()
 
319
 
class SqliteCacheUpdater(CacheUpdater):
 
321
 
    def __init__(self, cache, rev):
 
323
 
        self.db = self.cache.idmap.db
 
324
 
        self.revid = rev.revision_id
 
329
 
    def add_object(self, obj, ie, path):
 
330
 
        if obj.type_name == "commit":
 
332
 
            self._testament3_sha1 = ie["testament3-sha1"]
 
333
 
            assert type(ie) is dict
 
334
 
        elif obj.type_name == "tree":
 
336
 
                self._trees.append((obj.id, ie.file_id, self.revid))
 
337
 
        elif obj.type_name == "blob":
 
339
 
                self._blobs.append((obj.id, ie.file_id, ie.revision))
 
344
 
        if self._commit is None:
 
345
 
            raise AssertionError("No commit object added")
 
347
 
            "replace into trees (sha1, fileid, revid) values (?, ?, ?)",
 
350
 
            "replace into blobs (sha1, fileid, revid) values (?, ?, ?)",
 
353
 
            "replace into commits (sha1, revid, tree_sha, testament3_sha1) values (?, ?, ?, ?)",
 
354
 
            (self._commit.id, self.revid, self._commit.tree, self._testament3_sha1))
 
358
 
SqliteBzrGitCache = lambda p: BzrGitCache(SqliteGitShaMap(p), None, SqliteCacheUpdater)
 
361
 
class SqliteGitCacheFormat(BzrGitCacheFormat):
 
363
 
    def get_format_string(self):
 
364
 
        return 'bzr-git sha map version 1 using sqlite\n'
 
366
 
    def open(self, transport):
 
368
 
            basepath = transport.local_abspath(".")
 
369
 
        except bzrlib.errors.NotLocalUrl:
 
370
 
            basepath = get_cache_dir()
 
371
 
        return SqliteBzrGitCache(os.path.join(basepath, "idmap.db"))
 
374
 
class SqliteGitShaMap(GitShaMap):
 
375
 
    """Bazaar GIT Sha map that uses a sqlite database for storage."""
 
377
 
    def __init__(self, path=None):
 
380
 
            self.db = sqlite3.connect(":memory:")
 
382
 
            if not mapdbs().has_key(path):
 
383
 
                mapdbs()[path] = sqlite3.connect(path)
 
384
 
            self.db = mapdbs()[path]
 
385
 
        self.db.text_factory = str
 
386
 
        self.db.executescript("""
 
387
 
        create table if not exists commits(
 
388
 
            sha1 text not null check(length(sha1) == 40),
 
390
 
            tree_sha text not null check(length(tree_sha) == 40)
 
392
 
        create index if not exists commit_sha1 on commits(sha1);
 
393
 
        create unique index if not exists commit_revid on commits(revid);
 
394
 
        create table if not exists blobs(
 
395
 
            sha1 text not null check(length(sha1) == 40),
 
396
 
            fileid text not null,
 
399
 
        create index if not exists blobs_sha1 on blobs(sha1);
 
400
 
        create unique index if not exists blobs_fileid_revid on blobs(fileid, revid);
 
401
 
        create table if not exists trees(
 
402
 
            sha1 text unique not null check(length(sha1) == 40),
 
403
 
            fileid text not null,
 
406
 
        create unique index if not exists trees_sha1 on trees(sha1);
 
407
 
        create unique index if not exists trees_fileid_revid on trees(fileid, revid);
 
410
 
            self.db.executescript(
 
411
 
                "ALTER TABLE commits ADD testament3_sha1 TEXT;")
 
412
 
        except sqlite3.OperationalError:
 
413
 
            pass # Column already exists.
 
416
 
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
418
 
    def lookup_commit(self, revid):
 
419
 
        cursor = self.db.execute("select sha1 from commits where revid = ?", 
 
421
 
        row = cursor.fetchone()
 
426
 
    def commit_write_group(self):
 
429
 
    def lookup_blob_id(self, fileid, revision):
 
430
 
        row = self.db.execute("select sha1 from blobs where fileid = ? and revid = ?", (fileid, revision)).fetchone()
 
433
 
        raise KeyError(fileid)
 
435
 
    def lookup_tree_id(self, fileid, revision):
 
436
 
        row = self.db.execute("select sha1 from trees where fileid = ? and revid = ?", (fileid, revision)).fetchone()
 
439
 
        raise KeyError(fileid)
 
441
 
    def lookup_git_sha(self, sha):
 
442
 
        """Lookup a Git sha in the database.
 
444
 
        :param sha: Git object sha
 
445
 
        :return: (type, type_data) with type_data:
 
446
 
            commit: revid, tree sha, verifiers
 
450
 
        row = self.db.execute("select revid, tree_sha, testament3_sha1 from commits where sha1 = ?", (sha,)).fetchone()
 
452
 
            return ("commit", (row[0], row[1], {"testament3-sha1": row[2]}))
 
453
 
        row = self.db.execute("select fileid, revid from blobs where sha1 = ?", (sha,)).fetchone()
 
456
 
        row = self.db.execute("select fileid, revid from trees where sha1 = ?", (sha,)).fetchone()
 
462
 
        """List the revision ids known."""
 
463
 
        return (row for (row,) in self.db.execute("select revid from commits"))
 
466
 
        """List the SHA1s."""
 
467
 
        for table in ("blobs", "commits", "trees"):
 
468
 
            for (sha,) in self.db.execute("select sha1 from %s" % table):
 
472
 
class TdbCacheUpdater(CacheUpdater):
 
473
 
    """Cache updater for tdb-based caches."""
 
475
 
    def __init__(self, cache, rev):
 
477
 
        self.db = cache.idmap.db
 
478
 
        self.revid = rev.revision_id
 
479
 
        self.parent_revids = rev.parent_ids
 
483
 
    def add_object(self, obj, ie, path):
 
484
 
        sha = obj.sha().digest()
 
485
 
        if obj.type_name == "commit":
 
486
 
            self.db["commit\0" + self.revid] = "\0".join((sha, obj.tree))
 
487
 
            assert type(ie) is dict, "was %r" % ie
 
488
 
            type_data = (self.revid, obj.tree, ie["testament3-sha1"])
 
490
 
        elif obj.type_name == "blob":
 
493
 
            self.db["\0".join(("blob", ie.file_id, ie.revision))] = sha
 
494
 
            type_data = (ie.file_id, ie.revision)
 
495
 
        elif obj.type_name == "tree":
 
498
 
            type_data = (ie.file_id, self.revid)
 
501
 
        self.db["git\0" + sha] = "\0".join((obj.type_name, ) + type_data)
 
504
 
        if self._commit is None:
 
505
 
            raise AssertionError("No commit object added")
 
509
 
TdbBzrGitCache = lambda p: BzrGitCache(TdbGitShaMap(p), None, TdbCacheUpdater)
 
511
 
class TdbGitCacheFormat(BzrGitCacheFormat):
 
512
 
    """Cache format for tdb-based caches."""
 
514
 
    def get_format_string(self):
 
515
 
        return 'bzr-git sha map version 3 using tdb\n'
 
517
 
    def open(self, transport):
 
519
 
            basepath = transport.local_abspath(".")
 
520
 
        except bzrlib.errors.NotLocalUrl:
 
521
 
            basepath = get_cache_dir()
 
523
 
            return TdbBzrGitCache(os.path.join(basepath, "idmap.tdb"))
 
526
 
                "Unable to open existing bzr-git cache because 'tdb' is not "
 
530
 
class TdbGitShaMap(GitShaMap):
 
531
 
    """SHA Map that uses a TDB database.
 
535
 
    "git <sha1>" -> "<type> <type-data1> <type-data2>"
 
536
 
    "commit revid" -> "<sha1> <tree-id>"
 
537
 
    "tree fileid revid" -> "<sha1>"
 
538
 
    "blob fileid revid" -> "<sha1>"
 
542
 
    TDB_HASH_SIZE = 50000
 
544
 
    def __init__(self, path=None):
 
550
 
            if not mapdbs().has_key(path):
 
551
 
                mapdbs()[path] = tdb.Tdb(path, self.TDB_HASH_SIZE, tdb.DEFAULT,
 
552
 
                                          os.O_RDWR|os.O_CREAT)
 
553
 
            self.db = mapdbs()[path]
 
555
 
            if int(self.db["version"]) not in (2, 3):
 
556
 
                trace.warning("SHA Map is incompatible (%s -> %d), rebuilding database.",
 
557
 
                              self.db["version"], self.TDB_MAP_VERSION)
 
561
 
        self.db["version"] = str(self.TDB_MAP_VERSION)
 
563
 
    def start_write_group(self):
 
564
 
        """Start writing changes."""
 
565
 
        self.db.transaction_start()
 
567
 
    def commit_write_group(self):
 
568
 
        """Commit any pending changes."""
 
569
 
        self.db.transaction_commit()
 
571
 
    def abort_write_group(self):
 
572
 
        """Abort any pending changes."""
 
573
 
        self.db.transaction_cancel()
 
576
 
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
578
 
    def lookup_commit(self, revid):
 
579
 
        return sha_to_hex(self.db["commit\0" + revid][:20])
 
581
 
    def lookup_blob_id(self, fileid, revision):
 
582
 
        return sha_to_hex(self.db["\0".join(("blob", fileid, revision))])
 
584
 
    def lookup_git_sha(self, sha):
 
585
 
        """Lookup a Git sha in the database.
 
587
 
        :param sha: Git object sha
 
588
 
        :return: (type, type_data) with type_data:
 
589
 
            commit: revid, tree sha
 
594
 
            sha = hex_to_sha(sha)
 
595
 
        data = self.db["git\0" + sha].split("\0")
 
596
 
        if data[0] == "commit":
 
598
 
                return (data[0], (data[1], data[2], {}))
 
600
 
                return (data[0], (data[1], data[2], {"testament3-sha1": data[3]}))
 
602
 
            return (data[0], tuple(data[1:]))
 
604
 
    def missing_revisions(self, revids):
 
607
 
            if self.db.get("commit\0" + revid) is None:
 
612
 
        """List the revision ids known."""
 
613
 
        for key in self.db.iterkeys():
 
614
 
            if key.startswith("commit\0"):
 
618
 
        """List the SHA1s."""
 
619
 
        for key in self.db.iterkeys():
 
620
 
            if key.startswith("git\0"):
 
621
 
                yield sha_to_hex(key[4:])
 
624
 
class VersionedFilesContentCache(ContentCache):
 
626
 
    def __init__(self, vf):
 
630
 
        self._vf.insert_record_stream(
 
631
 
            [versionedfile.ChunkedContentFactory((obj.id,), [], None,
 
632
 
                obj.as_legacy_object_chunks())])
 
634
 
    def __getitem__(self, sha):
 
635
 
        stream = self._vf.get_record_stream([(sha,)], 'unordered', True)
 
636
 
        entry = stream.next() 
 
637
 
        if entry.storage_kind == 'absent':
 
639
 
        return ShaFile._parse_legacy_object(entry.get_bytes_as('fulltext'))
 
642
 
class GitObjectStoreContentCache(ContentCache):
 
644
 
    def __init__(self, store):
 
647
 
    def add_multi(self, objs):
 
648
 
        self.store.add_objects(objs)
 
650
 
    def add(self, obj, path):
 
651
 
        self.store.add_object(obj)
 
653
 
    def __getitem__(self, sha):
 
654
 
        return self.store[sha]
 
657
 
class IndexCacheUpdater(CacheUpdater):
 
659
 
    def __init__(self, cache, rev):
 
661
 
        self.revid = rev.revision_id
 
662
 
        self.parent_revids = rev.parent_ids
 
665
 
        self._cache_objs = set()
 
667
 
    def add_object(self, obj, ie, path):
 
668
 
        if obj.type_name == "commit":
 
670
 
            assert type(ie) is dict
 
671
 
            self.cache.idmap._add_git_sha(obj.id, "commit",
 
672
 
                (self.revid, obj.tree, ie))
 
673
 
            self.cache.idmap._add_node(("commit", self.revid, "X"),
 
674
 
                " ".join((obj.id, obj.tree)))
 
675
 
            self._cache_objs.add((obj, path))
 
676
 
        elif obj.type_name == "blob":
 
677
 
            self.cache.idmap._add_git_sha(obj.id, "blob",
 
678
 
                (ie.file_id, ie.revision))
 
679
 
            self.cache.idmap._add_node(("blob", ie.file_id, ie.revision), obj.id)
 
680
 
            if ie.kind == "symlink":
 
681
 
                self._cache_objs.add((obj, path))
 
682
 
        elif obj.type_name == "tree":
 
683
 
            self.cache.idmap._add_git_sha(obj.id, "tree",
 
684
 
                (ie.file_id, self.revid))
 
685
 
            self._cache_objs.add((obj, path))
 
690
 
        self.cache.content_cache.add_multi(self._cache_objs)
 
694
 
class IndexBzrGitCache(BzrGitCache):
 
696
 
    def __init__(self, transport=None):
 
697
 
        mapper = versionedfile.ConstantMapper("trees")
 
698
 
        shamap = IndexGitShaMap(transport.clone('index'))
 
699
 
        #trees_store = knit.make_file_factory(True, mapper)(transport)
 
700
 
        #content_cache = VersionedFilesContentCache(trees_store)
 
701
 
        from bzrlib.plugins.git.transportgit import TransportObjectStore
 
702
 
        store = TransportObjectStore(transport.clone('objects'))
 
703
 
        content_cache = GitObjectStoreContentCache(store)
 
704
 
        super(IndexBzrGitCache, self).__init__(shamap, content_cache,
 
708
 
class IndexGitCacheFormat(BzrGitCacheFormat):
 
710
 
    def get_format_string(self):
 
711
 
        return 'bzr-git sha map with git object cache version 1\n'
 
713
 
    def initialize(self, transport):
 
714
 
        super(IndexGitCacheFormat, self).initialize(transport)
 
715
 
        transport.mkdir('index')
 
716
 
        transport.mkdir('objects')
 
717
 
        from bzrlib.plugins.git.transportgit import TransportObjectStore
 
718
 
        TransportObjectStore.init(transport.clone('objects'))
 
720
 
    def open(self, transport):
 
721
 
        return IndexBzrGitCache(transport)
 
724
 
class IndexGitShaMap(GitShaMap):
 
725
 
    """SHA Map that uses the Bazaar APIs to store a cache.
 
727
 
    BTree Index file with the following contents:
 
729
 
    ("git", <sha1>) -> "<type> <type-data1> <type-data2>"
 
730
 
    ("commit", <revid>) -> "<sha1> <tree-id>"
 
731
 
    ("blob", <fileid>, <revid>) -> <sha1>
 
735
 
    def __init__(self, transport=None):
 
736
 
        if transport is None:
 
737
 
            self._transport = None
 
738
 
            self._index = _mod_index.InMemoryGraphIndex(0, key_elements=3)
 
739
 
            self._builder = self._index
 
742
 
            self._transport = transport
 
743
 
            self._index = _mod_index.CombinedGraphIndex([])
 
744
 
            for name in self._transport.list_dir("."):
 
745
 
                if not name.endswith(".rix"):
 
747
 
                x = _mod_btree_index.BTreeGraphIndex(self._transport, name,
 
748
 
                    self._transport.stat(name).st_size)
 
749
 
                self._index.insert_index(0, x)
 
752
 
    def from_repository(cls, repository):
 
753
 
        transport = getattr(repository, "_transport", None)
 
754
 
        if transport is not None:
 
756
 
                transport.mkdir('git')
 
757
 
            except bzrlib.errors.FileExists:
 
759
 
            return cls(transport.clone('git'))
 
760
 
        from bzrlib.transport import get_transport
 
761
 
        return cls(get_transport(get_cache_dir()))
 
764
 
        if self._transport is not None:
 
765
 
            return "%s(%r)" % (self.__class__.__name__, self._transport.base)
 
767
 
            return "%s()" % (self.__class__.__name__)
 
770
 
        assert self._builder is None
 
771
 
        self.start_write_group()
 
772
 
        for _, key, value in self._index.iter_all_entries():
 
773
 
            self._builder.add_node(key, value)
 
775
 
        for name in self._transport.list_dir('.'):
 
776
 
            if name.endswith('.rix'):
 
777
 
                to_remove.append(name)
 
778
 
        self.commit_write_group()
 
779
 
        del self._index.indices[1:]
 
780
 
        for name in to_remove:
 
781
 
            self._transport.rename(name, name + '.old')
 
783
 
    def start_write_group(self):
 
784
 
        assert self._builder is None
 
785
 
        self._builder = _mod_btree_index.BTreeBuilder(0, key_elements=3)
 
786
 
        self._name = osutils.sha()
 
788
 
    def commit_write_group(self):
 
789
 
        assert self._builder is not None
 
790
 
        stream = self._builder.finish()
 
791
 
        name = self._name.hexdigest() + ".rix"
 
792
 
        size = self._transport.put_file(name, stream)
 
793
 
        index = _mod_btree_index.BTreeGraphIndex(self._transport, name, size)
 
794
 
        self._index.insert_index(0, index)
 
798
 
    def abort_write_group(self):
 
799
 
        assert self._builder is not None
 
803
 
    def _add_node(self, key, value):
 
805
 
            self._builder.add_node(key, value)
 
806
 
        except bzrlib.errors.BadIndexDuplicateKey:
 
807
 
            # Multiple bzr objects can have the same contents
 
812
 
    def _get_entry(self, key):
 
813
 
        entries = self._index.iter_entries([key])
 
815
 
            return entries.next()[2]
 
816
 
        except StopIteration:
 
817
 
            if self._builder is None:
 
819
 
            entries = self._builder.iter_entries([key])
 
821
 
                return entries.next()[2]
 
822
 
            except StopIteration:
 
825
 
    def _iter_keys_prefix(self, prefix):
 
826
 
        for entry in self._index.iter_entries_prefix([prefix]):
 
828
 
        if self._builder is not None:
 
829
 
            for entry in self._builder.iter_entries_prefix([prefix]):
 
832
 
    def lookup_commit(self, revid):
 
833
 
        return self._get_entry(("commit", revid, "X"))[:40]
 
835
 
    def _add_git_sha(self, hexsha, type, type_data):
 
836
 
        if hexsha is not None:
 
837
 
            self._name.update(hexsha)
 
839
 
                td = (type_data[0], type_data[1], type_data[2]["testament3-sha1"])
 
842
 
            self._add_node(("git", hexsha, "X"), " ".join((type,) + td))
 
844
 
            # This object is not represented in Git - perhaps an empty
 
846
 
            self._name.update(type + " ".join(type_data))
 
848
 
    def lookup_blob_id(self, fileid, revision):
 
849
 
        return self._get_entry(("blob", fileid, revision))
 
851
 
    def lookup_git_sha(self, sha):
 
853
 
            sha = sha_to_hex(sha)
 
854
 
        data = self._get_entry(("git", sha, "X")).split(" ", 3)
 
855
 
        if data[0] == "commit":
 
856
 
            return ("commit", (data[1], data[2], {"testament3-sha1": data[3]}))
 
858
 
            return (data[0], tuple(data[1:]))
 
861
 
        """List the revision ids known."""
 
862
 
        for key in self._iter_keys_prefix(("commit", None, None)):
 
865
 
    def missing_revisions(self, revids):
 
866
 
        """Return set of all the revisions that are not present."""
 
867
 
        missing_revids = set(revids)
 
868
 
        for _, key, value in self._index.iter_entries((
 
869
 
            ("commit", revid, "X") for revid in revids)):
 
870
 
            missing_revids.remove(key[1])
 
871
 
        return missing_revids
 
874
 
        """List the SHA1s."""
 
875
 
        for key in self._iter_keys_prefix(("git", None, None)):
 
879
 
formats = registry.Registry()
 
880
 
formats.register(TdbGitCacheFormat().get_format_string(),
 
882
 
formats.register(SqliteGitCacheFormat().get_format_string(),
 
883
 
    SqliteGitCacheFormat())
 
884
 
formats.register(IndexGitCacheFormat().get_format_string(),
 
885
 
    IndexGitCacheFormat())
 
886
 
# In the future, this will become the default:
 
887
 
# formats.register('default', IndexGitCacheFormat())
 
891
 
    formats.register('default', SqliteGitCacheFormat())
 
893
 
    formats.register('default', TdbGitCacheFormat())
 
897
 
def migrate_ancient_formats(repo_transport):
 
898
 
    # Prefer migrating git.db over git.tdb, since the latter may not 
 
899
 
    # be openable on some platforms.
 
900
 
    if repo_transport.has("git.db"):
 
901
 
        SqliteGitCacheFormat().initialize(repo_transport.clone("git"))
 
902
 
        repo_transport.rename("git.db", "git/idmap.db")
 
903
 
    elif repo_transport.has("git.tdb"):
 
904
 
        TdbGitCacheFormat().initialize(repo_transport.clone("git"))
 
905
 
        repo_transport.rename("git.tdb", "git/idmap.tdb")
 
908
 
def remove_readonly_transport_decorator(transport):
 
909
 
    if transport.is_readonly():
 
910
 
        return transport._decorated
 
914
 
def from_repository(repository):
 
915
 
    """Open a cache file for a repository.
 
917
 
    If the repository is remote and there is no transport available from it
 
918
 
    this will use a local file in the users cache directory
 
919
 
    (typically ~/.cache/bazaar/git/)
 
921
 
    :param repository: A repository object
 
923
 
    repo_transport = getattr(repository, "_transport", None)
 
924
 
    if repo_transport is not None:
 
925
 
        # Migrate older cache formats
 
926
 
        repo_transport = remove_readonly_transport_decorator(repo_transport)
 
928
 
            repo_transport.mkdir("git")
 
929
 
        except bzrlib.errors.FileExists:
 
932
 
            migrate_ancient_formats(repo_transport)
 
933
 
    return BzrGitCacheFormat.from_repository(repository)