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)
123
def lookup_commit(self, revid):
124
"""Retrieve a Git commit SHA by Bazaar revision id.
126
raise NotImplementedError(self.lookup_commit)
129
"""List the revision ids known."""
130
raise NotImplementedError(self.revids)
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):
137
return revids - present_revids
140
"""List the SHA1s."""
141
raise NotImplementedError(self.sha1s)
143
def start_write_group(self):
144
"""Start writing changes."""
146
def commit_write_group(self):
147
"""Commit any pending changes."""
149
def abort_write_group(self):
150
"""Abort any pending changes."""
153
class ContentCache(object):
154
"""Object that can cache Git objects."""
156
def add(self, object):
158
raise NotImplementedError(self.add)
160
def add_multi(self, objects):
161
"""Add multiple objects."""
165
def __getitem__(self, sha):
166
"""Retrieve an item, by SHA."""
167
raise NotImplementedError(self.__getitem__)
170
class BzrGitCacheFormat(object):
171
"""Bazaar-Git Cache Format."""
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)
177
def open(self, transport):
178
"""Open this format on a transport."""
179
raise NotImplementedError(self.open)
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())
186
def from_transport(self, transport):
187
"""Open a cache file present on a transport, or initialize one.
189
:param transport: Transport to use
190
:return: A BzrGitCache instance
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)
201
def from_repository(cls, repository):
202
"""Open a cache file for a repository.
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.
208
:param repository: Repository to open the cache for
209
:return: A `BzrGitCache`
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)
217
repo_transport.mkdir('git')
218
except bzrlib.errors.FileExists:
220
transport = repo_transport.clone('git')
222
transport = get_remote_cache_transport()
223
return cls.from_transport(transport)
226
class CacheUpdater(object):
227
"""Base class for objects that can update a bzr-git cache."""
229
def add_object(self, obj, ie, path):
232
:param obj: Object type ("commit", "blob" or "tree")
233
:param ie: Inventory entry (for blob/tree) or testament_sha in case
235
:param path: Path of the object (optional)
237
raise NotImplementedError(self.add_object)
240
raise NotImplementedError(self.finish)
243
class BzrGitCache(object):
244
"""Caching backend."""
246
def __init__(self, idmap, content_cache, cache_updater_klass):
248
self.content_cache = content_cache
249
self._cache_updater_klass = cache_updater_klass
251
def get_updater(self, rev):
252
"""Update an object that implements the CacheUpdater interface for
255
return self._cache_updater_klass(self, rev)
258
DictBzrGitCache = lambda: BzrGitCache(DictGitShaMap(), None, DictCacheUpdater)
261
class DictCacheUpdater(CacheUpdater):
262
"""Cache updater for dict-based caches."""
264
def __init__(self, cache, rev):
266
self.revid = rev.revision_id
267
self.parent_revids = rev.parent_ids
271
def add_object(self, obj, ie, path):
272
if obj.type_name == "commit":
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"):
279
if obj.type_name == "blob":
280
revision = ie.revision
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
287
self.cache.idmap._by_sha[obj.id] = (obj.type_name, type_data)
290
if self._commit is None:
291
raise AssertionError("No commit object added")
295
class DictGitShaMap(GitShaMap):
296
"""Git SHA map that uses a dictionary."""
303
def lookup_blob_id(self, fileid, revision):
304
return self._by_fileid[revision][fileid]
306
def lookup_git_sha(self, sha):
307
return self._by_sha[sha]
309
def lookup_tree_id(self, fileid, revision):
310
return self._by_fileid[revision][fileid]
312
def lookup_commit(self, revid):
313
return self._by_revid[revid]
316
for key, (type, type_data) in self._by_sha.iteritems():
321
return self._by_sha.iterkeys()
324
class SqliteCacheUpdater(CacheUpdater):
326
def __init__(self, cache, rev):
328
self.db = self.cache.idmap.db
329
self.revid = rev.revision_id
334
def add_object(self, obj, ie, path):
335
if obj.type_name == "commit":
337
self._testament3_sha1 = ie["testament3-sha1"]
338
assert type(ie) is dict
339
elif obj.type_name == "tree":
341
self._trees.append((obj.id, ie.file_id, self.revid))
342
elif obj.type_name == "blob":
344
self._blobs.append((obj.id, ie.file_id, ie.revision))
349
if self._commit is None:
350
raise AssertionError("No commit object added")
352
"replace into trees (sha1, fileid, revid) values (?, ?, ?)",
355
"replace into blobs (sha1, fileid, revid) values (?, ?, ?)",
358
"replace into commits (sha1, revid, tree_sha, testament3_sha1) values (?, ?, ?, ?)",
359
(self._commit.id, self.revid, self._commit.tree, self._testament3_sha1))
363
SqliteBzrGitCache = lambda p: BzrGitCache(SqliteGitShaMap(p), None, SqliteCacheUpdater)
366
class SqliteGitCacheFormat(BzrGitCacheFormat):
368
def get_format_string(self):
369
return 'bzr-git sha map version 1 using sqlite\n'
371
def open(self, transport):
373
basepath = transport.local_abspath(".")
374
except bzrlib.errors.NotLocalUrl:
375
basepath = get_cache_dir()
376
return SqliteBzrGitCache(os.path.join(basepath, "idmap.db"))
379
class SqliteGitShaMap(GitShaMap):
380
"""Bazaar GIT Sha map that uses a sqlite database for storage."""
382
def __init__(self, path=None):
385
self.db = sqlite3.connect(":memory:")
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),
395
tree_sha text not null check(length(tree_sha) == 40)
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,
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,
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);
415
self.db.executescript(
416
"ALTER TABLE commits ADD testament3_sha1 TEXT;")
417
except sqlite3.OperationalError:
418
pass # Column already exists.
421
return "%s(%r)" % (self.__class__.__name__, self.path)
423
def lookup_commit(self, revid):
424
cursor = self.db.execute("select sha1 from commits where revid = ?",
426
row = cursor.fetchone()
431
def commit_write_group(self):
434
def lookup_blob_id(self, fileid, revision):
435
row = self.db.execute("select sha1 from blobs where fileid = ? and revid = ?", (fileid, revision)).fetchone()
438
raise KeyError(fileid)
440
def lookup_tree_id(self, fileid, revision):
441
row = self.db.execute("select sha1 from trees where fileid = ? and revid = ?", (fileid, revision)).fetchone()
444
raise KeyError(fileid)
446
def lookup_git_sha(self, sha):
447
"""Lookup a Git sha in the database.
449
:param sha: Git object sha
450
:return: (type, type_data) with type_data:
451
commit: revid, tree sha, verifiers
455
row = self.db.execute("select revid, tree_sha, testament3_sha1 from commits where sha1 = ?", (sha,)).fetchone()
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()
461
row = self.db.execute("select fileid, revid from trees where sha1 = ?", (sha,)).fetchone()
467
"""List the revision ids known."""
468
return (row for (row,) in self.db.execute("select revid from commits"))
471
"""List the SHA1s."""
472
for table in ("blobs", "commits", "trees"):
473
for (sha,) in self.db.execute("select sha1 from %s" % table):
477
class TdbCacheUpdater(CacheUpdater):
478
"""Cache updater for tdb-based caches."""
480
def __init__(self, cache, rev):
482
self.db = cache.idmap.db
483
self.revid = rev.revision_id
484
self.parent_revids = rev.parent_ids
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"])
495
elif obj.type_name == "blob":
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":
503
type_data = (ie.file_id, self.revid)
506
self.db["git\0" + sha] = "\0".join((obj.type_name, ) + type_data)
509
if self._commit is None:
510
raise AssertionError("No commit object added")
514
TdbBzrGitCache = lambda p: BzrGitCache(TdbGitShaMap(p), None, TdbCacheUpdater)
516
class TdbGitCacheFormat(BzrGitCacheFormat):
517
"""Cache format for tdb-based caches."""
519
def get_format_string(self):
520
return 'bzr-git sha map version 3 using tdb\n'
522
def open(self, transport):
524
basepath = transport.local_abspath(".")
525
except bzrlib.errors.NotLocalUrl:
526
basepath = get_cache_dir()
528
return TdbBzrGitCache(os.path.join(basepath, "idmap.tdb"))
531
"Unable to open existing bzr-git cache because 'tdb' is not "
535
class TdbGitShaMap(GitShaMap):
536
"""SHA Map that uses a TDB database.
540
"git <sha1>" -> "<type> <type-data1> <type-data2>"
541
"commit revid" -> "<sha1> <tree-id>"
542
"tree fileid revid" -> "<sha1>"
543
"blob fileid revid" -> "<sha1>"
547
TDB_HASH_SIZE = 50000
549
def __init__(self, path=None):
555
if not mapdbs().has_key(path):
556
mapdbs()[path] = tdb.Tdb(path, self.TDB_HASH_SIZE, tdb.DEFAULT,
557
os.O_RDWR|os.O_CREAT)
558
self.db = mapdbs()[path]
560
if int(self.db["version"]) not in (2, 3):
561
trace.warning("SHA Map is incompatible (%s -> %d), rebuilding database.",
562
self.db["version"], self.TDB_MAP_VERSION)
566
self.db["version"] = str(self.TDB_MAP_VERSION)
568
def start_write_group(self):
569
"""Start writing changes."""
570
self.db.transaction_start()
572
def commit_write_group(self):
573
"""Commit any pending changes."""
574
self.db.transaction_commit()
576
def abort_write_group(self):
577
"""Abort any pending changes."""
578
self.db.transaction_cancel()
581
return "%s(%r)" % (self.__class__.__name__, self.path)
583
def lookup_commit(self, revid):
584
return sha_to_hex(self.db["commit\0" + revid][:20])
586
def lookup_blob_id(self, fileid, revision):
587
return sha_to_hex(self.db["\0".join(("blob", fileid, revision))])
589
def lookup_git_sha(self, sha):
590
"""Lookup a Git sha in the database.
592
:param sha: Git object sha
593
:return: (type, type_data) with type_data:
594
commit: revid, tree sha
599
sha = hex_to_sha(sha)
600
data = self.db["git\0" + sha].split("\0")
601
if data[0] == "commit":
603
return (data[0], (data[1], data[2], {}))
605
return (data[0], (data[1], data[2], {"testament3-sha1": data[3]}))
607
return (data[0], tuple(data[1:]))
609
def missing_revisions(self, revids):
612
if self.db.get("commit\0" + revid) is None:
617
"""List the revision ids known."""
618
for key in self.db.iterkeys():
619
if key.startswith("commit\0"):
623
"""List the SHA1s."""
624
for key in self.db.iterkeys():
625
if key.startswith("git\0"):
626
yield sha_to_hex(key[4:])
629
class VersionedFilesContentCache(ContentCache):
631
def __init__(self, vf):
635
self._vf.insert_record_stream(
636
[versionedfile.ChunkedContentFactory((obj.id,), [], None,
637
obj.as_legacy_object_chunks())])
639
def __getitem__(self, sha):
640
stream = self._vf.get_record_stream([(sha,)], 'unordered', True)
641
entry = stream.next()
642
if entry.storage_kind == 'absent':
644
return ShaFile._parse_legacy_object(entry.get_bytes_as('fulltext'))
647
class GitObjectStoreContentCache(ContentCache):
649
def __init__(self, store):
652
def add_multi(self, objs):
653
self.store.add_objects(objs)
655
def add(self, obj, path):
656
self.store.add_object(obj)
658
def __getitem__(self, sha):
659
return self.store[sha]
662
class IndexCacheUpdater(CacheUpdater):
664
def __init__(self, cache, rev):
666
self.revid = rev.revision_id
667
self.parent_revids = rev.parent_ids
670
self._cache_objs = set()
672
def add_object(self, obj, ie, path):
673
if obj.type_name == "commit":
675
assert type(ie) is dict
676
self.cache.idmap._add_git_sha(obj.id, "commit",
677
(self.revid, obj.tree, ie))
678
self.cache.idmap._add_node(("commit", self.revid, "X"),
679
" ".join((obj.id, obj.tree)))
680
self._cache_objs.add((obj, path))
681
elif obj.type_name == "blob":
682
self.cache.idmap._add_git_sha(obj.id, "blob",
683
(ie.file_id, ie.revision))
684
self.cache.idmap._add_node(("blob", ie.file_id, ie.revision), obj.id)
685
if ie.kind == "symlink":
686
self._cache_objs.add((obj, path))
687
elif obj.type_name == "tree":
688
self.cache.idmap._add_git_sha(obj.id, "tree",
689
(ie.file_id, self.revid))
690
self._cache_objs.add((obj, path))
695
self.cache.content_cache.add_multi(self._cache_objs)
699
class IndexBzrGitCache(BzrGitCache):
701
def __init__(self, transport=None):
702
mapper = versionedfile.ConstantMapper("trees")
703
shamap = IndexGitShaMap(transport.clone('index'))
704
#trees_store = knit.make_file_factory(True, mapper)(transport)
705
#content_cache = VersionedFilesContentCache(trees_store)
706
from bzrlib.plugins.git.transportgit import TransportObjectStore
707
store = TransportObjectStore(transport.clone('objects'))
708
content_cache = GitObjectStoreContentCache(store)
709
super(IndexBzrGitCache, self).__init__(shamap, content_cache,
713
class IndexGitCacheFormat(BzrGitCacheFormat):
715
def get_format_string(self):
716
return 'bzr-git sha map with git object cache version 1\n'
718
def initialize(self, transport):
719
super(IndexGitCacheFormat, self).initialize(transport)
720
transport.mkdir('index')
721
transport.mkdir('objects')
722
from bzrlib.plugins.git.transportgit import TransportObjectStore
723
TransportObjectStore.init(transport.clone('objects'))
725
def open(self, transport):
726
return IndexBzrGitCache(transport)
729
class IndexGitShaMap(GitShaMap):
730
"""SHA Map that uses the Bazaar APIs to store a cache.
732
BTree Index file with the following contents:
734
("git", <sha1>) -> "<type> <type-data1> <type-data2>"
735
("commit", <revid>) -> "<sha1> <tree-id>"
736
("blob", <fileid>, <revid>) -> <sha1>
740
def __init__(self, transport=None):
741
if transport is None:
742
self._transport = None
743
self._index = _mod_index.InMemoryGraphIndex(0, key_elements=3)
744
self._builder = self._index
747
self._transport = transport
748
self._index = _mod_index.CombinedGraphIndex([])
749
for name in self._transport.list_dir("."):
750
if not name.endswith(".rix"):
752
x = _mod_btree_index.BTreeGraphIndex(self._transport, name,
753
self._transport.stat(name).st_size)
754
self._index.insert_index(0, x)
757
def from_repository(cls, repository):
758
transport = getattr(repository, "_transport", None)
759
if transport is not None:
761
transport.mkdir('git')
762
except bzrlib.errors.FileExists:
764
return cls(transport.clone('git'))
765
from bzrlib.transport import get_transport
766
return cls(get_transport(get_cache_dir()))
769
if self._transport is not None:
770
return "%s(%r)" % (self.__class__.__name__, self._transport.base)
772
return "%s()" % (self.__class__.__name__)
775
assert self._builder is None
776
self.start_write_group()
777
for _, key, value in self._index.iter_all_entries():
778
self._builder.add_node(key, value)
780
for name in self._transport.list_dir('.'):
781
if name.endswith('.rix'):
782
to_remove.append(name)
783
self.commit_write_group()
784
del self._index.indices[1:]
785
for name in to_remove:
786
self._transport.rename(name, name + '.old')
788
def start_write_group(self):
789
assert self._builder is None
790
self._builder = _mod_btree_index.BTreeBuilder(0, key_elements=3)
791
self._name = osutils.sha()
793
def commit_write_group(self):
794
assert self._builder is not None
795
stream = self._builder.finish()
796
name = self._name.hexdigest() + ".rix"
797
size = self._transport.put_file(name, stream)
798
index = _mod_btree_index.BTreeGraphIndex(self._transport, name, size)
799
self._index.insert_index(0, index)
803
def abort_write_group(self):
804
assert self._builder is not None
808
def _add_node(self, key, value):
810
self._builder.add_node(key, value)
811
except bzrlib.errors.BadIndexDuplicateKey:
812
# Multiple bzr objects can have the same contents
817
def _get_entry(self, key):
818
entries = self._index.iter_entries([key])
820
return entries.next()[2]
821
except StopIteration:
822
if self._builder is None:
824
entries = self._builder.iter_entries([key])
826
return entries.next()[2]
827
except StopIteration:
830
def _iter_keys_prefix(self, prefix):
831
for entry in self._index.iter_entries_prefix([prefix]):
833
if self._builder is not None:
834
for entry in self._builder.iter_entries_prefix([prefix]):
837
def lookup_commit(self, revid):
838
return self._get_entry(("commit", revid, "X"))[:40]
840
def _add_git_sha(self, hexsha, type, type_data):
841
if hexsha is not None:
842
self._name.update(hexsha)
844
td = (type_data[0], type_data[1], type_data[2]["testament3-sha1"])
847
self._add_node(("git", hexsha, "X"), " ".join((type,) + td))
849
# This object is not represented in Git - perhaps an empty
851
self._name.update(type + " ".join(type_data))
853
def lookup_blob_id(self, fileid, revision):
854
return self._get_entry(("blob", fileid, revision))
856
def lookup_git_sha(self, sha):
858
sha = sha_to_hex(sha)
859
data = self._get_entry(("git", sha, "X")).split(" ", 3)
860
if data[0] == "commit":
861
return ("commit", (data[1], data[2], {"testament3-sha1": data[3]}))
863
return (data[0], tuple(data[1:]))
866
"""List the revision ids known."""
867
for key in self._iter_keys_prefix(("commit", None, None)):
870
def missing_revisions(self, revids):
871
"""Return set of all the revisions that are not present."""
872
missing_revids = set(revids)
873
for _, key, value in self._index.iter_entries((
874
("commit", revid, "X") for revid in revids)):
875
missing_revids.remove(key[1])
876
return missing_revids
879
"""List the SHA1s."""
880
for key in self._iter_keys_prefix(("git", None, None)):
884
formats = registry.Registry()
885
formats.register(TdbGitCacheFormat().get_format_string(),
887
formats.register(SqliteGitCacheFormat().get_format_string(),
888
SqliteGitCacheFormat())
889
formats.register(IndexGitCacheFormat().get_format_string(),
890
IndexGitCacheFormat())
891
# In the future, this will become the default:
892
# formats.register('default', IndexGitCacheFormat())
896
formats.register('default', SqliteGitCacheFormat())
898
formats.register('default', TdbGitCacheFormat())
902
def migrate_ancient_formats(repo_transport):
903
# Prefer migrating git.db over git.tdb, since the latter may not
904
# be openable on some platforms.
905
if repo_transport.has("git.db"):
906
SqliteGitCacheFormat().initialize(repo_transport.clone("git"))
907
repo_transport.rename("git.db", "git/idmap.db")
908
elif repo_transport.has("git.tdb"):
909
TdbGitCacheFormat().initialize(repo_transport.clone("git"))
910
repo_transport.rename("git.tdb", "git/idmap.tdb")
913
def remove_readonly_transport_decorator(transport):
914
if transport.is_readonly():
915
return transport._decorated
919
def from_repository(repository):
920
"""Open a cache file for a repository.
922
If the repository is remote and there is no transport available from it
923
this will use a local file in the users cache directory
924
(typically ~/.cache/bazaar/git/)
926
:param repository: A repository object
928
repo_transport = getattr(repository, "_transport", None)
929
if repo_transport is not None:
930
# Migrate older cache formats
931
repo_transport = remove_readonly_transport_decorator(repo_transport)
933
repo_transport.mkdir("git")
934
except bzrlib.errors.FileExists:
937
migrate_ancient_formats(repo_transport)
938
return BzrGitCacheFormat.from_repository(repository)