Skip to content

Commit c2c7ee2

Browse files
committed
test: reproduce gitlink-with-no-.gitmodules abort (#1380)
A gitlink (mode 160000) outside `src` that has no matching .gitmodules entry currently aborts the whole fetch, because `checkout_version` runs `git submodule update --init --recursive` and `submodules()` before `_apply_src_and_ignore` gets a chance to drop out-of-scope submodules. Add a fast unit test exercising GitLocalRepo.checkout_version directly against a real repo, and a matching behave scenario mirroring the issue's reproduction steps. Both currently fail with the exact error from the issue: "fatal: No url found for submodule path '...' in .gitmodules". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DziAso23sxMgeTSUmzkus8
1 parent 1e9b848 commit c2c7ee2

3 files changed

Lines changed: 117 additions & 1 deletion

File tree

features/fetch-git-repo-with-submodule.feature

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,33 @@ Feature: Fetch projects with nested VCS dependencies
220220
README.md
221221
"""
222222

223+
Scenario: A gitlink with no .gitmodules entry outside src does not abort the fetch
224+
Given a git-repository "GitlinkProject.git" with "vendored" and a stray gitlink "some/worktree" outside it
225+
Given the manifest 'dfetch.yaml' in MyProject
226+
"""
227+
manifest:
228+
version: 0.0
229+
projects:
230+
- name: gitlink-project
231+
url: some-remote-server/GitlinkProject.git
232+
src: vendored
233+
"""
234+
When I run "dfetch update"
235+
Then the output shows
236+
"""
237+
Dfetch (0.14.3)
238+
gitlink-project:
239+
> Fetched master - e1fda19a57b873eb8e6ae37780594cbb77b70f1a
240+
"""
241+
Then 'MyProject' looks like:
242+
"""
243+
MyProject/
244+
dfetch.yaml
245+
gitlink-project/
246+
.dfetch_data.yaml
247+
file.txt
248+
"""
249+
223250
Scenario: A sibling submodule at the same top-level dir as src is not fetched
224251
Given a git-repository "SiblingSubmoduleProject.git" with the following submodules
225252
| path | url | revision |

features/steps/git_steps.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,37 @@ def step_impl(context, name=None):
6969
commit_all("Added submodules")
7070

7171

72+
@given(
73+
'a git-repository "{name}" with "{src_dir}" and a stray gitlink "{gitlink_path}" outside it'
74+
)
75+
def step_impl(context, name, src_dir, gitlink_path):
76+
remote_path = os.path.join(context.remotes_dir, name)
77+
pathlib.Path(remote_path).mkdir(parents=True, exist_ok=True)
78+
79+
with in_directory(remote_path):
80+
create_repo()
81+
generate_file(os.path.join(src_dir, "file.txt"), "hello")
82+
commit_all("Initial commit")
83+
84+
sha = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip()
85+
# A gitlink (mode 160000) with no corresponding .gitmodules entry, e.g. as
86+
# left behind by an accidentally committed `git worktree` or nested checkout.
87+
# `git add -A` would see the gitlink path as missing on disk and drop it
88+
# again, so commit directly instead of going through commit_all().
89+
subprocess.check_call(
90+
[
91+
"git",
92+
"update-index",
93+
"--add",
94+
"--cacheinfo",
95+
f"160000,{sha},{gitlink_path}",
96+
]
97+
)
98+
subprocess.check_call(
99+
["git", "commit", "-m", "Added a stray gitlink with no .gitmodules entry"]
100+
)
101+
102+
72103
@given('a new tag "{tagname}" is added to git-repository "{name}"')
73104
def step_impl(context, tagname, name):
74105
remote_path = os.path.join(context.remotes_dir, name)

tests/test_git_vcs.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# flake8: noqa
55

66
import os
7+
import subprocess
78
from subprocess import CompletedProcess
89
from unittest.mock import Mock, patch
910

@@ -16,7 +17,7 @@
1617
GitRemote,
1718
_build_git_ssh_command,
1819
)
19-
from dfetch.vcs.git_types import Submodule
20+
from dfetch.vcs.git_types import CheckoutOptions, Submodule
2021

2122
# ---------------------------------------------------------------------------
2223
# unique_parent_dirs (dfetch.util.util)
@@ -219,6 +220,63 @@ def test_filter_submodules_sibling_of_src_not_removed(tmp_path, monkeypatch):
219220
), "src submodule should appear in result before final os.path.exists filtering"
220221

221222

223+
# ---------------------------------------------------------------------------
224+
# GitLocalRepo.checkout_version — gitlink with no .gitmodules entry (#1380)
225+
# ---------------------------------------------------------------------------
226+
227+
228+
def _init_git_repo(path):
229+
subprocess.check_call(["git", "init", "--initial-branch=main", "--quiet"], cwd=path)
230+
subprocess.check_call(["git", "config", "user.email", "you@example.com"], cwd=path)
231+
subprocess.check_call(["git", "config", "user.name", "John Doe"], cwd=path)
232+
subprocess.check_call(["git", "config", "commit.gpgsign", "false"], cwd=path)
233+
234+
235+
def test_checkout_version_survives_gitlink_with_no_gitmodules_entry_outside_src(
236+
tmp_path,
237+
):
238+
"""Reproduces #1380.
239+
240+
An upstream repo can contain a gitlink (mode 160000) with no matching
241+
entry in .gitmodules — e.g. an accidentally committed `git worktree`.
242+
`git submodule update`/`foreach` abort on such a gitlink even when it
243+
sits entirely outside the requested `src`, so the whole fetch must not
244+
be allowed to die because of it.
245+
"""
246+
upstream = tmp_path / "upstream"
247+
upstream.mkdir()
248+
_init_git_repo(upstream)
249+
(upstream / "vendored").mkdir()
250+
(upstream / "vendored" / "file.txt").write_text("hello")
251+
subprocess.check_call(["git", "add", "-A"], cwd=upstream)
252+
subprocess.check_call(["git", "commit", "-qm", "initial"], cwd=upstream)
253+
254+
sha = (
255+
subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=upstream)
256+
.decode()
257+
.strip()
258+
)
259+
subprocess.check_call(
260+
["git", "update-index", "--add", "--cacheinfo", f"160000,{sha},some/worktree"],
261+
cwd=upstream,
262+
)
263+
subprocess.check_call(
264+
["git", "commit", "-qm", "commit a gitlink with no .gitmodules entry"],
265+
cwd=upstream,
266+
)
267+
268+
consumer = tmp_path / "consumer"
269+
consumer.mkdir()
270+
repo = GitLocalRepo(str(consumer))
271+
272+
_, submodules = repo.checkout_version(
273+
CheckoutOptions(remote=str(upstream), version="main", src="vendored")
274+
)
275+
276+
assert submodules == []
277+
assert (consumer / "file.txt").read_text() == "hello"
278+
279+
222280
@pytest.mark.parametrize(
223281
"name, cmd_result, expectation",
224282
[

0 commit comments

Comments
 (0)