1 # Copyright (C) 2008-2012 Ben Finney <benf@cybersource.com.au>
2 # Chris Ball <cjb@laptop.org>
3 # Gianluca Montecchi <gian@grys.it>
4 # Robert Lehmann <mail@robertlehmann.de>
5 # W. Trevor King <wking@tremily.us>
7 # This file is part of Bugs Everywhere.
9 # Bugs Everywhere is free software: you can redistribute it and/or modify it
10 # under the terms of the GNU General Public License as published by the Free
11 # Software Foundation, either version 2 of the License, or (at your option) any
14 # Bugs Everywhere is distributed in the hope that it will be useful, but
15 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
19 # You should have received a copy of the GNU General Public License along with
20 # Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
24 .. _Git: http://git-scm.com/
34 import pygit2 as _pygit2
35 except ImportError, error:
37 _pygit2_import_error = error
39 if getattr(_pygit2, '__version__', '0.17.3') == '0.17.3':
41 _pygit2_import_error = NotImplementedError(
42 'pygit2 <= 0.17.3 not supported')
45 from ...ui.util import user as _user
46 from ...util import encoding as _encoding
47 from ..base import EmptyCommit as _EmptyCommit
50 if libbe.TESTING == True:
62 class PygitGit(base.VCS):
63 """:py:class:`base.VCS` implementation for Git.
65 Using :py:mod:`pygit2` for the Git activity.
69 _null_oid = '\00' * 20
71 def __init__(self, *args, **kwargs):
72 base.VCS.__init__(self, *args, **kwargs)
74 self._pygit_repository = None
76 def __getstate__(self):
77 """`pygit2.Repository`\s don't seem to pickle well.
79 attrs = dict(self.__dict__)
80 if self._pygit_repository is not None:
81 attrs['_pygit_repository'] = self._pygit_repository.path
84 def __setstate__(self, state):
85 """`pygit2.Repository`\s don't seem to pickle well.
87 self.__dict__.update(state)
88 if self._pygit_repository is not None:
89 gitdir = self._pygit_repository
90 self._pygit_repository = _pygit2.Repository(gitdir)
92 def _vcs_version(self):
94 return getattr(_pygit2, '__verison__', '?')
97 def _vcs_get_user_id(self):
99 name = self._pygit_repository.config['user.name']
103 email = self._pygit_repository.config['user.email']
106 if name != '' or email != '': # got something!
107 # guess missing info, if necessary
109 name = _user.get_fallback_fullname()
111 email = _user.get_fallback_email()
113 raise ValueError((name, email))
114 return _user.create_user_id(name, email)
115 return None # Git has no infomation
117 def _vcs_detect(self, path):
119 _pygit2.discover_repository(path)
124 def _vcs_root(self, path):
125 """Find the root of the deepest repository containing path."""
126 # Assume that nothing funny is going on; in particular, that we aren't
127 # dealing with a bare repo.
128 gitdir = _pygit2.discover_repository(path)
129 self._pygit_repository = _pygit2.Repository(gitdir)
130 dirname,tip = os.path.split(gitdir)
131 if tip == '': # split('x/y/z/.git/') == ('x/y/z/.git', '')
132 dirname,tip = os.path.split(dirname)
133 assert tip == '.git', tip
136 def _vcs_init(self, path):
138 self._pygit_repository = _pygit2.init_repository(path, bare)
140 def _vcs_destroy(self):
141 vcs_dir = os.path.join(self.repo, '.git')
142 if os.path.exists(vcs_dir):
143 shutil.rmtree(vcs_dir)
145 def _vcs_add(self, path):
146 abspath = self._u_abspath(path)
147 if os.path.isdir(abspath):
149 self._pygit_repository.index.read()
150 self._pygit_repository.index.add(path)
151 self._pygit_repository.index.write()
153 def _vcs_remove(self, path):
154 abspath = self._u_abspath(path)
155 if not os.path.isdir(self._u_abspath(abspath)):
156 self._pygit_repository.index.read()
157 del self._pygit_repository.index[path]
158 self._pygit_repository.index.write()
159 os.remove(os.path.join(self.repo, path))
161 def _vcs_update(self, path):
164 def _git_get_commit(self, revision):
165 if isinstance(revision, str):
166 revision = unicode(revision, 'ascii')
167 commit = self._pygit_repository.revparse_single(revision)
168 assert commit.type == _pygit2.GIT_OBJ_COMMIT, commit
171 def _git_get_object(self, path, revision):
172 commit = self._git_get_commit(revision=revision)
174 sections = path.split(os.path.sep)
175 for section in sections[:-1]: # traverse trees
178 if entry.name == section:
179 eobj = entry.to_object()
180 if eobj.type == _pygit2.GIT_OBJ_TREE:
184 raise ValueError(path) # not a directory
185 if child_tree is None:
186 raise ValueError((path, sections, section, [e.name for e in tree]))
187 raise ValueError(path) # not found
191 if entry.name == sections[-1]:
192 eobj = entry.to_object()
195 def _vcs_get_file_contents(self, path, revision=None):
197 return base.VCS._vcs_get_file_contents(self, path, revision)
199 blob = self._git_get_object(path=path, revision=revision)
200 if blob.type != _pygit2.GIT_OBJ_BLOB:
201 raise ValueError(path) # not a file
202 return blob.read_raw()
204 def _vcs_path(self, id, revision):
205 return self._u_find_id(id, revision)
207 def _vcs_isdir(self, path, revision):
208 obj = self._git_get_object(path=path, revision=revision)
209 return obj.type == _pygit2.GIT_OBJ_TREE
211 def _vcs_listdir(self, path, revision):
212 tree = self._git_get_object(path=path, revision=revision)
213 assert tree.type == _pygit2.GIT_OBJ_TREE, tree
214 return [e.name for e in tree]
216 def _vcs_commit(self, commitfile, allow_empty=False):
217 self._pygit_repository.index.read()
218 tree_oid = self._pygit_repository.index.write_tree()
220 self._pygit_repository.head
221 except _pygit2.GitError: # no head; this is the first commit
223 tree = self._pygit_repository[tree_oid]
224 if not allow_empty and len(tree) == 0:
227 parents = [self._pygit_repository.head.oid]
228 if (not allow_empty and
229 tree_oid == self._pygit_repository.head.tree.oid):
232 user_id = self.get_user_id()
233 name,email = _user.parse_user_id(user_id)
234 # using default times is recent, see
235 # https://github.com/libgit2/pygit2/pull/129
236 author = _pygit2.Signature(name, email)
238 message = _encoding.get_file_contents(commitfile, decode=False)
239 encoding = _encoding.get_text_file_encoding()
240 commit_oid = self._pygit_repository.create_commit(
241 update_ref, author, committer, message, tree_oid, parents,
243 commit = self._pygit_repository[commit_oid]
246 def _vcs_revision_id(self, index):
247 walker = self._pygit_repository.walk(
248 self._pygit_repository.head.oid, _pygit2.GIT_SORT_TIME)
250 target_i = -1 - index # -1: 0, -2: 1, ...
251 for i,commit in enumerate(walker):
255 revisions = [commit.hex for commit in walker]
256 # revisions is [newest, older, ..., oldest]
257 if index > len(revisions):
259 return revisions[len(revisions) - index]
261 raise NotImplementedError('initial revision')
264 def _vcs_changed(self, revision):
265 commit = self._git_get_commit(revision=revision)
266 diff = commit.tree.diff(self._pygit_repository.head.tree)
270 for hunk in diff.changes['hunks']:
271 if hunk.old_oid == self._null_hex: # pygit2 uses hex in hunk.*_oid
272 new.add(hunk.new_file)
273 elif hunk.new_oid == self._null_hex:
274 removed.add(hunk.old_file)
276 modified.add(hunk.new_file)
277 return (list(new), list(modified), list(removed))
280 class ExecGit (PygitGit):
281 """:py:class:`base.VCS` implementation for Git.
286 def _vcs_version(self):
288 status,output,error = self._u_invoke_client('--version')
289 except CommandError: # command not found?
291 return output.strip()
293 def _vcs_get_user_id(self):
294 status,output,error = self._u_invoke_client(
295 'config', 'user.name', expect=(0,1))
297 name = output.rstrip('\n')
300 status,output,error = self._u_invoke_client(
301 'config', 'user.email', expect=(0,1))
303 email = output.rstrip('\n')
306 if name != '' or email != '': # got something!
307 # guess missing info, if necessary
309 name = _user.get_fallback_fullname()
311 email = _user.get_fallback_email()
312 return _user.create_user_id(name, email)
313 return None # Git has no infomation
315 def _vcs_detect(self, path):
316 if self._u_search_parent_directories(path, '.git') != None :
320 def _vcs_root(self, path):
321 """Find the root of the deepest repository containing path."""
322 # Assume that nothing funny is going on; in particular, that we aren't
323 # dealing with a bare repo.
324 if os.path.isdir(path) != True:
325 path = os.path.dirname(path)
326 status,output,error = self._u_invoke_client('rev-parse', '--git-dir',
328 gitdir = os.path.join(path, output.rstrip('\n'))
329 dirname = os.path.abspath(os.path.dirname(gitdir))
332 def _vcs_init(self, path):
333 self._u_invoke_client('init', cwd=path)
335 def _vcs_destroy(self):
336 vcs_dir = os.path.join(self.repo, '.git')
337 if os.path.exists(vcs_dir):
338 shutil.rmtree(vcs_dir)
340 def _vcs_add(self, path):
341 if os.path.isdir(path):
343 self._u_invoke_client('add', path)
345 def _vcs_remove(self, path):
346 if not os.path.isdir(self._u_abspath(path)):
347 self._u_invoke_client('rm', '-f', path)
349 def _vcs_update(self, path):
352 def _vcs_get_file_contents(self, path, revision=None):
354 return base.VCS._vcs_get_file_contents(self, path, revision)
356 arg = '%s:%s' % (revision,path)
357 status,output,error = self._u_invoke_client('show', arg)
360 def _vcs_path(self, id, revision):
361 return self._u_find_id(id, revision)
363 def _vcs_isdir(self, path, revision):
364 arg = '%s:%s' % (revision,path)
365 args = ['ls-tree', arg]
366 kwargs = {'expect':(0,128)}
367 status,output,error = self._u_invoke_client(*args, **kwargs)
369 if 'not a tree object' in error:
371 raise base.CommandError(args, status, stderr=error)
374 def _vcs_listdir(self, path, revision):
375 arg = '%s:%s' % (revision,path)
376 status,output,error = self._u_invoke_client(
377 'ls-tree', '--name-only', arg)
378 return output.rstrip('\n').splitlines()
380 def _vcs_commit(self, commitfile, allow_empty=False):
381 args = ['commit', '--file', commitfile]
382 if allow_empty == True:
383 args.append('--allow-empty')
384 status,output,error = self._u_invoke_client(*args)
386 kwargs = {'expect':(0,1)}
387 status,output,error = self._u_invoke_client(*args, **kwargs)
388 strings = ['nothing to commit',
389 'nothing added to commit']
390 if self._u_any_in_string(strings, output) == True:
391 raise base.EmptyCommit()
392 full_revision = self._vcs_revision_id(-1)
393 assert full_revision[:7] in output, \
394 'Mismatched revisions:\n%s\n%s' % (full_revision, output)
397 def _vcs_revision_id(self, index):
398 args = ['rev-list', '--first-parent', '--reverse', 'HEAD']
399 kwargs = {'expect':(0,128)}
400 status,output,error = self._u_invoke_client(*args, **kwargs)
402 if error.startswith("fatal: ambiguous argument 'HEAD': unknown "):
404 raise base.CommandError(args, status, stderr=error)
405 revisions = output.splitlines()
408 return revisions[index-1]
410 return revisions[index]
416 def _diff(self, revision):
417 status,output,error = self._u_invoke_client('diff', revision)
420 def _parse_diff(self, diff_text):
421 """_parse_diff(diff_text) -> (new,modified,removed)
423 `new`, `modified`, and `removed` are lists of files.
427 diff --git a/dir/changed b/dir/changed
428 index 6c3ea8c..2f2f7c7 100644
436 diff --git a/dir/deleted b/dir/deleted
437 deleted file mode 100644
438 index 225ec04..0000000
445 diff --git a/dir/moved b/dir/moved
446 deleted file mode 100644
447 index 5ef102f..0000000
455 diff --git a/dir/moved2 b/dir/moved2
457 index 0000000..5ef102f
465 diff --git a/dir/new b/dir/new
467 index 0000000..94954ab
477 lines = diff_text.splitlines()
478 for i,line in enumerate(lines):
479 if not line.startswith('diff '):
481 file_a,file_b = line.split()[-2:]
482 assert file_a.startswith('a/'), \
483 'missformed file_a %s' % file_a
484 assert file_b.startswith('b/'), \
485 'missformed file_b %s' % file_b
487 assert file_b[2:] == file, \
488 'diff file missmatch %s != %s' % (file_a, file_b)
489 if lines[i+1].startswith('new '):
491 elif lines[i+1].startswith('index '):
492 modified.append(file)
493 elif lines[i+1].startswith('deleted '):
495 return (new,modified,removed)
497 def _vcs_changed(self, revision):
498 return self._parse_diff(self._diff(revision))
501 if libbe.TESTING == True:
502 base.make_vcs_testcase_subclasses(PygitGit, sys.modules[__name__])
503 base.make_vcs_testcase_subclasses(ExecGit, sys.modules[__name__])
505 unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
506 suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])