/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 shamap.py

Move lookup_tree/lookup_blob to a separate object.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2009 Canonical Ltd
 
1
# Copyright (C) 2009 Jelmer Vernooij <jelmer@samba.org>
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
16
16
 
17
17
"""Map from Git sha's to Bazaar objects."""
18
18
 
 
19
from dulwich.objects import (
 
20
    sha_to_hex,
 
21
    hex_to_sha,
 
22
    )
 
23
import os
 
24
import threading
 
25
 
19
26
import bzrlib
20
 
 
21
 
from bzrlib.errors import NoSuchRevision
22
 
 
23
 
import os
 
27
from bzrlib import (
 
28
    trace,
 
29
    ui,
 
30
    )
 
31
 
 
32
 
 
33
def get_cache_dir():
 
34
    try:
 
35
        from xdg.BaseDirectory import xdg_cache_home
 
36
    except ImportError:
 
37
        from bzrlib.config import config_dir
 
38
        ret = os.path.join(config_dir(), "git")
 
39
    else:
 
40
        ret = os.path.join(xdg_cache_home, "bazaar", "git")
 
41
    if not os.path.isdir(ret):
 
42
        os.makedirs(ret)
 
43
    return ret
24
44
 
25
45
 
26
46
def check_pysqlite_version(sqlite3):
27
47
    """Check that sqlite library is compatible.
28
48
 
29
49
    """
