/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

Fix fetching between git repositories.

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