~ubuntu-branches/debian/jessie/bzr-git/jessie

« back to all changes in this revision

Viewing changes to cache.py

  • Committer: Bazaar Package Importer
  • Author(s): Jelmer Vernooij
  • Date: 2010-07-31 00:18:45 UTC
  • mfrom: (1.1.11 upstream) (8.1.5 sid)
  • Revision ID: james.westby@ubuntu.com-20100731001845-su2xo23q9xdqdcyu
* New upstream release.
* Bump standards version to 3.9.1.

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