/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

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-04-13 23:16:57 UTC
  • mfrom: (1662.1.1 bzr.mbp.integration)
  • Revision ID: pqm@pqm.ubuntu.com-20060413231657-bce3d67d3e7a4f2b
(mbp/olaf) push/pull/merge --remember improvements

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