22
19
"""A GIT branch and repository format implementation for bzr."""
29
from bzrlib import bzrdir, errors as bzr_errors
30
from bzrlib.foreign import foreign_vcs_registry
31
from bzrlib.lockable_files import TransportLock
32
from bzrlib.transport import register_lazy_transport
33
from bzrlib.commands import plugin_cmds
34
from bzrlib.trace import warning
36
MINIMUM_DULWICH_VERSION = (0, 1, 0)
37
COMPATIBLE_BZR_VERSIONS = [(1, 11, 0), (1, 12, 0)]
39
if getattr(sys, "frozen", None):
40
# allow import additional libs from ./_lib for bzr.exe only
41
sys.path.append(os.path.normpath(os.path.join(os.path.dirname(__file__), '_lib')))
43
_versions_checked = False
44
def lazy_check_versions():
45
global _versions_checked
48
_versions_checked = True
50
from dulwich import __version__ as dulwich_version
52
raise ImportError("bzr-git: Please install dulwich, https://launchpad.net/dulwich")
54
if dulwich_version < MINIMUM_DULWICH_VERSION:
55
raise ImportError("bzr-git: Dulwich is too old; at least %d.%d.%d is required" % MINIMUM_DULWICH_VERSION)
57
bzrlib.api.require_any_api(bzrlib, COMPATIBLE_BZR_VERSIONS)
59
bzrdir.format_registry.register_lazy('git',
60
"bzrlib.plugins.git.dir", "LocalGitBzrDirFormat",
61
help='GIT repository.', native=False, experimental=True,
65
from bzrlib.revisionspec import revspec_registry
66
revspec_registry.register_lazy("git:", "bzrlib.plugins.git.revspec",
70
from bzrlib.revisionspec import SPEC_TYPES
71
from bzrlib.plugins.git.revspec import RevisionSpec_git
72
SPEC_TYPES.append(RevisionSpec_git)
74
class GitBzrDirFormat(bzrdir.BzrDirFormat):
75
_lock_class = TransportLock
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
77
101
def is_supported(self):
81
class LocalGitBzrDirFormat(GitBzrDirFormat):
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):
82
118
"""The .git directory control format."""
85
121
def _known_formats(self):
86
return set([LocalGitBzrDirFormat()])
122
return set([GitBzrDirFormat()])
88
def open(self, transport, _found=None):
124
def open(self, transport, _create=False, _found=None):
89
125
"""Open this directory.
127
:param _create: create the git dir on the fly. private to GitDirFormat.
93
129
# we dont grok readonly - git isn't integrated with transport.
94
130
url = transport.base
95
131
if url.startswith('readonly+'):
96
132
url = url[len('readonly+'):]
99
gitrepo = git.repo.Repo(transport.local_abspath("."))
100
except bzr_errors.NotLocalUrl:
101
raise bzr_errors.NotBranchError(path=transport.base)
102
from bzrlib.plugins.git.dir import LocalGitDir, GitLockableFiles, GitLock
103
lockfiles = GitLockableFiles(transport, GitLock())
104
return LocalGitDir(transport, lockfiles, gitrepo, self)
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)
107
142
def probe_transport(klass, transport):
108
143
"""Our format is present if the transport ends in '.not/'."""
109
from bzrlib.transport.local import LocalTransport
111
if not isinstance(transport, LocalTransport):
112
raise bzr_errors.NotBranchError(path=transport.base)
114
# This should quickly filter out most things that are not
115
# git repositories, saving us the trouble from loading dulwich.
116
if not transport.has(".git") and not transport.has("objects"):
117
raise bzr_errors.NotBranchError(path=transport.base)
119
import dulwich as git
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.
122
154
format.open(transport)
124
except git.errors.NotGitRepository, e:
125
raise bzr_errors.NotBranchError(path=transport.base)
126
raise bzr_errors.NotBranchError(path=transport.base)
128
def get_format_description(self):
129
return "Local Git Repository"
131
def get_format_string(self):
132
return "Local Git Repository"
134
def initialize_on_transport(self, transport):
135
from bzrlib.transport.local import LocalTransport
137
if not isinstance(transport, LocalTransport):
138
raise NotImplementedError(self.initialize,
139
"Can't create Git Repositories/branches on "
140
"non-local transports")
142
from dulwich.repo import Repo
143
Repo.create(transport.local_abspath("."))
144
return self.open(transport)
146
def is_supported(self):
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)
150
class RemoteGitBzrDirFormat(GitBzrDirFormat):
151
"""The .git directory control format."""
154
def _known_formats(self):
155
return set([RemoteGitBzrDirFormat()])
157
def open(self, transport, _found=None):
158
"""Open this directory.
161
from bzrlib.plugins.git.remote import RemoteGitDir, GitSmartTransport
162
if not isinstance(transport, GitSmartTransport):
163
raise bzr_errors.NotBranchError(transport.base)
164
# we dont grok readonly - git isn't integrated with transport.
166
if url.startswith('readonly+'):
167
url = url[len('readonly+'):]
169
from bzrlib.plugins.git.dir import GitLockableFiles, GitLock
170
lockfiles = GitLockableFiles(transport, GitLock())
171
return RemoteGitDir(transport, lockfiles, self)
174
def probe_transport(klass, transport):
175
"""Our format is present if the transport ends in '.not/'."""
176
# little ugly, but works
178
from bzrlib.plugins.git.remote import GitSmartTransport
179
if not isinstance(transport, GitSmartTransport):
180
raise bzr_errors.NotBranchError(transport.base)
181
# The only way to know a path exists and contains a valid repository
182
# is to do a request against it:
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):
184
transport.fetch_pack(lambda x: [], None, lambda x: None,
185
lambda x: mutter("git: %s" % x))
186
except errors.git_errors.GitProtocolError:
187
raise bzr_errors.NotBranchError(path=transport.base)
190
raise bzr_errors.NotBranchError(path=transport.base)
192
def get_format_description(self):
193
return "Remote Git Repository"
195
def get_format_string(self):
196
return "Remote Git Repository"
198
def initialize_on_transport(self, transport):
199
raise bzr_errors.UninitializableFormat(self)
202
bzrdir.BzrDirFormat.register_control_format(LocalGitBzrDirFormat)
203
bzrdir.BzrDirFormat.register_control_format(RemoteGitBzrDirFormat)
205
register_lazy_transport("git://", 'bzrlib.plugins.git.remote',
208
foreign_vcs_registry.register_lazy("git",
209
"bzrlib.plugins.git.mapping",
211
"Stupid content tracker")
213
plugin_cmds.register_lazy("cmd_git_serve", [], "bzrlib.plugins.git.commands")
214
plugin_cmds.register_lazy("cmd_git_import", [], "bzrlib.plugins.git.commands")
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)
216
454
def test_suite():
217
from bzrlib.plugins.git import tests
218
return tests.test_suite()
455
from unittest import TestSuite, TestLoader
460
suite.addTest(tests.test_suite())