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
self._add_entry(commit_sha, "commit", (revid, root_tree_sha))
109
for (fileid, kind, hexsha, revision) in entries:
110
self._add_entry(hexsha, kind, (fileid, revision))
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):
165
class DictInventorySHAMap(InventorySHAMap):
167
def __init__(self, base, revid):
171
def lookup_blob(self, fileid, revision_hint=None):
172
if revision_hint is not None:
173
revid = revision_hint
176
return self._base._by_fileid[revid][fileid]
178
def lookup_tree(self, fileid):
179
return self._base._by_fileid[self.revid][fileid]
181
return DictInventorySHAMap(self, revid)
183
def lookup_git_sha(self, sha):
184
return self._by_sha[sha]
187
for key, (type, type_data) in self._by_sha.iteritems():
192
return self._by_sha.iterkeys()
195
class SqliteGitShaMap(GitShaMap):
197
def __init__(self, path=None):
200
self.db = sqlite3.connect(":memory:")
202
if not mapdbs().has_key(path):
203
mapdbs()[path] = sqlite3.connect(path)
204
self.db = mapdbs()[path]
205
self.db.text_factory = str
206
self.db.executescript("""
207
create table if not exists commits(
208
sha1 text not null check(length(sha1) == 40),
210
tree_sha text not null check(length(tree_sha) == 40)
212
create index if not exists commit_sha1 on commits(sha1);
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,
219
create index if not exists blobs_sha1 on blobs(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,
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);
231
return "%s(%r)" % (self.__class__.__name__, self.path)
234
def from_repository(cls, repository):
236
transport = getattr(repository, "_transport", None)
237
if transport is not None:
238
return cls(os.path.join(transport.local_abspath("."), "git.db"))
239
except bzrlib.errors.NotLocalUrl:
241
return cls(os.path.join(get_cache_dir(), "remote.db"))
243
def lookup_commit(self, revid):
244
row = self.db.execute("select sha1 from commits where revid = ?", (revid,)).fetchone()
249
def commit_write_group(self):
252
def add_entries(self, revid, parent_revids, commit_sha, root_tree_sha,
254
self._add_entry(commit_sha, "commit", (revid, root_tree_sha))
257
for (fileid, kind, hexsha, revision) in entries:
261
trees.append((hexsha, "tree", (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)
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)
378
return "%s(%r)" % (self.__class__.__name__, self.path)
381
def from_repository(cls, repository):
383
transport = getattr(repository, "_transport", None)
384
if transport is not None:
385
return cls(os.path.join(transport.local_abspath("."), "git.tdb"))
386
except bzrlib.errors.NotLocalUrl:
388
return cls(os.path.join(get_cache_dir(), "remote.tdb"))
390
def lookup_commit(self, revid):
391
return sha_to_hex(self.db["commit\0" + revid][:20])
393
def _add_entry(self, hexsha, type, type_data):
394
"""Add a new entry to the database.
399
sha = hex_to_sha(hexsha)
400
self.db["git\0" + sha] = "\0".join((type, type_data[0], type_data[1]))
402
self.db["commit\0" + type_data[0]] = "\0".join((sha, type_data[1]))
404
self.db["\0".join(("blob", type_data[0], type_data[1]))] = sha
406
def get_inventory_sha_map(self, revid):
408
class TdbInventorySHAMap(InventorySHAMap):
410
def __init__(self, db, revid):
414
def lookup_blob(self, fileid, revision_hint=None):
415
if revision_hint is not None:
416
revid = revision_hint
419
return sha_to_hex(self.db["\0".join(("blob", fileid, revid))])
421
return TdbInventorySHAMap(self.db, revid)
423
def lookup_git_sha(self, sha):
424
"""Lookup a Git sha in the database.
426
:param sha: Git object sha
427
:return: (type, type_data) with type_data:
428
revision: revid, tree sha
431
sha = hex_to_sha(sha)
432
data = self.db["git\0" + sha].split("\0")
433
return (data[0], (data[1], data[2]))
435
def missing_revisions(self, revids):
438
if self.db.get("commit\0" + revid) is None:
443
"""List the revision ids known."""
444
for key in self.db.iterkeys():
445
if key.startswith("commit\0"):
449
"""List the SHA1s."""
450
for key in self.db.iterkeys():
451
if key.startswith("git\0"):
452
yield sha_to_hex(key[4:])
455
def from_repository(repository):
457
return TdbGitShaMap.from_repository(repository)
459
return SqliteGitShaMap.from_repository(repository)