From 176696be5c22af1dfe09a7afe8a13a6dd0fe9fd0 Mon Sep 17 00:00:00 2001 From: Peter Jones Date: Mon, 26 Jun 2017 14:54:28 -0400 Subject: [PATCH 1/4] Repo: handle worktrees better This makes Repo("foo") work when foo/.git is a file of the form created by "git worktree add", i.e. it's a text file that says: gitdir: /home/me/project/.git/worktrees/bar and where /home/me/project/.git/ is the nominal gitdir, but /home/me/project/.git/worktrees/bar has this worktree's HEAD etc and a "gitdir" file that contains the path of foo/.git . Signed-off-by: Peter Jones --- AUTHORS | 1 + git/refs/symbolic.py | 27 ++++++++++++++++++++++++--- git/repo/base.py | 11 ++++++++--- git/repo/fun.py | 22 +++++++++++++++++++++- git/test/test_fun.py | 42 +++++++++++++++++++++++++++++++++++++----- git/test/test_repo.py | 21 ++++++++++++++------- 6 files changed, 105 insertions(+), 19 deletions(-) diff --git a/AUTHORS b/AUTHORS index 781695ba9..ad7c452c0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -18,5 +18,6 @@ Contributors are: -Bernard `Guyzmo` Pratz -Timothy B. Hartman -Konstantin Popov +-Peter Jones Portions derived from other open source works and are clearly marked. diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 22b7c53e9..90ecb62c6 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -75,7 +75,12 @@ def abspath(self): @classmethod def _get_packed_refs_path(cls, repo): - return osp.join(repo.git_dir, 'packed-refs') + try: + commondir = open(osp.join(repo.git_dir, 'commondir'), 'rt').readlines()[0].strip() + except (OSError, IOError): + commondir = '.' + repodir = osp.join(repo.git_dir, commondir) + return osp.join(repodir, 'packed-refs') @classmethod def _iter_packed_refs(cls, repo): @@ -122,13 +127,13 @@ def dereference_recursive(cls, repo, ref_path): # END recursive dereferencing @classmethod - def _get_ref_info(cls, repo, ref_path): + def _get_ref_info_helper(cls, repo, repodir, ref_path): """Return: (str(sha), str(target_ref_path)) if available, the sha the file at rela_path points to, or None. target_ref_path is the reference we point to, or None""" tokens = None try: - with open(osp.join(repo.git_dir, ref_path), 'rt') as fp: + with open(osp.join(repodir, ref_path), 'rt') as fp: value = fp.read().rstrip() # Don't only split on spaces, but on whitespace, which allows to parse lines like # 60b64ef992065e2600bfef6187a97f92398a9144 branch 'master' of git-server:/path/to/repo @@ -159,6 +164,22 @@ def _get_ref_info(cls, repo, ref_path): raise ValueError("Failed to parse reference information from %r" % ref_path) + @classmethod + def _get_ref_info(cls, repo, ref_path): + """Return: (str(sha), str(target_ref_path)) if available, the sha the file at + rela_path points to, or None. target_ref_path is the reference we + point to, or None""" + try: + return cls._get_ref_info_helper(repo, repo.git_dir, ref_path) + except ValueError: + try: + commondir = open(osp.join(repo.git_dir, 'commondir'), 'rt').readlines()[0].strip() + except (OSError, IOError): + commondir = '.' + + repodir = osp.join(repo.git_dir, commondir) + return cls._get_ref_info_helper(repo, repodir, ref_path) + def _get_object(self): """ :return: diff --git a/git/repo/base.py b/git/repo/base.py index 2f67a3411..28bb2a5d7 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -32,7 +32,7 @@ from git.util import Actor, finalize_process, decygpath, hex_to_bin import os.path as osp -from .fun import rev_parse, is_git_dir, find_submodule_git_dir, touch +from .fun import rev_parse, is_git_dir, find_submodule_git_dir, touch, find_worktree_git_dir import gc import gitdb @@ -138,10 +138,15 @@ def __init__(self, path=None, odbt=DefaultDBType, search_parent_directories=Fals self._working_tree_dir = os.getenv('GIT_WORK_TREE', os.path.dirname(self.git_dir)) break - sm_gitpath = find_submodule_git_dir(osp.join(curpath, '.git')) + dotgit = osp.join(curpath, '.git') + sm_gitpath = find_submodule_git_dir(dotgit) if sm_gitpath is not None: self.git_dir = osp.normpath(sm_gitpath) - sm_gitpath = find_submodule_git_dir(osp.join(curpath, '.git')) + + sm_gitpath = find_submodule_git_dir(dotgit) + if sm_gitpath is None: + sm_gitpath = find_worktree_git_dir(dotgit) + if sm_gitpath is not None: self.git_dir = _expand_path(sm_gitpath) self._working_tree_dir = curpath diff --git a/git/repo/fun.py b/git/repo/fun.py index 39e55880f..6aefd9d66 100644 --- a/git/repo/fun.py +++ b/git/repo/fun.py @@ -1,5 +1,6 @@ """Package with general repository related functions""" import os +import stat from string import digits from git.compat import xrange @@ -17,7 +18,7 @@ __all__ = ('rev_parse', 'is_git_dir', 'touch', 'find_submodule_git_dir', 'name_to_object', 'short_to_long', 'deref_tag', - 'to_commit') + 'to_commit', 'find_worktree_git_dir') def touch(filename): @@ -47,6 +48,25 @@ def is_git_dir(d): return False +def find_worktree_git_dir(dotgit): + """Search for a gitdir for this worktree.""" + try: + statbuf = os.stat(dotgit) + except OSError: + return None + if not stat.S_ISREG(statbuf.st_mode): + return None + + try: + lines = open(dotgit, 'r').readlines() + for key, value in [line.strip().split(': ') for line in lines]: + if key == 'gitdir': + return value + except ValueError: + pass + return None + + def find_submodule_git_dir(d): """Search for a submodule repo.""" if is_git_dir(d): diff --git a/git/test/test_fun.py b/git/test/test_fun.py index b472fe19c..5e32a1f9d 100644 --- a/git/test/test_fun.py +++ b/git/test/test_fun.py @@ -1,10 +1,14 @@ from io import BytesIO from stat import S_IFDIR, S_IFREG, S_IFLNK +from os import stat +import os.path as osp + try: - from unittest import skipIf + from unittest import skipIf, SkipTest except ImportError: - from unittest2 import skipIf + from unittest2 import skipIf, SkipTest +from git import Git from git.compat import PY3 from git.index import IndexFile from git.index.fun import ( @@ -14,13 +18,18 @@ traverse_tree_recursive, traverse_trees_recursive, tree_to_stream, - tree_entries_from_data + tree_entries_from_data, +) +from git.repo.fun import ( + find_worktree_git_dir ) from git.test.lib import ( + assert_true, TestBase, - with_rw_repo + with_rw_repo, + with_rw_directory ) -from git.util import bin_to_hex +from git.util import bin_to_hex, cygpath, join_path_native from gitdb.base import IStream from gitdb.typ import str_tree_type @@ -254,6 +263,29 @@ def test_tree_traversal_single(self): assert entries # END for each commit + @with_rw_directory + def test_linked_worktree_traversal(self, rw_dir): + """Check that we can identify a linked worktree based on a .git file""" + git = Git(rw_dir) + if git.version_info[:3] < (2, 5, 1): + raise SkipTest("worktree feature unsupported") + + rw_master = self.rorepo.clone(join_path_native(rw_dir, 'master_repo')) + branch = rw_master.create_head('aaaaaaaa') + worktree_path = join_path_native(rw_dir, 'worktree_repo') + if Git.is_cygwin(): + worktree_path = cygpath(worktree_path) + rw_master.git.worktree('add', worktree_path, branch.name) + + dotgit = osp.join(worktree_path, ".git") + statbuf = stat(dotgit) + assert_true(statbuf.st_mode & S_IFREG) + + gitdir = find_worktree_git_dir(dotgit) + self.assertIsNotNone(gitdir) + statbuf = stat(gitdir) + assert_true(statbuf.st_mode & S_IFDIR) + @skipIf(PY3, 'odd types returned ... maybe figure it out one day') def test_tree_entries_from_data_with_failing_name_decode_py2(self): r = tree_entries_from_data(b'100644 \x9f\0aaa') diff --git a/git/test/test_repo.py b/git/test/test_repo.py index 86019b73a..a6be4e66e 100644 --- a/git/test/test_repo.py +++ b/git/test/test_repo.py @@ -22,6 +22,7 @@ NoSuchPathError, Head, Commit, + Object, Tree, IndexFile, Git, @@ -911,22 +912,28 @@ def test_is_ancestor(self): self.assertRaises(GitCommandError, repo.is_ancestor, i, j) @with_rw_directory - def test_work_tree_unsupported(self, rw_dir): + def test_git_work_tree_dotgit(self, rw_dir): + """Check that we find .git as a worktree file and find the worktree + based on it.""" git = Git(rw_dir) if git.version_info[:3] < (2, 5, 1): raise SkipTest("worktree feature unsupported") rw_master = self.rorepo.clone(join_path_native(rw_dir, 'master_repo')) - rw_master.git.checkout('HEAD~10') + branch = rw_master.create_head('aaaaaaaa') worktree_path = join_path_native(rw_dir, 'worktree_repo') if Git.is_cygwin(): worktree_path = cygpath(worktree_path) - try: - rw_master.git.worktree('add', worktree_path, 'master') - except Exception as ex: - raise AssertionError(ex, "It's ok if TC not running from `master`.") + rw_master.git.worktree('add', worktree_path, branch.name) + + # this ensures that we can read the repo's gitdir correctly + repo = Repo(worktree_path) + self.assertIsInstance(repo, Repo) - self.failUnlessRaises(InvalidGitRepositoryError, Repo, worktree_path) + # this ensures we're able to actually read the refs in the tree, which + # means we can read commondir correctly. + commit = repo.head.commit + self.assertIsInstance(commit, Object) @with_rw_directory def test_git_work_tree_env(self, rw_dir): From 9667b7afeeb9b571f11456b219d9db9efcef19e6 Mon Sep 17 00:00:00 2001 From: Peter Jones Date: Wed, 28 Jun 2017 10:27:58 -0400 Subject: [PATCH 2/4] Maybe work around AppVeyor setting a bad email? One of the submodule tests says: Traceback (most recent call last): File "C:\projects\gitpython\git\test\lib\helper.py", line 92, in wrapper return func(self, path) File "C:\projects\gitpython\git\test\test_submodule.py", line 706, in test_git_submodules_and_add_sm_with_new_commit smm.git.commit(m="new file added") File "C:\projects\gitpython\git\cmd.py", line 425, in return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) File "C:\projects\gitpython\git\cmd.py", line 877, in _call_process return self.execute(call, **exec_kwargs) File "C:\projects\gitpython\git\cmd.py", line 688, in execute raise GitCommandError(command, status, stderr_value, stdout_value) git.exc.GitCommandError: Cmd('git') failed due to: exit code(128) cmdline: git commit -m new file added stderr: ' *** Please tell me who you are. Run git config --global user.email "you@example.com" git config --global user.name "Your Name" to set your account's default identity. Omit --global to set the identity only in this repository. fatal: unable to auto-detect email address (got 'appveyor@APPVYR-WIN.(none)')' Clearly this is failing because (none) isn't a valid TLD, but I figure I'll try to set a fake value and see if that works around it. --- git/test/test_submodule.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/git/test/test_submodule.py b/git/test/test_submodule.py index 9e79a72ca..2da7071ff 100644 --- a/git/test/test_submodule.py +++ b/git/test/test_submodule.py @@ -698,6 +698,9 @@ def test_git_submodules_and_add_sm_with_new_commit(self, rwdir): parent.index.commit("moved submodules") + with sm.config_writer() as writer: + writer.set_value('user.email', 'example@example.com') + writer.set_value('user.name', 'me') smm = sm.module() fp = osp.join(smm.working_tree_dir, 'empty-file') with open(fp, 'w'): From 0e9b99bf3e25092a313e2f71ab348ca774c92087 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 1 Jul 2017 13:49:49 +0200 Subject: [PATCH 3/4] Try to ignore test on windows as it fails for the wrong reasons Here is the error log we see: ====================================================================== ERROR: test_git_submodules_and_add_sm_with_new_commit (git.test.test_submodule.TestSubmodule) ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\projects\gitpython\git\test\lib\helper.py", line 92, in wrapper return func(self, path) File "C:\projects\gitpython\git\test\test_submodule.py", line 709, in test_git_submodules_and_add_sm_with_new_commit smm.git.commit(m="new file added") File "C:\projects\gitpython\git\cmd.py", line 425, in return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) File "C:\projects\gitpython\git\cmd.py", line 877, in _call_process return self.execute(call, **exec_kwargs) File "C:\projects\gitpython\git\cmd.py", line 688, in execute raise GitCommandError(command, status, stderr_value, stdout_value) GitCommandError: Cmd('git') failed due to: exit code(128) cmdline: git commit -m new file added stderr: ' *** Please tell me who you are. --- git/test/test_submodule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/test/test_submodule.py b/git/test/test_submodule.py index 2da7071ff..d14bf5c76 100644 --- a/git/test/test_submodule.py +++ b/git/test/test_submodule.py @@ -663,7 +663,7 @@ def test_add_empty_repo(self, rwdir): url=empty_repo_dir, no_checkout=checkout_mode and True or False) # end for each checkout mode - @skipIf(HIDE_WINDOWS_KNOWN_ERRORS and Git.is_cygwin(), + @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, """FIXME: ile "C:\\projects\\gitpython\\git\\cmd.py", line 671, in execute raise GitCommandError(command, status, stderr_value, stdout_value) GitCommandError: Cmd('git') failed due to: exit code(128) From afe947b808c06f2a5adc03d160c2376947e1a1ef Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 1 Jul 2017 13:55:13 +0200 Subject: [PATCH 4/4] Update changelog and improve docs on skipped test [skip ci] --- doc/source/changes.rst | 5 +++++ git/test/test_submodule.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 4ef40f628..4aedf9365 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,11 @@ Changelog ========= +2.1.6 - Bugfixes +==================================== + +* support for worktrees + 2.1.3 - Bugfixes ==================================== diff --git a/git/test/test_submodule.py b/git/test/test_submodule.py index d14bf5c76..e667ae177 100644 --- a/git/test/test_submodule.py +++ b/git/test/test_submodule.py @@ -664,11 +664,12 @@ def test_add_empty_repo(self, rwdir): # end for each checkout mode @skipIf(HIDE_WINDOWS_KNOWN_ERRORS, - """FIXME: ile "C:\\projects\\gitpython\\git\\cmd.py", line 671, in execute + """FIXME on cygwin: File "C:\\projects\\gitpython\\git\\cmd.py", line 671, in execute raise GitCommandError(command, status, stderr_value, stdout_value) GitCommandError: Cmd('git') failed due to: exit code(128) cmdline: git add 1__Xava verbXXten 1_test _myfile 1_test_other_file 1_XXava-----verbXXten stderr: 'fatal: pathspec '"1__çava verböten"' did not match any files' + FIXME on appveyor: see https://ci.appveyor.com/project/Byron/gitpython/build/1.0.185 """) @with_rw_directory def test_git_submodules_and_add_sm_with_new_commit(self, rwdir):