30
 
    if (sqlite3.sqlite_version_info[0] < 3 or 
31
 
            (sqlite3.sqlite_version_info[0] == 3 and 
 
50
    if (sqlite3.sqlite_version_info[0] < 3 or
 
51
            (sqlite3.sqlite_version_info[0] == 3 and
32
52
             sqlite3.sqlite_version_info[1] < 3)):
33
 
        warning('Needs at least sqlite 3.3.x')
 
53
        trace.warning('Needs at least sqlite 3.3.x')
34
54
        raise bzrlib.errors.BzrError("incompatible sqlite library")
35
55
 
36
56
try:
37
57
    try:
38
58
        import sqlite3
39
59
        check_pysqlite_version(sqlite3)
40
 
    except (ImportError, bzrlib.errors.BzrError), e: 
 
60
    except (ImportError, bzrlib.errors.BzrError), e:
41
61
        from pysqlite2 import dbapi2 as sqlite3
42
62
        check_pysqlite_version(sqlite3)
43
63
except:
44
 
    warning('Needs at least Python2.5 or Python2.4 with the pysqlite2 '
 
64
    trace.warning('Needs at least Python2.5 or Python2.4 with the pysqlite2 '
45
65
            'module')
46
66
    raise bzrlib.errors.BzrError("missing sqlite library")
47
67
 
48
68
 
 
69
_mapdbs = threading.local()
 
70
def mapdbs():
 
71
    """Get a cache for this thread's db connections."""
 
72
    try:
 
73
        return _mapdbs.cache
 
74
    except AttributeError:
 
75
        _mapdbs.cache = {}
 
76
        return _mapdbs.cache
 
77
 
 
78
 
 
79
class InventorySHAMap(object):
 
80
    """Maps inventory file ids to Git SHAs."""
 
81
 
 
82
    def lookup_blob(self, file_id, revision_hint=None):
 
83
        """Retrieve a Git blob SHA by file id.
 
84
 
 
85
        :param file_id: File id of the file/symlink
 
86
        :param revision_hint: Optional revision in which the file was last
 
87
            changed.
 
88
        """
 
89
        raise NotImplementedError(self.lookup_blob)
 
90
 
 
91
    def lookup_tree(self, file_id):
 
92
        """Retrieve a Git tree SHA by file id.
 
93
        """
 
94
        raise NotImplementedError(self.lookup_tree)
 
95
 
 
96
 
49
97
class GitShaMap(object):
50
 
 
51
 
    def __init__(self, transport):
52
 
        self.transport = transport
53
 
        self.db = sqlite3.connect(
54
 
            os.path.join(self.transport.local_abspath("."), "git.db"))
 
98
    """Git<->Bzr revision id mapping database."""
 
99
 
 
100
    def add_entry(self, sha, type, type_data):
 
101
        """Add a new entry to the database.
 
102
        """
 
103
        raise NotImplementedError(self.add_entry)
 
104
 
 
105
    def add_entries(self, entries):
 
106
        """Add multiple new entries to the database.
 
107
        """
 
108
        for e in entries:
 
109
            self.add_entry(*e)
 
110
 
 
111
    def get_inventory_sha_map(self, revid):
 
112
        """Return the inventory SHA map for a revision.
 
113
 
 
114
        :param revid: Revision to fetch the map for
 
115
        :return: A `InventorySHAMap`
 
116
        """
 
117
        raise NotImplementedError(self.get_inventory_sha_map)
 
118
 
 
119
    def lookup_git_sha(self, sha):
 
120
        """Lookup a Git sha in the database.
 
121
 
 
122
        :param sha: Git object sha
 
123
        :return: (type, type_data) with type_data:
 
124
            revision: revid, tree sha
 
125
        """
 
126
        raise NotImplementedError(self.lookup_git_sha)
 
127
 
 
128
    def revids(self):
 
129
        """List the revision ids known."""
 
130
        raise NotImplementedError(self.revids)
 
131
 
 
132
    def missing_revisions(self, revids):
 
133
        """Return set of all the revisions that are not present."""
 
134
        present_revids = set(self.revids())
 
135
        if not isinstance(revids, set):
 
136
            revids = set(revids)
 
137
        return revids - present_revids
 
138
 
 
139
    def sha1s(self):
 
140
        """List the SHA1s."""
 
141
        raise NotImplementedError(self.sha1s)
 
142
 
 
143
    def start_write_group(self):
 
144
        """Start writing changes."""
 
145
 
 
146
    def commit_write_group(self):
 
147
        """Commit any pending changes."""
 
148
 
 
149
    def abort_write_group(self):
 
150
        """Abort any pending changes."""
 
151
 
 
152
 
 
153
class DictGitShaMap(GitShaMap):
 
154
 
 
155
    def __init__(self):
 
156
        self._by_sha = {}
 
157
        self._by_fileid = {}
 
158
 
 
159
    def add_entry(self, sha, type, type_data):
 
160
        self._by_sha[sha] = (type, type_data)
 
161
        if type in ("blob", "tree"):
 
162
            self._by_fileid.setdefault(type_data[1], {})[type_data[0]] = sha
 
163
 
 
164
    def get_inventory_sha_map(self, revid):
 
165
        class DictInventorySHAMap(InventorySHAMap):
 
166
 
 
167
            def __init__(self, base, revid):
 
168
                self._base = base
 
169
                self.revid = revid
 
170
 
 
171
            def lookup_blob(self, fileid, revision_hint=None):
 
172
                if revision_hint is not None:
 
173
                    revid = revision_hint
 
174
                else:
 
175
                    revid = self.revid
 
176
                return self._base._by_fileid[revid][fileid]
 
177
 
 
178
            def lookup_tree(self, fileid):
 
179
                return self._base._by_fileid[self.revid][fileid]
 
180
 
 
181
        return DictInventorySHAMap(self, revid)
 
182
 
 
183
    def lookup_git_sha(self, sha):
 
184
        return self._by_sha[sha]
 
185
 
 
186
    def revids(self):
 
187
        for key, (type, type_data) in self._by_sha.iteritems():
 
188
            if type == "commit":
 
189
                yield type_data[0]
 
190
 
 
191
    def sha1s(self):
 
192
        return self._by_sha.iterkeys()
 
193
 
 
194
 
 
195
class SqliteGitShaMap(GitShaMap):
 
196
 
 
197
    def __init__(self, path=None):
 
198
        self.path = path
 
199
        if path is None:
 
200
            self.db = sqlite3.connect(":memory:")
 
201
        else:
 
202
            if not mapdbs().has_key(path):
 
203
                mapdbs()[path] = sqlite3.connect(path)
 
204
            self.db = mapdbs()[path]
 
205
        self.db.text_factory = str
55
206
        self.db.executescript("""
56
 
        create table if not exists commits(sha1 text, revid text, tree_sha text);
 
207
        create table if not exists commits(
 
208
            sha1 text not null check(length(sha1) == 40),
 
209
            revid text not null,
 
210
            tree_sha text not null check(length(tree_sha) == 40)
 
211
        );
57
212
        create index if not exists commit_sha1 on commits(sha1);
58
 
        create table if not exists blobs(sha1 text, fileid text, revid text);
 
213
        create unique index if not exists commit_revid on commits(revid);
 
214
        create table if not exists blobs(
 
215
            sha1 text not null check(length(sha1) == 40),
 
216
            fileid text not null,
 
217
            revid text not null
 
218
        );
59
219
        create index if not exists blobs_sha1 on blobs(sha1);
60
 
        create table if not exists trees(sha1 text, fileid text, revid text);
61
 
        create index if not exists trees_sha1 on trees(sha1);
 
220
        create unique index if not exists blobs_fileid_revid on blobs(fileid, revid);
 
221
        create table if not exists trees(
 
222
            sha1 text unique not null check(length(sha1) == 40),
 
223
            fileid text not null,
 
224
            revid text not null
 
225
        );
 
226
        create unique index if not exists trees_sha1 on trees(sha1);
 
227
        create unique index if not exists trees_fileid_revid on trees(fileid, revid);
62
228
""")
63
229
 
64
 
    def _parent_lookup(self, revid):
65
 
        return self.db.execute("select sha1 from commits where revid = ?", (revid,)).fetchone()[0].encode("utf-8")
 
230
    def __repr__(self):
 
231
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
232
    
 
233
    @classmethod
 
234
    def remove_for_repository(cls, repository):
 
235
        repository._transport.delete('git.db')
 
236
 
 
237
    @classmethod
 
238
    def exists_for_repository(cls, repository):
 
239
        try:
 
240
            transport = getattr(repository, "_transport", None)
 
241
            if transport is not None:
 
242
                return transport.has("git.db")
 
243
        except bzrlib.errors.NotLocalUrl:
 
244
            return False
 
245
 
 
246
    @classmethod
 
247
    def from_repository(cls, repository):
 
248
        try:
 
249
            transport = getattr(repository, "_transport", None)
 
250
            if transport is not None:
 
251
                return cls(os.path.join(transport.local_abspath("."), "git.db"))
 
252
        except bzrlib.errors.NotLocalUrl:
 
253
            pass
 
254
        return cls(os.path.join(get_cache_dir(), "remote.db"))
 
255
 
 
256
    def lookup_commit(self, revid):
 
257
        row = self.db.execute("select sha1 from commits where revid = ?", (revid,)).fetchone()
 
258
        if row is not None:
 
259
            return row[0]
 
260
        raise KeyError
 
261
 
 
262
    def commit_write_group(self):
 
263
        self.db.commit()
 
264
 
 
265
    def add_entries(self, entries):
 
266
        trees = []
 
267
        blobs = []
 
268
        for sha, type, type_data in entries:
 
269
            assert isinstance(type_data[0], str)
 
270
            assert isinstance(type_data[1], str)
 
271
            entry = (sha, type_data[0], type_data[1])
 
272
            if type == "tree":
 
273
                trees.append(entry)
 
274
            elif type == "blob":
 
275
                blobs.append(entry)
 
276
            else:
 
277
                raise AssertionError
 
278
        if trees:
 
279
            self.db.executemany("replace into trees (sha1, fileid, revid) values (?, ?, ?)", trees)
 
280
        if blobs:
 
281
            self.db.executemany("replace into blobs (sha1, fileid, revid) values (?, ?, ?)", blobs)
 
282
 
66
283
 
67
284
    def add_entry(self, sha, type, type_data):
68
285
        """Add a new entry to the database.
69
286
        """
70
287
        assert isinstance(type_data, tuple)
 
288
        if sha is None:
 
289
            return
71
290
        assert isinstance(sha, str), "type was %r" % sha
72
291
        if type == "commit":
73
292
            self.db.execute("replace into commits (sha1, revid, tree_sha) values (?, ?, ?)", (sha, type_data[0], type_data[1]))
74
 
        elif type == "blob":
75
 
            self.db.execute("replace into blobs (sha1, fileid, revid) values (?, ?, ?)", (sha, type_data[0], type_data[1]))
76
 
        elif type == "tree":
77
 
            self.db.execute("replace into trees (sha1, fileid, revid) values (?, ?, ?)", (sha, type_data[0], type_data[1]))
 
293
        elif type in ("blob", "tree"):
 
294
            self.db.execute("replace into %ss (sha1, fileid, revid) values (?, ?, ?)" % type, (sha, type_data[0], type_data[1]))
78
295
        else:
79
296
            raise AssertionError("Unknown type %s" % type)
80
297
 
 
298
    def get_inventory_sha_map(self, revid):
 
299
        class SqliteInventorySHAMap(InventorySHAMap):
 
300
 
 
301
            def __init__(self, db, revid):
 
302
                self.db = db
 
303
                self.revid = revid
 
304
 
 
305
            def lookup_blob(self, fileid, revision_hint=None):
 
306
                if revision_hint is not None:
 
307
                    revid = revision_hint
 
308
                else:
 
309
                    revid = self.revid
 
310
                row = self.db.execute("select sha1 from blobs where fileid = ? and revid = ?", (fileid, revid)).fetchone()
 
311
                if row is not None:
 
312
                    return row[0]
 
313
                raise KeyError(fileid)
 
314
 
 
315
            def lookup_tree(self, fileid):
 
316
                row = self.db.execute("select sha1 from trees where fileid = ? and revid = ?", (fileid, self.revid)).fetchone()
 
317
                if row is not None:
 
318
                    return row[0]
 
319
                raise KeyError(fileid)
 
320
 
 
321
        return SqliteInventorySHAMap(self.db, revid)
 
322
 
81
323
    def lookup_git_sha(self, sha):
82
324
        """Lookup a Git sha in the database.
83
325
 
85
327
        :return: (type, type_data) with type_data:
86
328
            revision: revid, tree sha
87
329
        """
 
330
        def format(type, row):
 
331
            return (type, (row[0], row[1]))
88
332
        row = self.db.execute("select revid, tree_sha from commits where sha1 = ?", (sha,)).fetchone()
89
333
        if row is not None:
90
 
            return ("commit", row)
 
334
            return format("commit", row)
91
335
        row = self.db.execute("select fileid, revid from blobs where sha1 = ?", (sha,)).fetchone()
92
336
        if row is not None:
93
 
            return ("blob", row)
 
337
            return format("blob", row)
94
338
        row = self.db.execute("select fileid, revid from trees where sha1 = ?", (sha,)).fetchone()
95
339
        if row is not None:
96
 
            return ("tree", row)
 
340
            return format("tree", row)
97
341
        raise KeyError(sha)
98
342
 
99
343
    def revids(self):
100
 
        for row in self.db.execute("select revid from commits").fetchall():
101
 
            yield row[0]
 
344
        """List the revision ids known."""
 
345
        return (row for (row,) in self.db.execute("select revid from commits"))
 
346
 
 
347
    def sha1s(self):
 
348
        """List the SHA1s."""
 
349
        for table in ("blobs", "commits", "trees"):
 
350
            trace.note(table)
 
351
            for (row,) in self.db.execute("select sha1 from %s" % table):
 
352
                yield row
 
353
 
 
354
 
 
355
TDB_MAP_VERSION = 3
 
356
TDB_HASH_SIZE = 50000
 
357
 
 
358
 
 
359
class TdbGitShaMap(GitShaMap):
 
360
    """SHA Map that uses a TDB database.
 
361
 
 
362
    Entries:
 
363
 
 
364
    "git <sha1>" -> "<type> <type-data1> <type-data2>"
 
365
    "commit revid" -> "<sha1> <tree-id>"
 
366
    "tree fileid revid" -> "<sha1>"
 
367
    "blob fileid revid" -> "<sha1>"
 
368
    """
 
369
 
 
370
    def __init__(self, path=None):
 
371
        import tdb
 
372
        self.path = path
 
373
        if path is None:
 
374
            self.db = {}
 
375
        else:
 
376
            if not mapdbs().has_key(path):
 
377
                mapdbs()[path] = tdb.Tdb(path, TDB_HASH_SIZE, tdb.DEFAULT,
 
378
                                          os.O_RDWR|os.O_CREAT)
 
379
            self.db = mapdbs()[path]
 
380
        try:
 
381
            if int(self.db["version"]) not in (2, 3):
 
382
                trace.warning("SHA Map is incompatible (%s -> %d), rebuilding database.",
 
383
                              self.db["version"], TDB_MAP_VERSION)
 
384
                self.db.clear()
 
385
        except KeyError:
 
386
            pass
 
387
        self.db["version"] = str(TDB_MAP_VERSION)
 
388
 
 
389
    def __repr__(self):
 
390
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
391
 
 
392
    @classmethod
 
393
    def exists_for_repository(cls, repository):
 
394
        try:
 
395
            transport = getattr(repository, "_transport", None)
 
396
            if transport is not None:
 
397
                return transport.has("git.tdb")
 
398
        except bzrlib.errors.NotLocalUrl:
 
399
            return False
 
400
 
 
401
    @classmethod
 
402
    def remove_for_repository(cls, repository):
 
403
        repository._transport.delete('git.tdb')
 
404
 
 
405
    @classmethod
 
406
    def from_repository(cls, repository):
 
407
        try:
 
408
            transport = getattr(repository, "_transport", None)
 
409
            if transport is not None:
 
410
                return cls(os.path.join(transport.local_abspath("."), "git.tdb"))
 
411
        except bzrlib.errors.NotLocalUrl:
 
412
            pass
 
413
        return cls(os.path.join(get_cache_dir(), "remote.tdb"))
 
414
 
 
415
    def lookup_commit(self, revid):
 
416
        return sha_to_hex(self.db["commit\0" + revid][:20])
 
417
 
 
418
    def add_entry(self, hexsha, type, type_data):
 
419
        """Add a new entry to the database.
 
420
        """
 
421
        if hexsha is None:
 
422
            sha = ""
 
423
        else:
 
424
            sha = hex_to_sha(hexsha)
 
425
            self.db["git\0" + sha] = "\0".join((type, type_data[0], type_data[1]))
 
426
        if type == "commit":
 
427
            self.db["commit\0" + type_data[0]] = "\0".join((sha, type_data[1]))
 
428
        elif type == "blob":
 
429
            self.db["\0".join(("blob", type_data[0], type_data[1]))] = sha
 
430
 
 
431
    def get_inventory_sha_map(self, revid):
 
432
 
 
433
        class TdbInventorySHAMap(InventorySHAMap):
 
434
 
 
435
            def __init__(self, db, revid):
 
436
                self.db = db
 
437
                self.revid = revid
 
438
 
 
439
            def lookup_blob(self, fileid, revision_hint=None):
 
440
                if revision_hint is not None:
 
441
                    revid = revision_hint
 
442
                else:
 
443
                    revid = self.revid
 
444
                return sha_to_hex(self.db["\0".join(("blob", fileid, revid))])
 
445
                
 
446
        return TdbInventorySHAMap(self.db, revid)
 
447
 
 
448
    def lookup_git_sha(self, sha):
 
449
        """Lookup a Git sha in the database.
 
450
 
 
451
        :param sha: Git object sha
 
452
        :return: (type, type_data) with type_data:
 
453
            revision: revid, tree sha
 
454
        """
 
455
        if len(sha) == 40:
 
456
            sha = hex_to_sha(sha)
 
457
        data = self.db["git\0" + sha].split("\0")
 
458
        return (data[0], (data[1], data[2]))
 
459
 
 
460
    def missing_revisions(self, revids):
 
461
        ret = set()
 
462
        for revid in revids:
 
463
            if self.db.get("commit\0" + revid) is None:
 
464
                ret.add(revid)
 
465
        return ret
 
466
 
 
467
    def revids(self):
 
468
        """List the revision ids known."""
 
469
        for key in self.db.iterkeys():
 
470
            if key.startswith("commit\0"):
 
471
                yield key[7:]
 
472
 
 
473
    def sha1s(self):
 
474
        """List the SHA1s."""
 
475
        for key in self.db.iterkeys():
 
476
            if key.startswith("git\0"):
 
477
                yield sha_to_hex(key[4:])
 
478
 
 
479
 
 
480
def migrate(source, target):
 
481
    """Migrate from one cache map to another."""
 
482
    pb = ui.ui_factory.nested_progress_bar()
 
483
    try:
 
484
        target.start_write_group()
 
485
        try:
 
486
            for i, sha in enumerate(source.sha1s()):
 
487
                pb.update("migrating sha map", i)
 
488
                (kind, info) = source.lookup_git_sha(sha)
 
489
                target.add_entry(sha, kind, info)
 
490
        except:
 
491
            target.abort_write_group()
 
492
            raise
 
493
        else:
 
494
            target.commit_write_group()
 
495
    finally:
 
496
        pb.finished()
 
497
 
 
498
 
 
499
def from_repository(repository):
 
500
    upgrade_from = []
 
501
    try:
 
502
        shamap = TdbGitShaMap.from_repository(repository)
 
503
        upgrade_from = [SqliteGitShaMap]
 
504
    except ImportError:
 
505
        shamap = SqliteGitShaMap.from_repository(repository)
 
506
    for cls in upgrade_from:
 
507
        if not cls.exists_for_repository(repository):
 
508
            continue
 
509
        old_shamap = cls.from_repository(repository)
 
510
        trace.info('Importing SHA map from %r into %r',
 
511
            old_shamap, shamap)
 
512
        migrate(old_shamap, shamap)
 
513
        cls.remove_for_repository(repository)
 
514
    return shamap