22
19
"""A GIT branch and repository format implementation for bzr."""
30
# versions ending in 'exp' mean experimental mappings
31
# versions ending in 'dev' mean development version
32
# versions ending in 'final' mean release (well tested, etc)
33
version_info = (0, 3, 3, 'dev', 0)
35
if version_info[3] == 'final':
36
version_string = '%d.%d.%d' % version_info[:3]
38
version_string = '%d.%d.%d%s%d' % version_info
39
__version__ = version_string
41
MINIMUM_DULWICH_VERSION = (0, 3, 1)
42
COMPATIBLE_BZR_VERSIONS = [(1, 14, 0), (1, 15, 0)]
44
bzrlib.api.require_any_api(bzrlib, COMPATIBLE_BZR_VERSIONS)
52
from bzrlib.foreign import (
55
from bzrlib.lockable_files import (
58
from bzrlib.transport import (
59
register_lazy_transport,
60
register_transport_proto,
62
from bzrlib.commands import (
65
from bzrlib.trace import (
68
from bzrlib.version_info_formats.format_rio import (
69
RioVersionInfoBuilder,
73
if getattr(sys, "frozen", None):
74
# allow import additional libs from ./_lib for bzr.exe only
75
sys.path.append(os.path.normpath(os.path.join(os.path.dirname(__file__), '_lib')))
77
_versions_checked = False
78
def lazy_check_versions():
79
global _versions_checked
82
_versions_checked = True
84
from dulwich import __version__ as dulwich_version
86
raise ImportError("bzr-git: Please install dulwich, https://launchpad.net/dulwich")
88
if dulwich_version < MINIMUM_DULWICH_VERSION:
89
raise ImportError("bzr-git: Dulwich is too old; at least %d.%d.%d is required" % MINIMUM_DULWICH_VERSION)
91
bzrdir.format_registry.register_lazy('git',
92
"bzrlib.plugins.git.dir", "LocalGitBzrDirFormat",
93
help='GIT repository.', native=False, experimental=True,
96
from bzrlib.revisionspec import revspec_registry
97
revspec_registry.register_lazy("git:", "bzrlib.plugins.git.revspec",
101
class GitBzrDirFormat(bzrdir.BzrDirFormat):
102
_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
104
101
def is_supported(self):
108
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):
109
118
"""The .git directory control format."""
112
121
def _known_formats(self):
113
return set([LocalGitBzrDirFormat()])
122
return set([GitBzrDirFormat()])
115
def open(self, transport, _found=None):
124
def open(self, transport, _create=False, _found=None):
116
125
"""Open this directory.
127
:param _create: create the git dir on the fly. private to GitDirFormat.
119
import dulwich as git
120
129
# we dont grok readonly - git isn't integrated with transport.
121
130
url = transport.base
122
131
if url.startswith('readonly+'):
123
132
url = url[len('readonly+'):]
126
gitrepo = git.repo.Repo(transport.local_abspath(".").encode(osutils._fs_enc))
127
except bzr_errors.NotLocalUrl:
128
raise bzr_errors.NotBranchError(path=transport.base)
129
from bzrlib.plugins.git.dir import LocalGitDir, GitLockableFiles, GitLock
130
lockfiles = GitLockableFiles(transport, GitLock())
131
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)
134
142
def probe_transport(klass, transport):
135
143
"""Our format is present if the transport ends in '.not/'."""
136
from bzrlib.transport.local import LocalTransport
138
if not isinstance(transport, LocalTransport):
139
raise bzr_errors.NotBranchError(path=transport.base)
141
# This should quickly filter out most things that are not
142
# git repositories, saving us the trouble from loading dulwich.
143
if not transport.has(".git") and not transport.has("objects"):
144
raise bzr_errors.NotBranchError(path=transport.base)
146
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.
149
154
format.open(transport)
151
except git.errors.NotGitRepository, e:
152
raise bzr_errors.NotBranchError(path=transport.base)
153
raise bzr_errors.NotBranchError(path=transport.base)
155
def get_format_description(self):
156
return "Local Git Repository"
158
def get_format_string(self):
159
return "Local Git Repository"
161
def initialize_on_transport(self, transport):
162
from bzrlib.transport.local import LocalTransport
164
if not isinstance(transport, LocalTransport):
165
raise NotImplementedError(self.initialize,
166
"Can't create Git Repositories/branches on "
167
"non-local transports")
169
from dulwich.repo import Repo
170
Repo.create(transport.local_abspath(".").encode(osutils._fs_enc))
171
return self.open(transport)
173
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)
177
class RemoteGitBzrDirFormat(GitBzrDirFormat):
178
"""The .git directory control format."""
181
def _known_formats(self):
182
return set([RemoteGitBzrDirFormat()])
184
def open(self, transport, _found=None):
185
"""Open this directory.
188
# we dont grok readonly - git isn't integrated with transport.
190
if url.startswith('readonly+'):
191
url = url[len('readonly+'):]
192
if (not url.startswith("git://") and
193
not url.startswith("git+")):
194
raise bzr_errors.NotBranchError(transport.base)
195
from bzrlib.plugins.git.remote import RemoteGitDir, GitSmartTransport
196
if not isinstance(transport, GitSmartTransport):
197
raise bzr_errors.NotBranchError(transport.base)
198
from bzrlib.plugins.git.dir import GitLockableFiles, GitLock
199
lockfiles = GitLockableFiles(transport, GitLock())
200
return RemoteGitDir(transport, lockfiles, self)
203
def probe_transport(klass, transport):
204
"""Our format is present if the transport ends in '.not/'."""
206
if url.startswith('readonly+'):
207
url = url[len('readonly+'):]
208
if (not url.startswith("git://") and
209
not url.startswith("git+")):
210
raise bzr_errors.NotBranchError(transport.base)
211
# little ugly, but works
213
from bzrlib.plugins.git.remote import GitSmartTransport
214
if not isinstance(transport, GitSmartTransport):
215
raise bzr_errors.NotBranchError(transport.base)
218
def get_format_description(self):
219
return "Remote Git Repository"
221
def get_format_string(self):
222
return "Remote Git Repository"
224
def initialize_on_transport(self, transport):
225
raise bzr_errors.UninitializableFormat(self)
228
bzrdir.BzrDirFormat.register_control_format(LocalGitBzrDirFormat)
229
bzrdir.BzrDirFormat.register_control_format(RemoteGitBzrDirFormat)
231
register_transport_proto('git://',
232
help="Access using the Git smart server protocol.")
233
register_transport_proto('git+ssh://',
234
help="Access using the Git smart server protocol over SSH.")
236
register_lazy_transport("git://", 'bzrlib.plugins.git.remote',
237
'TCPGitSmartTransport')
238
register_lazy_transport("git+ssh://", 'bzrlib.plugins.git.remote',
239
'SSHGitSmartTransport')
241
foreign_vcs_registry.register_lazy("git",
242
"bzrlib.plugins.git.mapping", "foreign_git", "Stupid content tracker")
244
plugin_cmds.register_lazy("cmd_git_import", [], "bzrlib.plugins.git.commands")
245
plugin_cmds.register_lazy("cmd_git_object", ["git-objects", "git-cat"],
246
"bzrlib.plugins.git.commands")
248
def update_stanza(rev, stanza):
249
mapping = getattr(rev, "mapping", None)
250
if mapping is not None and mapping.revid_prefix.startswith("git-"):
251
stanza.add("git-commit", rev.foreign_revid)
254
rio_hooks = getattr(RioVersionInfoBuilder, "hooks", None)
255
if rio_hooks is not None:
256
rio_hooks.install_named_hook('revision', update_stanza, None)
260
from bzrlib.transport import transport_server_registry
264
transport_server_registry.register_lazy('git',
265
'bzrlib.plugins.git.server',
267
'Git Smart server protocol over TCP. (default port: 9418)')
270
def get_rich_root_format(stacked=False):
272
return bzrdir.format_registry.make_bzrdir("1.9-rich-root")
274
return bzrdir.format_registry.make_bzrdir("default-rich-root")
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)
276
454
def test_suite():
277
from bzrlib.plugins.git import tests
278
return tests.test_suite()
455
from unittest import TestSuite, TestLoader
460
suite.addTest(tests.test_suite())