Skip to content

Repo: handle worktrees better #638

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ Contributors are:
-Bernard `Guyzmo` Pratz <[email protected]>
-Timothy B. Hartman <tbhartman _at_ gmail.com>
-Konstantin Popov <konstantin.popov.89 _at_ yandex.ru>
-Peter Jones <pjones _at_ redhat.com>

Portions derived from other open source works and are clearly marked.
5 changes: 5 additions & 0 deletions doc/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
Changelog
=========

2.1.6 - Bugfixes
====================================

* support for worktrees

2.1.3 - Bugfixes
====================================

Expand Down
27 changes: 24 additions & 3 deletions git/refs/symbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
11 changes: 8 additions & 3 deletions git/repo/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
22 changes: 21 additions & 1 deletion git/repo/fun.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Package with general repository related functions"""
import os
import stat
from string import digits

from git.compat import xrange
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
42 changes: 37 additions & 5 deletions git/test/test_fun.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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

Expand Down Expand Up @@ -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')
Expand Down
21 changes: 14 additions & 7 deletions git/test/test_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
NoSuchPathError,
Head,
Commit,
Object,
Tree,
IndexFile,
Git,
Expand Down Expand Up @@ -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):
Expand Down
8 changes: 6 additions & 2 deletions git/test/test_submodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,12 +663,13 @@ 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(),
"""FIXME: ile "C:\\projects\\gitpython\\git\\cmd.py", line 671, in execute
@skipIf(HIDE_WINDOWS_KNOWN_ERRORS,
"""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):
Expand Down Expand Up @@ -698,6 +699,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', '[email protected]')
writer.set_value('user.name', 'me')
smm = sm.module()
fp = osp.join(smm.working_tree_dir, 'empty-file')
with open(fp, 'w'):
Expand Down