Skip to content
Open
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
22 changes: 22 additions & 0 deletions copier/_vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,28 @@ def _clone_via_cache(ref: str, location: str, mirror: Path) -> str:
ref,
)
with local.cwd(location):
# Worktrees share the mirror's config, so `git submodule update
# --init` from an earlier checkout may have registered
# `submodule.<name>.url` entries pointing at that checkout's
# submodule URLs, silently overriding the current `.gitmodules`
# (e.g. after a submodule moved to a new repository). Drop any
# registrations so each checkout resolves its submodules from its
# own `.gitmodules`.
for key in git(
"config",
"--file",
str(mirror / "config"),
"--get-regexp",
r"^submodule\..+\.url$",
retcode=None,
).splitlines():
git(
"config",
"--file",
str(mirror / "config"),
"--unset-all",
key.split()[0],
)
Comment on lines +340 to +361

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify and robustify this using git submodule sync:

Suggested change
# Worktrees share the mirror's config, so `git submodule update
# --init` from an earlier checkout may have registered
# `submodule.<name>.url` entries pointing at that checkout's
# submodule URLs, silently overriding the current `.gitmodules`
# (e.g. after a submodule moved to a new repository). Drop any
# registrations so each checkout resolves its submodules from its
# own `.gitmodules`.
for key in git(
"config",
"--file",
str(mirror / "config"),
"--get-regexp",
r"^submodule\..+\.url$",
retcode=None,
).splitlines():
git(
"config",
"--file",
str(mirror / "config"),
"--unset-all",
key.split()[0],
)
git("submodule", "sync", "--recursive")

WDYT?

git("submodule", "update", "--checkout", "--init", "--recursive", "--force")
return location

Expand Down
77 changes: 77 additions & 0 deletions tests/test_vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,83 @@ def test_remote_clone_recovers_from_corrupt_mirror(
assert Path(dst2, "README.md").read_text() == "hello world"


@pytest.fixture(scope="module", autouse=True)
def allow_file_submodules() -> None:
"""Allow the fixture repos below to be used as submodules.

Since Git 2.38.1 the file protocol is blocked for submodules by default
(see GHSA-3wp6-j8xr-qw85), so local submodules require this setting. Set
it via the environment so it also applies to Copier's own subprocesses.
"""
with local.env(
GIT_CONFIG_COUNT="1",
GIT_CONFIG_KEY_0="protocol.file.allow",
GIT_CONFIG_VALUE_0="always",
):
yield
Comment on lines +208 to +221

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about setting

git = git["-c", "protocol.file.allow=always"]

at the beginning of test_remote_clone_submodule_with_moved_url instead of using this autouse fixture? This would avoid enabling the file protocol for the entire module and setting Git config environment variables (which would need incrementing GIT_CONFIG_COUNT instead of overwriting it and using the new index for GIT_CONFIG_{KEY,VALUE}_<N> even)?



def test_remote_clone_submodule_with_moved_url(
tmp_path_factory: pytest.TempPathFactory,
) -> None:
"""Submodules are resolved from each checkout's own ``.gitmodules``.

A previous worktree's `git submodule update --init` registers the
submodule URL in the mirror's shared config, so a submodule that moved
between template revisions would be fetched from the stale URL, failing
when the pinned commit no longer exists there.
"""
# The submodule, initially hosted in one place...
sub1 = tmp_path_factory.mktemp("sub1")
with local.cwd(sub1):
git("init")
Path("sub.txt").write_text("v1")
git("add", "-A")
git("commit", "-m", "submodule v1")

# ...and later moved to a new location, with a new commit.
sub2 = tmp_path_factory.mktemp("sub2")
with local.cwd(sub2):
git("init")
Path("sub.txt").write_text("v2")
git("add", "-A")
git("commit", "-m", "submodule v2")

# The template's submodule points at the first location...
template = tmp_path_factory.mktemp("template")
with local.cwd(template):
git("init")
Path("README.md").write_text("template")
git("add", "-A")
git("commit", "-m", "init")
git("submodule", "add", str(sub1), "sub")
git("commit", "-m", "add submodule at its original location")
git("tag", "v1")

# ...and moves to the second location, pinning a commit that only
# exists there.
Path(".gitmodules").write_text(
Path(".gitmodules").read_text().replace(str(sub1), str(sub2))
)
with local.cwd("sub"):
git("fetch", str(sub2))
git("checkout", "FETCH_HEAD")
git("add", "-A")
git("commit", "-m", "move submodule to its new location")
git("tag", "v2")

clones = tmp_path_factory.mktemp("clones")
# Checking out the first revision registers the original submodule URL
# in the mirror's shared config.
dst1 = clone(template.as_uri(), "v1", location=str(clones / "first"))
assert Path(dst1, "sub", "sub.txt").read_text() == "v1"

# The second checkout must follow v2's `.gitmodules` instead of using
# the stale URL registered by the first checkout.
dst2 = clone(template.as_uri(), "v2", location=str(clones / "second"))
assert Path(dst2, "sub", "sub.txt").read_text() == "v2"


def test_local_dirty_clone(
tmp_path_factory: pytest.TempPathFactory, gitconfig: GitConfig
) -> None:
Expand Down