From 44809694cf978360172866920c7b62de4038757c Mon Sep 17 00:00:00 2001 From: Caleb Tuttle <1calebtuttle@gmail.com> Date: Wed, 8 Apr 2026 10:23:40 -0400 Subject: [PATCH] fix: resolve relative gitdir paths for nested submodules Nested submodules (e.g., libs/some-submodule) failed during environment creation because readSubmoduleGitdirPath returned relative gitdir paths verbatim. After exportEnvironment wipes the worktree and recreates root .git as a pointer file, these relative paths no longer resolve on the filesystem. Resolve relative paths to absolute in readSubmoduleGitdirPath, matching the approach already used for the root .git pointer in exportEnvironment. Co-Authored-By: Claude Opus 4.6 (1M context) --- environment/integration/repository_test.go | 30 ++++++++++ repository/git.go | 13 ++++- repository/git_test.go | 65 ++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/environment/integration/repository_test.go b/environment/integration/repository_test.go index 9e0ede64..1d161a7c 100644 --- a/environment/integration/repository_test.go +++ b/environment/integration/repository_test.go @@ -297,6 +297,36 @@ func TestRepositoryWithSubmodule(t *testing.T) { }) } +func TestRepositoryWithNestedSubmodule(t *testing.T) { + t.Parallel() + WithRepository(t, "repository-with-nested-submodule", SetupEmptyRepo, func(t *testing.T, repo *repository.Repository, user *UserActions) { + ctx := t.Context() + + // Add a submodule at a nested path (not at the repo root) + user.GitCommand("submodule", "add", "https://github.com/dagger/dagger-test-modules.git", "libs/nested-submodule") + user.GitCommand("submodule", "update", "--init") + + user.GitCommand("commit", "-am", "add nested submodule") + + env := user.CreateEnvironment("Test Nested Submodule", "Testing repository with nested submodule") + + // Add a file to the base repo (triggers exportEnvironment + commitWorktreeChanges) + user.FileWrite(env.ID, "test.txt", "initial content\n", "Initial commit") + + assert.NoError(t, repo.Update(ctx, env, "write the env back to the repo")) + + // Check that the nested submodule content is readable inside the container + readmeContent, err := env.FileRead(ctx, "libs/nested-submodule/README.md", true, 0, 0) + require.NoError(t, err, "Should be able to read libs/nested-submodule/README.md from inside container") + assert.Contains(t, readmeContent, "Test fixtures used by dagger integration tests.") + + // Verify that the git working tree remains clean + gitStatus, err := repository.RunGitCommand(ctx, repo.SourcePath(), "status", "--porcelain") + require.NoError(t, err, "Should be able to check git status") + assert.Empty(t, strings.TrimSpace(gitStatus), "Git working tree should remain clean") + }) +} + func TestRepositoryWithRecursiveSubmodule(t *testing.T) { t.Parallel() WithRepository(t, "repository-with-submodule", SetupEmptyRepo, func(t *testing.T, repo *repository.Repository, user *UserActions) { diff --git a/repository/git.go b/repository/git.go index 55dacfbb..7a00ae52 100644 --- a/repository/git.go +++ b/repository/git.go @@ -300,7 +300,18 @@ func readSubmoduleGitdirPath(worktreePath, submodulePath string) (string, error) return "", fmt.Errorf("invalid .git file format in submodule %s: %s", submoduleGitPath, gitContentStr) } - return gitContentStr, nil + gitdirValue := strings.TrimPrefix(gitContentStr, "gitdir: ") + + // Resolve relative paths to absolute, matching the approach used for + // the root .git pointer in exportEnvironment. Relative paths like + // "../../.git/modules/libs/foo" break after the worktree is wiped and + // the root .git is recreated as a pointer file (not a directory). + if !filepath.IsAbs(gitdirValue) { + submoduleDir := filepath.Join(worktreePath, submodulePath) + gitdirValue = filepath.Clean(filepath.Join(submoduleDir, gitdirValue)) + } + + return fmt.Sprintf("gitdir: %s", gitdirValue), nil } // addSubmoduleGitdirFiles adds .git files for all submodules to the provided directory diff --git a/repository/git_test.go b/repository/git_test.go index 702f0874..7fef8790 100644 --- a/repository/git_test.go +++ b/repository/git_test.go @@ -172,6 +172,71 @@ func TestCommitWorktreeChanges(t *testing.T) { }) } +func TestReadSubmoduleGitdirPath(t *testing.T) { + t.Run("root_level_submodule_relative_path", func(t *testing.T) { + dir := t.TempDir() + // Simulate a root-level submodule at "sub" with a relative gitdir pointer + subPath := filepath.Join(dir, "sub") + require.NoError(t, os.MkdirAll(subPath, 0755)) + // Relative path from sub/.git: ../.git/modules/sub -> resolves to dir/.git/modules/sub + require.NoError(t, os.WriteFile(filepath.Join(subPath, ".git"), []byte("gitdir: ../.git/modules/sub"), 0644)) + + result, err := readSubmoduleGitdirPath(dir, "sub") + require.NoError(t, err) + + expected := filepath.Clean(filepath.Join(dir, ".git/modules/sub")) + assert.Equal(t, "gitdir: "+expected, result) + }) + + t.Run("nested_submodule_relative_path", func(t *testing.T) { + dir := t.TempDir() + // Simulate a nested submodule at "libs/nested-sub" with a relative gitdir pointer + subPath := filepath.Join(dir, "libs", "nested-sub") + require.NoError(t, os.MkdirAll(subPath, 0755)) + // Relative path from libs/nested-sub/.git: ../../.git/modules/libs/nested-sub + require.NoError(t, os.WriteFile(filepath.Join(subPath, ".git"), []byte("gitdir: ../../.git/modules/libs/nested-sub"), 0644)) + + result, err := readSubmoduleGitdirPath(dir, "libs/nested-sub") + require.NoError(t, err) + + expected := filepath.Clean(filepath.Join(dir, ".git/modules/libs/nested-sub")) + assert.Equal(t, "gitdir: "+expected, result) + }) + + t.Run("already_absolute_path", func(t *testing.T) { + dir := t.TempDir() + subPath := filepath.Join(dir, "sub") + require.NoError(t, os.MkdirAll(subPath, 0755)) + absGitdir := "/abs/path/modules/sub" + require.NoError(t, os.WriteFile(filepath.Join(subPath, ".git"), []byte("gitdir: "+absGitdir), 0644)) + + result, err := readSubmoduleGitdirPath(dir, "sub") + require.NoError(t, err) + assert.Equal(t, "gitdir: "+absGitdir, result) + }) + + t.Run("malformed_git_file", func(t *testing.T) { + dir := t.TempDir() + subPath := filepath.Join(dir, "sub") + require.NoError(t, os.MkdirAll(subPath, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(subPath, ".git"), []byte("not-a-gitdir-file"), 0644)) + + _, err := readSubmoduleGitdirPath(dir, "sub") + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid .git file format") + }) + + t.Run("missing_git_file", func(t *testing.T) { + dir := t.TempDir() + subPath := filepath.Join(dir, "sub") + require.NoError(t, os.MkdirAll(subPath, 0755)) + + _, err := readSubmoduleGitdirPath(dir, "sub") + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to read submodule .git file") + }) +} + // Test helper functions func writeFile(t *testing.T, dir, name, content string) { t.Helper()