22
19
"""A GIT branch and repository format implementation for bzr."""
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.
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.
274
129
# we dont grok readonly - git isn't integrated with transport.
275
130
url = transport.base
276
131
if url.startswith('readonly+'):
277
132
url = url[len('readonly+'):]
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)
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)
288
142
def probe_transport(klass, transport):
289
143
"""Our format is present if the transport ends in '.not/'."""
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')
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)
379
454
def test_suite():
380
from bzrlib.plugins.git import tests
381
return tests.test_suite()
455
from unittest import TestSuite, TestLoader
460
suite.addTest(tests.test_suite())