1
# Copyright (C) 2009 Jelmer Vernooij <jelmer@samba.org>
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.
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.
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
17
"""Map from Git sha's to Bazaar objects."""
19
from dulwich.objects import (
34
from xdg.BaseDirectory import xdg_cache_home
36
from bzrlib.config import config_dir
37
ret = os.path.join(config_dir(), "git")
39
ret = os.path.join(xdg_cache_home, "bazaar", "git")
40
if not os.path.isdir(ret):
45
def check_pysqlite_version(sqlite3):
46
"""Check that sqlite library is compatible.
49
if (sqlite3.sqlite_version_info[0] < 3 or
50
(sqlite3.sqlite_version_info[0] == 3 and
51
sqlite3.sqlite_version_info[1] < 3)):
52
trace.warning('Needs at least sqlite 3.3.x')
53
raise bzrlib.errors.BzrError("incompatible sqlite library")
58
check_pysqlite_version(sqlite3)
59
except (ImportError, bzrlib.errors.BzrError), e:
60
from pysqlite2 import dbapi2 as sqlite3
61
check_pysqlite_version(sqlite3)
63
trace.warning('Needs at least Python2.5 or Python2.4 with the pysqlite2 '
65
raise bzrlib.errors.BzrError("missing sqlite library")
68
_mapdbs = threading.local()
70
"""Get a cache for this thread's db connections."""
73
except AttributeError:
78
class InventorySHAMap(object):
79
"""Maps inventory file ids to Git SHAs."""
81
def lookup_blob(self, file_id, revision_hint=None):
82
"""Retrieve a Git blob SHA by file id.
84
:param file_id: File id of the file/symlink
85
:param revision_hint: Optional revision in which the file was last
88
raise NotImplementedError(self.lookup_blob)
90
def lookup_tree(self, file_id):
91
"""Retrieve a Git tree SHA by file id.
93
raise NotImplementedError(self.lookup_tree)
96
class GitShaMap(object):
97
"""Git<->Bzr revision id mapping database."""
99
def _add_entry(self, sha, type, type_data):
100
"""Add a new entry to the database.
102
raise NotImplementedError(self._add_entry)
104
def add_entries(self, revid, parent_revids, commit_sha, root_tree_sha,
106
"""Add multiple new entries to the database.
108
for (fileid, kind, hexsha, revision) in entries:
109
self._add_entry(hexsha, kind, (fileid, revision))
110
self._add_entry(commit_sha, "commit", (revid, root_tree_sha))
112
def get_inventory_sha_map(self, revid):
113
"""Return the inventory SHA map for a revision.
115
:param revid: Revision to fetch the map for
116
:return: A `InventorySHAMap`
118
raise NotImplementedError(self.get_inventory_sha_map)
120
def lookup_git_sha(self, sha):
121
"""Lookup a Git sha in the database.
122
:param sha: Git object sha
123
:return: (type, type_data) with type_data:
124
revision: revid, tree sha
126
raise NotImplementedError(self.lookup_git_sha)
129
"""List the revision ids known."""
130
raise NotImplementedError(self.revids)
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):
137
return revids - present_revids
140
"""List the SHA1s."""
141
raise NotImplementedError(self.sha1s)
143
def start_write_group(self):
144
"""Start writing changes."""
146
def commit_write_group(self):
147
"""Commit any pending changes."""
149
def abort_write_group(self):
150
"""Abort any pending changes."""
153
class DictGitShaMap(GitShaMap):
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
164
def get_inventory_sha_map(self, revid):
166
class DictInventorySHAMap(InventorySHAMap):
168
def __init__(self, base, revid):
172
def lookup_blob(self, fileid, revision_hint=None):
173
if revision_hint is not None:
174
revid = revision_hint
177
return self._base._by_fileid[revid][fileid]
179
def lookup_tree(self, fileid):
180
return self._base._by_fileid[self.revid][fileid]
182
return DictInventorySHAMap(self, revid)
184
def lookup_git_sha(self, sha):
185
return self._by_sha[sha]
188
for key, (type, type_data) in self._by_sha.iteritems():
193
return self._by_sha.iterkeys()
196
class SqliteGitShaMap(GitShaMap):
198
def __init__(self, path=None):
201
self.db = sqlite3.connect(":memory:")
203
if not mapdbs().has_key(path):
204
mapdbs()[path] = sqlite3.connect(path)
205
self.db = mapdbs()[path]
206
self.db.text_factory = str
207
self.db.executescript("""
208
create table if not exists commits(
209
sha1 text not null check(length(sha1) == 40),
211
tree_sha text not null check(length(tree_sha) == 40)
213
create index if not exists commit_sha1 on commits(sha1);
214
create unique index if not exists commit_revid on commits(revid);
215
create table if not exists blobs(
216
sha1 text not null check(length(sha1) == 40),
217
fileid text not null,
220
create index if not exists blobs_sha1 on blobs(sha1);
221
create unique index if not exists blobs_fileid_revid on blobs(fileid, revid);
222
create table if not exists trees(
223
sha1 text unique not null check(length(sha1) == 40),
224
fileid text not null,
227
create unique index if not exists trees_sha1 on trees(sha1);
228
create unique index if not exists trees_fileid_revid on trees(fileid, revid);
232
return "%s(%r)" % (self.__class__.__name__, self.path)
235
def from_repository(cls, repository):
237
transport = getattr(repository, "_transport", None)
238
if transport is not None:
239
return cls(os.path.join(transport.local_abspath("."), "git.db"))
240
except bzrlib.errors.NotLocalUrl:
242
return cls(os.path.join(get_cache_dir(), "remote.db"))
244
def lookup_commit(self, revid):
245
row = self.db.execute("select sha1 from commits where revid = ?", (revid,)).fetchone()
250
def commit_write_group(self):
253
def add_entries(self, revid, parent_revids, commit_sha, root_tree_sha,
257
for (fileid, kind, hexsha, revision) in entries:
261
trees.append((hexsha, fileid, revid))
263
blobs.append((hexsha, fileid, revision))
267
self.db.executemany("replace into trees (sha1, fileid, revid) values (?, ?, ?)", trees)
269
self.db.executemany("replace into blobs (sha1, fileid, revid) values (?, ?, ?)", blobs)
270
self._add_entry(commit_sha, "commit", (revid, root_tree_sha))
272
def _add_entry(self, sha, type, type_data):
273
"""Add a new entry to the database.
275
assert isinstance(type_data, tuple)
278
assert isinstance(sha, str), "type was %r" % sha
280
self.db.execute("replace into commits (sha1, revid, tree_sha) values (?, ?, ?)", (sha, type_data[0], type_data[1]))
281
elif type in ("blob", "tree"):
282
self.db.execute("replace into %ss (sha1, fileid, revid) values (?, ?, ?)" % type, (sha, type_data[0], type_data[1]))
284
raise AssertionError("Unknown type %s" % type)
286
def get_inventory_sha_map(self, revid):
287
class SqliteInventorySHAMap(InventorySHAMap):
289
def __init__(self, db, revid):
293
def lookup_blob(self, fileid, revision_hint=None):
294
if revision_hint is not None:
295
revid = revision_hint
298
row = self.db.execute("select sha1 from blobs where fileid = ? and revid = ?", (fileid, revid)).fetchone()
301
raise KeyError(fileid)
303
def lookup_tree(self, fileid):
304
row = self.db.execute("select sha1 from trees where fileid = ? and revid = ?", (fileid, self.revid)).fetchone()
307
raise KeyError(fileid)
309
return SqliteInventorySHAMap(self.db, revid)
311
def lookup_git_sha(self, sha):
312
"""Lookup a Git sha in the database.
314
:param sha: Git object sha
315
:return: (type, type_data) with type_data:
316
revision: revid, tree sha
318
def format(type, row):
319
return (type, (row[0], row[1]))
320
row = self.db.execute("select revid, tree_sha from commits where sha1 = ?", (sha,)).fetchone()
322
return format("commit", row)
323
row = self.db.execute("select fileid, revid from blobs where sha1 = ?", (sha,)).fetchone()
325
return format("blob", row)
326
row = self.db.execute("select fileid, revid from trees where sha1 = ?", (sha,)).fetchone()
328
return format("tree", row)
332
"""List the revision ids known."""
333
return (row for (row,) in self.db.execute("select revid from commits"))
336
"""List the SHA1s."""
337
for table in ("blobs", "commits", "trees"):
339
for (row,) in self.db.execute("select sha1 from %s" % table):
344
TDB_HASH_SIZE = 50000
347
class TdbGitShaMap(GitShaMap):
348
"""SHA Map that uses a TDB database.
352
"git <sha1>" -> "<type> <type-data1> <type-data2>"
353
"commit revid" -> "<sha1> <tree-id>"
354
"tree fileid revid" -> "<sha1>"
355
"blob fileid revid" -> "<sha1>"
358
def __init__(self, path=None):
364
if not mapdbs().has_key(path):
365
mapdbs()[path] = tdb.Tdb(path, TDB_HASH_SIZE, tdb.DEFAULT,
366
os.O_RDWR|os.O_CREAT)
367
self.db = mapdbs()[path]
369
if int(self.db["version"]) not in (2, 3):
370
trace.warning("SHA Map is incompatible (%s -> %d), rebuilding database.",
371
self.db["version"], TDB_MAP_VERSION)
375
self.db["version"] = str(TDB_MAP_VERSION)
377
def add_entries(self, revid, parent_revids, commit_sha, root_tree_sha,
379
"""Add multiple new entries to the database.
381
self.db.transaction_start()
383
self._add_entry(commit_sha, "commit", (revid, root_tree_sha))
384
for (fileid, kind, hexsha, revision) in entries:
385
self._add_entry(hexsha, kind, (fileid, revision))
387
self.db.transaction_cancel()
390
self.db.transaction_commit()
393
return "%s(%r)" % (self.__class__.__name__, self.path)
396
def from_repository(cls, repository):
398
transport = getattr(repository, "_transport", None)
399
if transport is not None:
400
return cls(os.path.join(transport.local_abspath("."), "git.tdb"))
401
except bzrlib.errors.NotLocalUrl:
403
return cls(os.path.join(get_cache_dir(), "remote.tdb"))
405
def lookup_commit(self, revid):
406
return sha_to_hex(self.db["commit\0" + revid][:20])
408
def _add_entry(self, hexsha, type, type_data):
409
"""Add a new entry to the database.
414
sha = hex_to_sha(hexsha)
415
self.db["git\0" + sha] = "\0".join((type, type_data[0], type_data[1]))
417
self.db["commit\0" + type_data[0]] = "\0".join((sha, type_data[1]))
419
self.db["\0".join(("blob", type_data[0], type_data[1]))] = sha
421
def get_inventory_sha_map(self, revid):
423
class TdbInventorySHAMap(InventorySHAMap):
425
def __init__(self, db, revid):
429
def lookup_blob(self, fileid, revision_hint=None):
430
if revision_hint is not None:
431
revid = revision_hint
434
return sha_to_hex(self.db["\0".join(("blob", fileid, revid))])
436
return TdbInventorySHAMap(self.db, revid)
438
def lookup_git_sha(self, sha):
439
"""Lookup a Git sha in the database.
441
:param sha: Git object sha
442
:return: (type, type_data) with type_data:
443
revision: revid, tree sha
446
sha = hex_to_sha(sha)
447
data = self.db["git\0" + sha].split("\0")
448
return (data[0], (data[1], data[2]))
450
def missing_revisions(self, revids):
453
if self.db.get("commit\0" + revid) is None:
458
"""List the revision ids known."""
459
for key in self.db.iterkeys():
460
if key.startswith("commit\0"):
464
"""List the SHA1s."""
465
for key in self.db.iterkeys():
466
if key.startswith("git\0"):
467
yield sha_to_hex(key[4:])
470
def from_repository(repository):
472
return TdbGitShaMap.from_repository(repository)
474
return SqliteGitShaMap.from_repository(repository)