/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

Cope with files disappearing during commit.

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