diff --git a/code/source_manifest.go b/code/source_manifest.go index ac69c5eb..031877b1 100644 --- a/code/source_manifest.go +++ b/code/source_manifest.go @@ -237,7 +237,14 @@ func (s *DefaultCodeServer) revisionSourceManifest(ctx context.Context, revision return nil, err } resolved = strings.TrimSpace(resolved) - output, err := s.runGit(ctx, "ls-tree", "-rlz", "--full-tree", resolved) + // ARCHITECTURE: DefaultCodeServer is a SourceDir-rooted capability. Git + // normally reports tree paths relative to the current directory, which is + // exactly the same namespace used by the worktree manifest and every file + // operation. --full-tree would silently widen a server rooted at a nested + // code unit to the enclosing repository and make immutable/worktree + // manifests incomparable. The explicit pathspec also prevents Git from + // returning siblings outside this capability boundary. + output, err := s.runGit(ctx, "ls-tree", "-rlz", resolved, "--", ".") if err != nil { return nil, err } diff --git a/code/source_manifest_test.go b/code/source_manifest_test.go index 5b4ac374..b25b33f4 100644 --- a/code/source_manifest_test.go +++ b/code/source_manifest_test.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "testing" @@ -126,6 +127,44 @@ func TestGetSourceManifestRevisionPreservesGitEntryKinds(t *testing.T) { } } +func TestGetSourceManifestRevisionRespectsNestedServerRoot(t *testing.T) { + repository := t.TempDir() + writeSourceManifestFile(t, repository, "outside.txt", "outside\n", 0o644) + writeSourceManifestFile(t, repository, "units/wordcount/main.go", "package wordcount\n", 0o644) + writeSourceManifestFile(t, repository, "units/wordcount/tests/main_test.go", "package tests\n", 0o644) + gitSourceManifest(t, repository, "init", "-b", "main") + gitSourceManifest(t, repository, "config", "commit.gpgsign", "false") + gitSourceManifest(t, repository, "add", ".") + gitSourceManifest(t, repository, "commit", "-m", "nested unit") + + server := NewDefaultCodeServer(filepath.Join(repository, "units", "wordcount")) + worktreeResponse, err := server.Execute(t.Context(), &codev0.CodeRequest{ + Operation: &codev0.CodeRequest_GetSourceManifest{GetSourceManifest: &codev0.GetSourceManifestRequest{}}, + }) + if err != nil || worktreeResponse.GetFailure() != nil { + t.Fatalf("nested worktree manifest: response=%+v err=%v", worktreeResponse, err) + } + revisionResponse, err := server.Execute(t.Context(), &codev0.CodeRequest{ + Operation: &codev0.CodeRequest_GetSourceManifest{GetSourceManifest: &codev0.GetSourceManifestRequest{ + Revision: "HEAD", + IdentityMode: basev0.SourceManifestIdentityMode_SOURCE_MANIFEST_IDENTITY_MODE_CONTENT_SHA256, + }}, + }) + if err != nil || revisionResponse.GetFailure() != nil { + t.Fatalf("nested revision manifest: response=%+v err=%v", revisionResponse, err) + } + want := []string{"main.go", "tests/main_test.go"} + if got := sourceManifestEntryPaths(worktreeResponse.GetGetSourceManifest()); !slices.Equal(got, want) { + t.Fatalf("nested worktree paths = %v, want %v", got, want) + } + if got := sourceManifestEntryPaths(revisionResponse.GetGetSourceManifest()); !slices.Equal(got, want) { + t.Fatalf("nested revision paths = %v, want %v", got, want) + } + entries := sourceManifestEntriesByPath(revisionResponse.GetGetSourceManifest()) + assertSourceManifestEntry(t, entries["main.go"], 0o100644, basev0.SourceEntryKind_SOURCE_ENTRY_KIND_FILE, basev0.SourceIdentityAlgorithm_SOURCE_IDENTITY_ALGORITHM_SHA256, "package wordcount\n") + assertSourceManifestEntry(t, entries["tests/main_test.go"], 0o100644, basev0.SourceEntryKind_SOURCE_ENTRY_KIND_FILE, basev0.SourceIdentityAlgorithm_SOURCE_IDENTITY_ALGORITHM_SHA256, "package tests\n") +} + func TestGetSourceManifestWorktreeTreatsInitializedSubmoduleAsOneGitlink(t *testing.T) { dependency := t.TempDir() gitSourceManifest(t, dependency, "init", "-b", "main")