Skip to content

Commit aec58a9

Browse files
vathpelaByron
authored andcommitted
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 <[email protected]>
1 parent 4bd708d commit aec58a9

File tree

6 files changed

+105
-19
lines changed

6 files changed

+105
-19
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ Contributors are:
1818
-Bernard `Guyzmo` Pratz <[email protected]>
1919
-Timothy B. Hartman <tbhartman _at_ gmail.com>
2020
-Konstantin Popov <konstantin.popov.89 _at_ yandex.ru>
21+
-Peter Jones <pjones _at_ redhat.com>
2122

2223
Portions derived from other open source works and are clearly marked.

git/refs/symbolic.py

+24-3
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ def abspath(self):
7575

7676
@classmethod
7777
def _get_packed_refs_path(cls, repo):
78-
return osp.join(repo.git_dir, 'packed-refs')
78+
try:
79+
commondir = open(osp.join(repo.git_dir, 'commondir'), 'rt').readlines()[0].strip()
80+
except (OSError, IOError):
81+
commondir = '.'
82+
repodir = osp.join(repo.git_dir, commondir)
83+
return osp.join(repodir, 'packed-refs')
7984

8085
@classmethod
8186
def _iter_packed_refs(cls, repo):
@@ -122,13 +127,13 @@ def dereference_recursive(cls, repo, ref_path):
122127
# END recursive dereferencing
123128

124129
@classmethod
125-
def _get_ref_info(cls, repo, ref_path):
130+
def _get_ref_info_helper(cls, repo, repodir, ref_path):
126131
"""Return: (str(sha), str(target_ref_path)) if available, the sha the file at
127132
rela_path points to, or None. target_ref_path is the reference we
128133
point to, or None"""
129134
tokens = None
130135
try:
131-
with open(osp.join(repo.git_dir, ref_path), 'rt') as fp:
136+
with open(osp.join(repodir, ref_path), 'rt') as fp:
132137
value = fp.read().rstrip()
133138
# Don't only split on spaces, but on whitespace, which allows to parse lines like
134139
# 60b64ef992065e2600bfef6187a97f92398a9144 branch 'master' of git-server:/path/to/repo
@@ -159,6 +164,22 @@ def _get_ref_info(cls, repo, ref_path):
159164

160165
raise ValueError("Failed to parse reference information from %r" % ref_path)
161166

167+
@classmethod
168+
def _get_ref_info(cls, repo, ref_path):
169+
"""Return: (str(sha), str(target_ref_path)) if available, the sha the file at
170+
rela_path points to, or None. target_ref_path is the reference we
171+
point to, or None"""
172+
try:
173+
return cls._get_ref_info_helper(repo, repo.git_dir, ref_path)
174+
except ValueError:
175+
try:
176+
commondir = open(osp.join(repo.git_dir, 'commondir'), 'rt').readlines()[0].strip()
177+
except (OSError, IOError):
178+
commondir = '.'
179+
180+
repodir = osp.join(repo.git_dir, commondir)
181+
return cls._get_ref_info_helper(repo, repodir, ref_path)
182+
162183
def _get_object(self):
163184
"""
164185
:return:

git/repo/base.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from git.util import Actor, finalize_process, decygpath, hex_to_bin
3333
import os.path as osp
3434

35-
from .fun import rev_parse, is_git_dir, find_submodule_git_dir, touch
35+
from .fun import rev_parse, is_git_dir, find_submodule_git_dir, touch, find_worktree_git_dir
3636
import gc
3737
import gitdb
3838

@@ -138,10 +138,15 @@ def __init__(self, path=None, odbt=DefaultDBType, search_parent_directories=Fals
138138
self._working_tree_dir = os.getenv('GIT_WORK_TREE', os.path.dirname(self.git_dir))
139139
break
140140

141-
sm_gitpath = find_submodule_git_dir(osp.join(curpath, '.git'))
141+
dotgit = osp.join(curpath, '.git')
142+
sm_gitpath = find_submodule_git_dir(dotgit)
142143
if sm_gitpath is not None:
143144
self.git_dir = osp.normpath(sm_gitpath)
144-
sm_gitpath = find_submodule_git_dir(osp.join(curpath, '.git'))
145+
146+
sm_gitpath = find_submodule_git_dir(dotgit)
147+
if sm_gitpath is None:
148+
sm_gitpath = find_worktree_git_dir(dotgit)
149+
145150
if sm_gitpath is not None:
146151
self.git_dir = _expand_path(sm_gitpath)
147152
self._working_tree_dir = curpath

git/repo/fun.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Package with general repository related functions"""
22
import os
3+
import stat
34
from string import digits
45

56
from git.compat import xrange
@@ -17,7 +18,7 @@
1718

1819

1920
__all__ = ('rev_parse', 'is_git_dir', 'touch', 'find_submodule_git_dir', 'name_to_object', 'short_to_long', 'deref_tag',
20-
'to_commit')
21+
'to_commit', 'find_worktree_git_dir')
2122

2223

2324
def touch(filename):
@@ -47,6 +48,25 @@ def is_git_dir(d):
4748
return False
4849

4950

