Skip to content

Commit e9e271b

Browse files
Byroncodex
andcommitted
fix: Clone into dangling submodule metadata symlink targets on Windows
rubber stamp <!-- agent --> Removing a submodule retains its metadata alias but deletes the target. Adding the same submodule again then passes a dangling directory symlink to git clone --separate-git-dir. Git for Windows fails while copying its template files through that alias. Both native-realpath and simulated Windows 3.7 remove-leaf cases reproduced this failure locally. When the metadata destination is a leaf symlink, pass its target to Git and leave the alias intact. Resolve relative targets against the link's parent, create missing target parents through the existing clone setup, and let Git create the repository directory itself. Precreating that directory is insufficient because Git rejects an existing separate git repository destination. Read the link explicitly because Python 3.7 on Windows cannot resolve a dangling link with Path.resolve(). Normalize the Windows namespace prefix returned by newer os.readlink implementations, including UNC targets, and use forward slashes before passing the path through Git's URL logic. The shared clone helper covers add() and initialization through update(). Add regression coverage for direct cloning through absolute and relative dangling metadata links with missing target parents. Verify that the link and its stored target are retained, metadata is created at the target, and the resulting checkout works under both realpath modes. Validation: 61 focused tests passed on Windows/Python 3.10 with HIDE_WINDOWS_KNOWN_ERRORS=0, including both reported failures. Ruff 0.16.5 lint and formatting checks and git diff --check passed. Native Python 3.7 and UNC network shares were not available for execution. A broader run also exposed sharing violations in sibling-reinitialization tests before cloning; a representative case also failed with the unchanged HEAD clone helper loaded in memory. That separate removal issue is not changed here. Assisted-by: GPT 6.0 Co-authored-by: GPT 6.0 <codex@openai.com>
1 parent 958002b commit e9e271b

2 files changed

Lines changed: 31 additions & 0 deletions

File tree

git/objects/submodule/base.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,18 @@ def _clone_repo(
377377
repo.unsafe_git_clone_options,
378378
)
379379
allow_unsafe_options = True
380+
if osp.islink(module_abspath):
381+
# Clone into the target while retaining the metadata alias. Git for
382+
# Windows cannot initialize through a dangling directory symlink.
383+
# Read the link explicitly for Python 3.7, and remove the Windows
384+
# namespace prefix returned by newer Python versions for Git.
385+
target = os.readlink(module_abspath)
386+
if sys.platform == "win32":
387+
if target.startswith("\\\\?\\UNC\\"):
388+
target = "\\\\" + target[8:]
389+
elif target.startswith("\\\\?\\"):
390+
target = target[4:]
391+
module_abspath = to_native_path_linux(osp.join(osp.dirname(module_abspath), target))
380392
kwargs["separate_git_dir"] = module_abspath
381393
module_abspath_dir = osp.dirname(module_abspath)
382394
if not osp.isdir(module_abspath_dir):

test/test_submodule.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,25 @@ def test_remove_linked_metadata_keeps_siblings_and_can_reinitialize(
451451
assert Path(module.git.rev_parse("--show-toplevel")).resolve() == Path(sm.abspath).resolve()
452452

453453

454+
@pytest.mark.parametrize("relative_target", [False, True], ids=["absolute-target", "relative-target"])
455+
def test_add_to_dangling_metadata_symlink(movable_submodule, tmp_path, metadata_realpath, relative_target):
456+
sm = movable_submodule
457+
link = Path(sm.repo.git_dir) / "modules/new"
458+
target = tmp_path / "missing" / "metadata"
459+
link.symlink_to(osp.relpath(target, link.parent) if relative_target else target, target_is_directory=True)
460+
link_target = os.readlink(link)
461+
462+
added = Submodule.add(sm.repo, "new", "new", sm.url)
463+
464+
assert link.is_symlink() and link.is_dir()
465+
assert os.readlink(link) == link_target
466+
assert (target / "HEAD").is_file()
467+
with added.module() as module:
468+
assert Path(module.git_dir).resolve() == target.resolve()
469+
assert Path(module.git.rev_parse("--show-toplevel")).resolve() == Path(added.abspath).resolve()
470+
assert Path(added.abspath, "file").read_text() == "content"
471+
472+
454473
class TestRootProgress(RootUpdateProgress):
455474
"""Just prints messages, for now without checking the correctness of the states"""
456475

0 commit comments

Comments
 (0)