19
19
"""A GIT branch and repository format implementation for bzr."""
25
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "dulwich"))
27
from bzrlib import bzrdir
28
from bzrlib.foreign import ForeignVcs, VcsMappingRegistry, foreign_vcs_registry
29
from bzrlib.plugins.git.dir import LocalGitBzrDirFormat, RemoteGitBzrDirFormat
30
from bzrlib.transport import register_lazy_transport
32
bzrdir.format_registry.register(
33
'git', LocalGitBzrDirFormat,
34
help='GIT repository.',
35
native=False, experimental=True,
38
bzrdir.BzrDirFormat.register_control_format(LocalGitBzrDirFormat)
39
bzrdir.BzrDirFormat.register_control_format(RemoteGitBzrDirFormat)
41
register_lazy_transport("git://", 'bzrlib.plugins.git.remote',
45
class ForeignGit(ForeignVcs):
49
git_mapping_registry = VcsMappingRegistry()
50
git_mapping_registry.register_lazy('git-experimental', "bzrlib.plugins.git.mapping",
51
"BzrGitMappingExperimental")
52
foreign_vcs_registry.register("git", ForeignGit(git_mapping_registry),
53
"Stupid content tracker")
22
from StringIO import StringIO
25
import stgit.git as git
27
from bzrlib import config, iterablefile, graph, osutils, urlutils
28
from bzrlib.decorators import *
31
import bzrlib.errors as errors
32
import bzrlib.repository
33
from bzrlib.revision import Revision
36
class GitBranchConfig(config.BranchConfig):
37
"""BranchConfig that uses locations.conf in place of branch.conf"""
39
def __init__(self, branch):
40
config.BranchConfig.__init__(self, branch)
41
# do not provide a BranchDataConfig
42
self.option_sources = self.option_sources[0], self.option_sources[2]
44
def set_user_option(self, name, value, local=False):
45
"""Force local to True"""
46
config.BranchConfig.set_user_option(self, name, value, local=True)
49
def gitrevid_from_bzr(revision_id):
50
if revision_id is None:
52
return revision_id[4:]
55
def bzrrevid_from_git(revision_id):
56
return "git:" + revision_id
59
class GitLock(object):
60
"""A lock that thunks through to Git."""
72
class GitLockableFiles(bzrlib.lockable_files.LockableFiles):
73
"""Git specific lockable files abstraction."""
75
def __init__(self, lock):
77
self._transaction = None
78
self._lock_mode = None
82
class GitDir(bzrlib.bzrdir.BzrDir):
83
"""An adapter to the '.git' dir used by git."""
85
def __init__(self, transport, lockfiles, format):
87
self.root_transport = transport
88
self.transport = transport.clone('.git')
89
self._lockfiles = lockfiles
91
def get_branch_transport(self, branch_format):
92
if branch_format is None:
94
if isinstance(branch_format, GitBzrDirFormat):
96
raise errors.IncompatibleFormat(branch_format, self._format)
98
get_repository_transport = get_branch_transport
99
get_workingtree_transport = get_branch_transport
101
def is_supported(self):
104
def open_branch(self, ignored=None):
105
"""'crate' a branch for this dir."""
106
return GitBranch(self, self._lockfiles)
108
def open_repository(self, shared=False):
109
"""'open' a repository for this dir."""
110
return GitRepository(self._gitrepo, self, self._lockfiles)
112
def open_workingtree(self):
113
loc = urlutils.unescape_for_display(self.root_transport.base, 'ascii')
114
raise errors.NoWorkingTree(loc)
117
class GitBzrDirFormat(bzrlib.bzrdir.BzrDirFormat):
118
"""The .git directory control format."""
121
def _known_formats(self):
122
return set([GitBzrDirFormat()])
124
def open(self, transport, _create=False, _found=None):
125
"""Open this directory.
127
:param _create: create the git dir on the fly. private to GitDirFormat.
129
# we dont grok readonly - git isn't integrated with transport.
131
if url.startswith('readonly+'):
132
url = url[len('readonly+'):]
133
if url.startswith('file://'):
134
url = url[len('file://'):]
135
url = url.encode('utf8')
136
if not transport.has('.git'):
137
raise errors.NotBranchError(path=transport.base)
138
lockfiles = GitLockableFiles(GitLock())
139
return GitDir(transport, lockfiles, self)
142
def probe_transport(klass, transport):
143
"""Our format is present if the transport ends in '.not/'."""
144
# little ugly, but works
146
# try a manual probe first, its a little faster perhaps ?
147
if transport.has('.git'):
149
# delegate to the main opening code. This pays a double rtt cost at the
150
# moment, so perhaps we want probe_transport to return the opened thing
151
# rather than an openener ? or we could return a curried thing with the
152
# dir to open already instantiated ? Needs more thought.
154
format.open(transport)
157
raise errors.NotBranchError(path=transport.base)
158
raise errors.NotBranchError(path=transport.base)
161
bzrlib.bzrdir.BzrDirFormat.register_control_format(GitBzrDirFormat)
164
class GitBranch(bzrlib.branch.Branch):
165
"""An adapter to git repositories for bzr Branch objects."""
167
def __init__(self, gitdir, lockfiles):
169
self.control_files = lockfiles
170
self.repository = GitRepository(gitdir, lockfiles)
171
self.base = gitdir.root_transport.base
172
if '.git' not in gitdir.root_transport.list_dir('.'):
173
raise errors.NotBranchError(self.base)
175
def lock_write(self):
176
self.control_files.lock_write()
179
def last_revision(self):
180
# perhaps should escape this ?
181
return bzrrevid_from_git(self.repository.git.get_head())
184
def revision_history(self):
185
node = self.last_revision()
186
ancestors = self.repository.get_revision_graph(node)
188
while node is not None:
190
if len(ancestors[node]) > 0:
191
node = ancestors[node][0]
194
return list(reversed(history))
196
def get_config(self):
197
return GitBranchConfig(self)
200
self.control_files.lock_read()
203
self.control_files.unlock()
205
def get_push_location(self):
206
"""See Branch.get_push_location."""
207
push_loc = self.get_config().get_user_option('push_location')
210
def set_push_location(self, location):
211
"""See Branch.set_push_location."""
212
self.get_config().set_user_option('push_location', location,
216
class GitRepository(bzrlib.repository.Repository):
217
"""An adapter to git repositories for bzr."""
219
def __init__(self, gitdir, lockfiles):
221
self.control_files = lockfiles
222
gitdirectory = urlutils.local_path_from_url(gitdir.transport.base)
223
self.git = GitModel(gitdirectory)
224
self._revision_cache = {}
226
def _ancestor_revisions(self, revision_ids):
227
if revision_ids is not None:
228
git_revisions = [gitrevid_from_bzr(r) for r in revision_ids]
231
for lines in self.git.ancestor_lines(git_revisions):
232
yield self.parse_rev(lines)
237
def get_revision_graph(self, revision_id=None):
238
if revision_id is None:
241
revisions = [revision_id]
242
return self.get_revision_graph_with_ghosts(revisions).get_ancestors()
244
def get_revision_graph_with_ghosts(self, revision_ids=None):
245
result = graph.Graph()
246
for revision in self._ancestor_revisions(revision_ids):
247
result.add_node(revision.revision_id, revision.parent_ids)
248
self._revision_cache[revision.revision_id] = revision
251
def get_revision(self, revision_id):
252
if revision_id in self._revision_cache:
253
return self._revision_cache[revision_id]
254
raw = self.git.rev_list([gitrevid_from_bzr(revision_id)], max_count=1,
256
return self.parse_rev(raw)
258
def has_revision(self, revision_id):
260
self.get_revision(revision_id)
261
except NoSuchRevision:
266
def get_revisions(self, revisions):
267
return [self.get_revision(r) for r in revisions]
269
def parse_rev(self, raw):
270
# first field is the rev itself.
271
# then its 'field value'
277
revision_id = bzrrevid_from_git(raw[0][:-1])
278
for field in raw[1:]:
279
#if field.startswith('author '):
280
# committer = field[7:]
281
if field.startswith('parent '):
282
parents.append(bzrrevid_from_git(field.split()[1]))
283
elif field.startswith('committer '):
284
commit_fields = field.split()
285
if committer is None:
286
committer = ' '.join(commit_fields[1:-3])
287
timestamp = commit_fields[-2]
288
timezone = commit_fields[-1]
289
elif field.startswith('tree '):
290
tree_id = field.split()[1]
292
log.append(field[4:])
297
result = Revision(revision_id)
298
result.parent_ids = parents
300
result.inventory_sha1 = ""
301
result.timezone = timezone and int(timezone)
302
result.timestamp = float(timestamp)
303
result.committer = committer
304
result.properties['git-tree-id'] = tree_id
307
def revision_tree(self, revision_id):
308
return GitRevisionTree(self, revision_id)
310
def get_inventory(self, revision_id):
311
revision = self.get_revision(revision_id)
312
inventory = GitInventory(revision_id)
313
tree_id = revision.properties['git-tree-id']
314
type_map = {'blob': 'file', 'tree': 'directory' }
315
def get_inventory(tree_id, prefix):
316
for perms, type, obj_id, name in self.git.get_inventory(tree_id):
317
full_path = prefix + name
322
executable = (perms[-3] in ('1', '3', '5', '7'))
323
entry = GitEntry(full_path, type_map[type], revision_id,
324
text_sha1, executable)
325
inventory.entries[full_path] = entry
327
get_inventory(obj_id, full_path+'/')
328
get_inventory(tree_id, '')
332
class GitRevisionTree(object):
334
def __init__(self, repository, revision_id):
335
self.repository = repository
336
self.revision_id = revision_id
337
self.inventory = repository.get_inventory(revision_id)
339
def get_file(self, file_id):
340
obj_id = self.inventory[file_id].text_sha1
341
lines = self.repository.git.cat_file('blob', obj_id)
342
return iterablefile.IterableFile(lines)
344
def is_executable(self, file_id):
345
return self.inventory[file_id].executable
348
class GitInventory(object):
350
def __init__(self, revision_id):
352
self.root = GitEntry('', 'directory', revision_id)
353
self.entries[''] = self.root
355
def __getitem__(self, key):
356
return self.entries[key]
358
def iter_entries(self):
359
return iter(sorted(self.entries.items()))
361
def iter_entries_by_dir(self):
362
return self.iter_entries()
365
return len(self.entries)
368
class GitEntry(object):
370
def __init__(self, path, kind, revision, text_sha1=None, executable=False,
375
self.executable = executable
376
self.name = osutils.basename(path)
378
self.parent_id = None
380
self.parent_id = osutils.dirname(path)
381
self.revision = revision
382
self.symlink_target = None
383
self.text_sha1 = text_sha1
384
self.text_size = None
387
return "GitEntry(%r, %r, %r, %r)" % (self.path, self.kind,
388
self.revision, self.parent_id)
391
class GitModel(object):
392
"""API that follows GIT model closely"""
394
def __init__(self, git_dir):
395
self.git_dir = git_dir
397
def git_command(self, command, args):
398
args = ' '.join("'%s'" % arg for arg in args)
399
return 'git --git-dir=%s %s %s' % (self.git_dir, command, args)
401
def git_lines(self, command, args):
402
return stgit.git._output_lines(self.git_command(command, args))
404
def git_line(self, command, args):
405
return stgit.git._output_one_line(self.git_command(command, args))
407
def cat_file(self, type, object_id, pretty=False):
413
args.append(object_id)
414
return self.git_lines('cat-file', args)
416
def rev_list(self, heads, max_count=None, header=False):
418
if max_count is not None:
419
args.append('--max-count=%d' % max_count)
420
if header is not False:
421
args.append('--header')
426
return self.git_lines('rev-list', args)
428
def rev_parse(self, git_id):
429
args = ['--verify', git_id]
430
return self.git_line('rev-parse', args)
433
return self.rev_parse('HEAD')
435
def ancestor_lines(self, revisions):
437
for line in self.rev_list(revisions, header=True):
438
if line.startswith('\x00'):
440
revision_lines = [line[1:].decode('latin-1')]
442
revision_lines.append(line.decode('latin-1'))
443
assert revision_lines == ['']
445
def get_inventory(self, tree_id):
446
for line in self.cat_file('tree', tree_id, True):
447
sections = line.split(' ', 2)
448
obj_id, name = sections[2].split('\t', 1)
449
name = name.rstrip('\n')
450
if name.startswith('"'):
451
name = name[1:-1].decode('string_escape').decode('utf-8')
452
yield (sections[0], sections[1], obj_id, name)
57
from bzrlib.plugins.git import tests
58
return tests.test_suite()
455
from unittest import TestSuite, TestLoader
460
suite.addTest(tests.test_suite())