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()