51+
def find_worktree_git_dir(dotgit):
52+
"""Search for a gitdir for this worktree."""
53+
try:
54+
statbuf = os.stat(dotgit)
55+
except OSError:
56+
return None
57+
if not stat.S_ISREG(statbuf.st_mode):
58+
return None
59+
60+
try:
61+
lines = open(dotgit, 'r').readlines()
62+
for key, value in [line.strip().split(': ') for line in lines]:
63+
if key == 'gitdir':
64+
return value
65+
except ValueError:
66+
pass
67+
return None
68+
69+
5070
def find_submodule_git_dir(d):
5171
"""Search for a submodule repo."""
5272
if is_git_dir(d):

git/test/test_fun.py

+37-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
from io import BytesIO
22
from stat import S_IFDIR, S_IFREG, S_IFLNK
3+
from os import stat
4+
import os.path as osp
5+
36
try:
4-
from unittest import skipIf
7+
from unittest import skipIf, SkipTest
58
except ImportError:
6-
from unittest2 import skipIf
9+
from unittest2 import skipIf, SkipTest
710

11+
from git import Git
812
from git.compat import PY3
913
from git.index import IndexFile
1014
from git.index.fun import (
@@ -14,13 +18,18 @@
1418
traverse_tree_recursive,
1519
traverse_trees_recursive,
1620
tree_to_stream,
17-
tree_entries_from_data
21+
tree_entries_from_data,
22+
)
23+
from git.repo.fun import (
24+
find_worktree_git_dir
1825
)
1926
from git.test.lib import (
27+
assert_true,
2028
TestBase,
21-
with_rw_repo
29+
with_rw_repo,
30+
with_rw_directory
2231
)
23-
from git.util import bin_to_hex
32+
from git.util import bin_to_hex, cygpath, join_path_native
2433
from gitdb.base import IStream
2534
from gitdb.typ import str_tree_type
2635

@@ -254,6 +263,29 @@ def test_tree_traversal_single(self):
254263
assert entries
255264
# END for each commit
256265

266+
@with_rw_directory
267+
def test_linked_worktree_traversal(self, rw_dir):
268+
"""Check that we can identify a linked worktree based on a .git file"""
269+
git = Git(rw_dir)
270+
if git.version_info[:3] < (2, 5, 1):
271+
raise SkipTest("worktree feature unsupported")
272+
273+
rw_master = self.rorepo.clone(join_path_native(rw_dir, 'master_repo'))
274+
branch = rw_master.create_head('aaaaaaaa')
275+
worktree_path = join_path_native(rw_dir, 'worktree_repo')
276+
if Git.is_cygwin():
277+
worktree_path = cygpath(worktree_path)
278+
rw_master.git.worktree('add', worktree_path, branch.name)
279+
280+
dotgit = osp.join(worktree_path, ".git")
281+
statbuf = stat(dotgit)
282+
assert_true(statbuf.st_mode & S_IFREG)
283+
284+
gitdir = find_worktree_git_dir(dotgit)
285+
self.assertIsNotNone(gitdir)
286+
statbuf = stat(gitdir)
287+
assert_true(statbuf.st_mode & S_IFDIR)
288+
257289
@skipIf(PY3, 'odd types returned ... maybe figure it out one day')
258290
def test_tree_entries_from_data_with_failing_name_decode_py2(self):
259291
r = tree_entries_from_data(b'100644 \x9f\0aaa')

git/test/test_repo.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
NoSuchPathError,
2323
Head,
2424
Commit,
25+
Object,
2526
Tree,
2627
IndexFile,
2728
Git,
@@ -911,22 +912,28 @@ def test_is_ancestor(self):
911912
self.assertRaises(GitCommandError, repo.is_ancestor, i, j)
912913

913914
@with_rw_directory
914-
def test_work_tree_unsupported(self, rw_dir):
915+
def test_git_work_tree_dotgit(self, rw_dir):
916+
"""Check that we find .git as a worktree file and find the worktree
917+
based on it."""
915918
git = Git(rw_dir)
916919
if git.version_info[:3] < (2, 5, 1):
917920
raise SkipTest("worktree feature unsupported")
918921

919922
rw_master = self.rorepo.clone(join_path_native(rw_dir, 'master_repo'))
920-
rw_master.git.checkout('HEAD~10')
923+
branch = rw_master.create_head('aaaaaaaa')
921924
worktree_path = join_path_native(rw_dir, 'worktree_repo')
922925
if Git.is_cygwin():
923926
worktree_path = cygpath(worktree_path)
924-
try:
925-
rw_master.git.worktree('add', worktree_path, 'master')
926-
except Exception as ex:
927-
raise AssertionError(ex, "It's ok if TC not running from `master`.")
927+
rw_master.git.worktree('add', worktree_path, branch.name)
928+
929+
# this ensures that we can read the repo's gitdir correctly
930+
repo = Repo(worktree_path)
931+
self.assertIsInstance(repo, Repo)
928932

929-
self.failUnlessRaises(InvalidGitRepositoryError, Repo, worktree_path)
933+
# this ensures we're able to actually read the refs in the tree, which
934+
# means we can read commondir correctly.
935+
commit = repo.head.commit
936+
self.assertIsInstance(commit, Object)
930937

931938
@with_rw_directory
932939
def test_git_work_tree_env(self, rw_dir):

0 commit comments

Comments
 (0)