1
# Copyright (C) 2006 Canonical Ltd
2
# Authors: Robert Collins <robert.collins@canonical.com>
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
"""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.
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)
455
from unittest import TestSuite, TestLoader
460
suite.addTest(tests.test_suite())