19
22
"""A GIT branch and repository format implementation for bzr."""
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.
31
bzr_compatible_versions,
32
bzr_plugin_version as version_info,
33
dulwich_minimum_version,
36
if version_info[3] == 'final':
37
version_string = '%d.%d.%d' % version_info[:3]
39
version_string = '%d.%d.%d%s%d' % version_info
40
__version__ = version_string
42
bzrlib.api.require_any_api(bzrlib, bzr_compatible_versions)
50
from bzrlib.controldir import (
58
from bzrlib.bzrdir import (
64
ControlDirFormat = BzrDirFormat
66
has_controldir = False
70
from bzrlib.foreign import (
73
from bzrlib.help_topics import (
76
from bzrlib.lockable_files import (
79
from bzrlib.transport import (
80
register_lazy_transport,
81
register_transport_proto,
83
from bzrlib.commands import (
86
from bzrlib.version_info_formats.format_rio import (
87
RioVersionInfoBuilder,
89
from bzrlib.send import (
90
format_registry as send_format_registry,
94
if getattr(sys, "frozen", None):
95
# allow import additional libs from ./_lib for bzr.exe only
96
sys.path.append(os.path.normpath(
97
os.path.join(os.path.dirname(__file__), '_lib')))
100
def import_dulwich():
102
from dulwich import __version__ as dulwich_version
104
raise bzr_errors.DependencyNotPresent("dulwich",
105
"bzr-git: Please install dulwich, https://launchpad.net/dulwich")
107
if dulwich_version < dulwich_minimum_version:
108
raise bzr_errors.DependencyNotPresent("dulwich",
109
"bzr-git: Dulwich is too old; at least %d.%d.%d is required" %
110
dulwich_minimum_version)
113
_versions_checked = False
114
def lazy_check_versions():
115
global _versions_checked
116
if _versions_checked:
119
_versions_checked = True
121
format_registry.register_lazy('git',
122
"bzrlib.plugins.git.dir", "LocalGitControlDirFormat",
123
help='GIT repository.', native=False, experimental=False,
126
format_registry.register_lazy('git-bare',
127
"bzrlib.plugins.git.dir", "BareLocalGitControlDirFormat",
128
help='Bare GIT repository (no working tree).', native=False,
132
from bzrlib.revisionspec import revspec_registry
133
revspec_registry.register_lazy("git:", "bzrlib.plugins.git.revspec",
137
from bzrlib.revisionspec import dwim_revspecs
141
from bzrlib.plugins.git.revspec import RevisionSpec_git
142
dwim_revspecs.append(RevisionSpec_git)
145
class GitControlDirFormat(ControlDirFormat):
147
_lock_class = TransportLock
149
colocated_branches = True
151
def __eq__(self, other):
152
return type(self) == type(other)
154
def is_supported(self):
157
def network_name(self):
161
class LocalGitProber(Prober):
163
def probe_transport(self, transport):
165
if not transport.has_any(['info/refs', '.git/branches',
167
raise bzr_errors.NotBranchError(path=transport.base)
168
except bzr_errors.NoSuchFile:
169
raise bzr_errors.NotBranchError(path=transport.base)
170
from bzrlib import urlutils
171
if urlutils.split(transport.base)[1] == ".git":
172
raise bzr_errors.NotBranchError(path=transport.base)
173
lazy_check_versions()
175
from bzrlib.plugins.git.transportgit import TransportRepo
177
gitrepo = TransportRepo(transport)
178
except dulwich.errors.NotGitRepository, e:
179
raise bzr_errors.NotBranchError(path=transport.base)
182
return BareLocalGitControlDirFormat()
184
return LocalGitControlDirFormat()
187
class LocalGitControlDirFormat(GitControlDirFormat):
188
"""The .git directory control format."""
193
def _known_formats(self):
194
return set([LocalGitControlDirFormat()])
196
def open(self, transport, _found=None):
197
"""Open this directory.
200
lazy_check_versions()
201
from bzrlib.plugins.git.transportgit import TransportRepo
202
gitrepo = TransportRepo(transport)
203
from bzrlib.plugins.git.dir import LocalGitDir, GitLockableFiles, GitLock
204
lockfiles = GitLockableFiles(transport, GitLock())
205
return LocalGitDir(transport, lockfiles, gitrepo, self)
208
def probe_transport(klass, transport):
209
prober = LocalGitProber()
210
return prober.probe_transport(transport)
212
def get_format_description(self):
213
return "Local Git Repository"
215
def initialize_on_transport(self, transport):
216
from bzrlib.transport.local import LocalTransport
218
if not isinstance(transport, LocalTransport):
219
raise NotImplementedError(self.initialize,
220
"Can't create Git Repositories/branches on "
221
"non-local transports")
222
lazy_check_versions()
223
from dulwich.repo import Repo
224
Repo.init(transport.local_abspath(".").encode(osutils._fs_enc),
226
return self.open(transport)
228
def is_supported(self):
232
class BareLocalGitControlDirFormat(LocalGitControlDirFormat):
235
supports_workingtrees = False
238
def _known_formats(self):
239
return set([RemoteGitControlDirFormat()])
241
def get_format_description(self):
242
return "Local Git Repository (bare)"
245
class RemoteGitProber(Prober):
247
def probe_transport(self, transport):
249
if url.startswith('readonly+'):
250
url = url[len('readonly+'):]
251
if (not url.startswith("git://") and not url.startswith("git+")):
252
raise bzr_errors.NotBranchError(transport.base)
253
# little ugly, but works
254
from bzrlib.plugins.git.remote import GitSmartTransport
255
if not isinstance(transport, GitSmartTransport):
256
raise bzr_errors.NotBranchError(transport.base)
257
return RemoteGitControlDirFormat()
261
class RemoteGitControlDirFormat(GitControlDirFormat):
262
"""The .git directory control format."""
264
supports_workingtrees = False
267
def _known_formats(self):
268
return set([RemoteGitControlDirFormat()])
270
def open(self, transport, _found=None):
271
"""Open this directory.
129
274
# we dont grok readonly - git isn't integrated with transport.
130
275
url = transport.base
131
276
if url.startswith('readonly+'):
132
277
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)
278
if (not url.startswith("git://") and not url.startswith("git+")):
279
raise bzr_errors.NotBranchError(transport.base)
280
from bzrlib.plugins.git.remote import RemoteGitDir, GitSmartTransport
281
if not isinstance(transport, GitSmartTransport):
282
raise bzr_errors.NotBranchError(transport.base)
283
from bzrlib.plugins.git.dir import GitLockableFiles, GitLock
284
lockfiles = GitLockableFiles(transport, GitLock())
285
return RemoteGitDir(transport, lockfiles, self)
142
288
def probe_transport(klass, transport):
143
289
"""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)
290
prober = RemoteGitProber()
291
return prober.probe_transport(transport)
293
def get_format_description(self):
294
return "Remote Git Repository"
296
def initialize_on_transport(self, transport):
297
raise bzr_errors.UninitializableFormat(self)
301
ControlDirFormat.register_format(LocalGitControlDirFormat())
302
ControlDirFormat.register_format(BareLocalGitControlDirFormat())
303
ControlDirFormat.register_format(RemoteGitControlDirFormat())
304
ControlDirFormat.register_prober(LocalGitProber)
305
ControlDirFormat.register_prober(RemoteGitProber)
307
ControlDirFormat.register_control_format(LocalGitControlDirFormat)
308
ControlDirFormat.register_control_format(BareLocalGitControlDirFormat)
309
ControlDirFormat.register_control_format(RemoteGitControlDirFormat)
311
register_transport_proto('git://',
312
help="Access using the Git smart server protocol.")
313
register_transport_proto('git+ssh://',
314
help="Access using the Git smart server protocol over SSH.")
316
register_lazy_transport("git://", 'bzrlib.plugins.git.remote',
317
'TCPGitSmartTransport')
318
register_lazy_transport("git+ssh://", 'bzrlib.plugins.git.remote',
319
'SSHGitSmartTransport')
321
foreign_vcs_registry.register_lazy("git",
322
"bzrlib.plugins.git.mapping", "foreign_git", "Stupid content tracker")
324
plugin_cmds.register_lazy("cmd_git_import", [], "bzrlib.plugins.git.commands")
325
plugin_cmds.register_lazy("cmd_git_object", ["git-objects", "git-cat"],
326
"bzrlib.plugins.git.commands")
327
plugin_cmds.register_lazy("cmd_git_refs", [], "bzrlib.plugins.git.commands")
328
plugin_cmds.register_lazy("cmd_git_apply", [], "bzrlib.plugins.git.commands")
330
def update_stanza(rev, stanza):
331
mapping = getattr(rev, "mapping", None)
332
if mapping is not None and mapping.revid_prefix.startswith("git-"):
333
stanza.add("git-commit", rev.foreign_revid)
336
rio_hooks = getattr(RioVersionInfoBuilder, "hooks", None)
337
if rio_hooks is not None:
338
rio_hooks.install_named_hook('revision', update_stanza, None)
341
from bzrlib.transport import transport_server_registry
342
transport_server_registry.register_lazy('git',
343
'bzrlib.plugins.git.server',
345
'Git Smart server protocol over TCP. (default port: 9418)')
348
from bzrlib.repository import (
349
network_format_registry as repository_network_format_registry,
351
repository_network_format_registry.register_lazy('git',
352
'bzrlib.plugins.git.repository', 'GitRepositoryFormat')
355
from bzrlib.controldir import (
356
network_format_registry as controldir_network_format_registry,
359
from bzrlib.bzrdir import (
360
network_format_registry as controldir_network_format_registry,
362
controldir_network_format_registry.register('git', GitControlDirFormat)
364
send_format_registry.register_lazy('git', 'bzrlib.plugins.git.send',
365
'send_git', 'Git am-style diff format')
367
topic_registry.register_lazy('git',
368
'bzrlib.plugins.git.help',
369
'help_git', 'Using Bazaar with Git')
372
from bzrlib.diff import format_registry as diff_format_registry
376
diff_format_registry.register_lazy('git', 'bzrlib.plugins.git.send',
377
'GitDiffTree', 'Git am-style diff format')
454
379
def test_suite():
455
from unittest import TestSuite, TestLoader
460
suite.addTest(tests.test_suite())
380
from bzrlib.plugins.git import tests
381
return tests.test_suite()