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‎

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 24 additions & 3 deletions
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-
returnosp.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+
returnosp.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-
withopen(osp.join(repo.git_dir, ref_path), 'rt') asfp:
136+
withopen(osp.join(repodir, ref_path), 'rt') asfp:
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
raiseValueError("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+
returncls._get_ref_info_helper(repo, repo.git_dir, ref_path)
174+
exceptValueError:
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+
returncls._get_ref_info_helper(repo, repodir, ref_path)
182+
162183
def_get_object(self):
163184
"""
164185
:return:

‎git/repo/base.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
fromgit.utilimportActor, finalize_process, decygpath, hex_to_bin
3333
importos.pathasosp
3434

35-
from .funimportrev_parse, is_git_dir, find_submodule_git_dir, touch
35+
from .funimportrev_parse, is_git_dir, find_submodule_git_dir, touch, find_worktree_git_dir
3636
importgc
3737
importgitdb
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
ifsm_gitpathisnotNone:
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+
ifsm_gitpathisNone:
148+
sm_gitpath=find_worktree_git_dir(dotgit)
149+
145150
ifsm_gitpathisnotNone:
146151
self.git_dir=_expand_path(sm_gitpath)
147152
self._working_tree_dir=curpath

‎git/repo/fun.py‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Package with general repository related functions"""
22
importos
3+
importstat
34
fromstringimportdigits
45

56
fromgit.compatimportxrange
@@ -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
deftouch(filename):
@@ -47,6 +48,25 @@ def is_git_dir(d):
4748
returnFalse
4849

4950

51+
deffind_worktree_git_dir(dotgit):
52+
"""Search for a gitdir for this worktree."""
53+
try:
54+
statbuf=os.stat(dotgit)
55+
exceptOSError:
56+
returnNone
57+
ifnotstat.S_ISREG(statbuf.st_mode):
58+
returnNone
59+
60+
try:
61+
lines=open(dotgit, 'r').readlines()
62+
forkey, valuein [line.strip().split(': ') forlineinlines]:
63+
ifkey=='gitdir':
64+
returnvalue
65+
exceptValueError:
66+
pass
67+
returnNone
68+
69+
5070
deffind_submodule_git_dir(d):
5171
"""Search for a submodule repo."""
5272
ifis_git_dir(d):

‎git/test/test_fun.py‎

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
fromioimportBytesIO
22
fromstatimportS_IFDIR, S_IFREG, S_IFLNK
3+
fromosimportstat
4+
importos.pathasosp
5+
36
try:
4-
fromunittestimportskipIf
7+
fromunittestimportskipIf, SkipTest
58
exceptImportError:
6-
fromunittest2importskipIf
9+
fromunittest2importskipIf, SkipTest
710

11+
fromgitimportGit
812
fromgit.compatimportPY3
913
fromgit.indeximportIndexFile
1014
fromgit.index.funimport (
@@ -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+
fromgit.repo.funimport (
24+
find_worktree_git_dir
1825
)
1926
fromgit.test.libimport (
27+
assert_true,
2028
TestBase,
21-
with_rw_repo
29+
with_rw_repo,
30+
with_rw_directory
2231
)
23-
fromgit.utilimportbin_to_hex
32+
fromgit.utilimportbin_to_hex, cygpath, join_path_native
2433
fromgitdb.baseimportIStream
2534
fromgitdb.typimportstr_tree_type
2635

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

266+
@with_rw_directory
267+
deftest_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+
ifgit.version_info[:3] < (2, 5, 1):
271+
raiseSkipTest("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+
ifGit.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
deftest_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‎

Lines changed: 14 additions & 7 deletions
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-
deftest_work_tree_unsupported(self, rw_dir):
915+
deftest_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
ifgit.version_info[:3] < (2, 5, 1):
917920
raiseSkipTest("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
ifGit.is_cygwin():
923926
worktree_path=cygpath(worktree_path)
924-
try:
925-
rw_master.git.worktree('add', worktree_path, 'master')
926-
exceptExceptionasex:
927-
raiseAssertionError(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
deftest_git_work_tree_env(self, rw_dir):

0 commit comments

Comments
(0)