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)