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):
82
"""Retrieve a Git blob SHA by file id.
84
:param file_id: File id of the file/symlink
85
:param revision: revision in which the file was last changed.
87
raise NotImplementedError(self.lookup_blob)
89
def lookup_tree(self, file_id):
90
"""Retrieve a Git tree SHA by file id.
92
raise NotImplementedError(self.lookup_tree)
95
class GitShaMap(object):
96
"""Git<->Bzr revision id mapping database."""
98
def _add_entry(self, sha, type, type_data):
99
"""Add a new entry to the database.
101
raise NotImplementedError(self._add_entry)
103
def add_entries(self, revid, parent_revids, commit_sha, root_tree_sha,
105
"""Add multiple new entries to the database.
107
for (fileid, kind, hexsha, revision) in entries:
108
self._add_entry(hexsha, kind, (fileid, revision))
109
self._add_entry(commit_sha, "commit", (revid, root_tree_sha))
111
def get_inventory_sha_map(self, revid):
112
"""Return the inventory SHA map for a revision.
114
:param revid: Revision to fetch the map for
115
:return: A `InventorySHAMap`
117
raise NotImplementedError(self.get_inventory_sha_map)
119
def lookup_git_sha(self, sha):
120
"""Lookup a Git sha in the database.
121
:param sha: Git object sha
122
:return: (type, type_data) with type_data:
123
revision: revid, tree sha
125
raise NotImplementedError(self.lookup_git_sha)
128
"""List the revision ids known."""
129
raise NotImplementedError(self.revids)
131
def missing_revisions(self, revids):
132
"""Return set of all the revisions that are not present."""
133
present_revids = set(self.revids())
134
if not isinstance(revids, set):
136
return revids - present_revids
139
"""List the SHA1s."""
140
raise NotImplementedError(self.sha1s)
142
def start_write_group(self):
143
"""Start writing changes."""
145
def commit_write_group(self):
146
"""Commit any pending changes."""
148
def abort_write_group(self):
149
"""Abort any pending changes."""
152
class DictGitShaMap(GitShaMap):
158
def _add_entry(self, sha, type, type_data):
159
self._by_sha[sha] = (type, type_data)
160
if type in ("blob", "tree"):
161
self._by_fileid.setdefault(type_data[1], {})[type_data[0]] = sha
163
def get_inventory_sha_map(self, revid):
165
class DictInventorySHAMap(InventorySHAMap):
167
def __init__(self, base, revid):
171
def lookup_blob(self, fileid, revision):
172
return self._base._by_fileid[revision][fileid]
174
def lookup_tree(self, fileid):
175
return self._base._by_fileid[self.revid][fileid]
177
return DictInventorySHAMap(self, revid)
179
def lookup_git_sha(self, sha):
180
return self._by_sha[sha]
183
for key, (type, type_data) in self._by_sha.iteritems():
188
return self._by_sha.iterkeys()
191
class SqliteGitShaMap(GitShaMap):
193
def __init__(self, path=None):
196
self.db = sqlite3.connect(":memory:")
198
if not mapdbs().has_key(path):
199
mapdbs()[path] = sqlite3.connect(path)
200
self.db = mapdbs()[path]
201
self.db.text_factory = str
202
self.db.executescript("""
203
create table if not exists commits(
204
sha1 text not null check(length(sha1) == 40),
206
tree_sha text not null check(length(tree_sha) == 40)
208
create index if not exists commit_sha1 on commits(sha1);
209
create unique index if not exists commit_revid on commits(revid);
210
create table if not exists blobs(
211
sha1 text not null check(length(sha1) == 40),
212
fileid text not null,
215
create index if not exists blobs_sha1 on blobs(sha1);
216
create unique index if not exists blobs_fileid_revid on blobs(fileid, revid);
217
create table if not exists trees(
218
sha1 text unique not null check(length(sha1) == 40),
219
fileid text not null,
222
create unique index if not exists trees_sha1 on trees(sha1);
223
create unique index if not exists trees_fileid_revid on trees(fileid, revid);
227
return "%s(%r)" % (self.__class__.__name__, self.path)
230
def from_repository(cls, repository):
232
transport = getattr(repository, "_transport", None)
233
if transport is not None:
234
return cls(os.path.join(transport.local_abspath("."), "git.db"))
235
except bzrlib.errors.NotLocalUrl:
237
return cls(os.path.join(get_cache_dir(), "remote.db"))
239
def lookup_commit(self, revid):
240
row = self.db.execute("select sha1 from commits where revid = ?", (revid,)).fetchone()
245
def commit_write_group(self):
248
def add_entries(self, revid, parent_revids, commit_sha, root_tree_sha,
252
for (fileid, kind, hexsha, revision) in entries:
256
trees.append((hexsha, fileid, revid))
258
blobs.append((hexsha, fileid, revision))
262
self.db.executemany("replace into trees (sha1, fileid, revid) values (?, ?, ?)", trees)
264
self.db.executemany("replace into blobs (sha1, fileid, revid) values (?, ?, ?)", blobs)
265
self._add_entry(commit_sha, "commit", (revid, root_tree_sha))
267
def _add_entry(self, sha, type, type_data):
268
"""Add a new entry to the database.
270
assert isinstance(type_data, tuple)
273
assert isinstance(sha, str), "type was %r" % sha
275
self.db.execute("replace into commits (sha1, revid, tree_sha) values (?, ?, ?)", (sha, type_data[0], type_data[1]))
276
elif type in ("blob", "tree"):
277
self.db.execute("replace into %ss (sha1, fileid, revid) values (?, ?, ?)" % type, (sha, type_data[0], type_data[1]))
279
raise AssertionError("Unknown type %s" % type)
281
def get_inventory_sha_map(self, revid):
282
class SqliteInventorySHAMap(InventorySHAMap):
284
def __init__(self, db, revid):
288
def lookup_blob(self, fileid, revision):
289
row = self.db.execute("select sha1 from blobs where fileid = ? and revid = ?", (fileid, revision)).fetchone()
292
raise KeyError(fileid)
294
def lookup_tree(self, fileid):
295
row = self.db.execute("select sha1 from trees where fileid = ? and revid = ?", (fileid, self.revid)).fetchone()
298
raise KeyError(fileid)
300
return SqliteInventorySHAMap(self.db, revid)
302
def lookup_git_sha(self, sha):
303
"""Lookup a Git sha in the database.
305
:param sha: Git object sha
306
:return: (type, type_data) with type_data:
307
revision: revid, tree sha
309
def format(type, row):
310
return (type, (row[0], row[1]))
311
row = self.db.execute("select revid, tree_sha from commits where sha1 = ?", (sha,)).fetchone()
313
return format("commit", row)
314
row = self.db.execute("select fileid, revid from blobs where sha1 = ?", (sha,)).fetchone()
316
return format("blob", row)
317
row = self.db.execute("select fileid, revid from trees where sha1 = ?", (sha,)).fetchone()
319
return format("tree", row)
323
"""List the revision ids known."""
324
return (row for (row,) in self.db.execute("select revid from commits"))
327
"""List the SHA1s."""
328
for table in ("blobs", "commits", "trees"):
330
for (row,) in self.db.execute("select sha1 from %s" % table):
335
TDB_HASH_SIZE = 50000
338
class TdbGitShaMap(GitShaMap):
339
"""SHA Map that uses a TDB database.
343
"git <sha1>" -> "<type> <type-data1> <type-data2>"
344
"commit revid" -> "<sha1> <tree-id>"
345
"tree fileid revid" -> "<sha1>"
346
"blob fileid revid" -> "<sha1>"
349
def __init__(self, path=None):
355
if not mapdbs().has_key(path):
356
mapdbs()[path] = tdb.Tdb(path, TDB_HASH_SIZE, tdb.DEFAULT,
357
os.O_RDWR|os.O_CREAT)
358
self.db = mapdbs()[path]
360
if int(self.db["version"]) not in (2, 3):
361
trace.warning("SHA Map is incompatible (%s -> %d), rebuilding database.",
362
self.db["version"], TDB_MAP_VERSION)
366
self.db["version"] = str(TDB_MAP_VERSION)
368
def start_write_group(self):
369
"""Start writing changes."""
370
self.db.transaction_start()
372
def commit_write_group(self):
373
"""Commit any pending changes."""
374
self.db.transaction_commit()
376
def abort_write_group(self):
377
"""Abort any pending changes."""
378
self.db.transaction_cancel()
381
return "%s(%r)" % (self.__class__.__name__, self.path)
384
def from_repository(cls, repository):
386
transport = getattr(repository, "_transport", None)
387
if transport is not None:
388
return cls(os.path.join(transport.local_abspath("."), "git.tdb"))
389
except bzrlib.errors.NotLocalUrl:
391
return cls(os.path.join(get_cache_dir(), "remote.tdb"))
393
def lookup_commit(self, revid):
394
return sha_to_hex(self.db["commit\0" + revid][:20])
396
def _add_entry(self, hexsha, type, type_data):
397
"""Add a new entry to the database.
402
sha = hex_to_sha(hexsha)
403
self.db["git\0" + sha] = "\0".join((type, type_data[0], type_data[1]))
405
self.db["commit\0" + type_data[0]] = "\0".join((sha, type_data[1]))
407
self.db["\0".join(("blob", type_data[0], type_data[1]))] = sha
409
def get_inventory_sha_map(self, revid):
411
class TdbInventorySHAMap(InventorySHAMap):
413
def __init__(self, db, revid):
417
def lookup_blob(self, fileid, revision):
418
return sha_to_hex(self.db["\0".join(("blob", fileid, revision))])
420
return TdbInventorySHAMap(self.db, revid)
422
def lookup_git_sha(self, sha):
423
"""Lookup a Git sha in the database.
425
:param sha: Git object sha
426
:return: (type, type_data) with type_data:
427
revision: revid, tree sha
430
sha = hex_to_sha(sha)
431
data = self.db["git\0" + sha].split("\0")
432
return (data[0], (data[1], data[2]))
434
def missing_revisions(self, revids):
437
if self.db.get("commit\0" + revid) is None:
442
"""List the revision ids known."""
443
for key in self.db.iterkeys():
444
if key.startswith("commit\0"):
448
"""List the SHA1s."""
449
for key in self.db.iterkeys():
450
if key.startswith("git\0"):
451
yield sha_to_hex(key[4:])
454
def from_repository(repository):
456
return TdbGitShaMap.from_repository(repository)
458
return SqliteGitShaMap.from_repository(repository)