diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index bb155e0..7045bfc 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -97,6 +97,10 @@ than relying on random retries. ## R03: Allow an explicit override into Git metadata — P2 +**Completed:** The override guard recognizes Git-discovered private and shared +metadata directories. Recording tests passed for ordinary, linked-worktree, and +separate-Git-directory layouts, alongside rejection of unignored worktree files. + **Relevant code:** `internal/publish/record.go:142`, especially the worktree containment and `git check-ignore` handling at lines 148–159. diff --git a/internal/git/source.go b/internal/git/source.go index 48df854..1a1265e 100644 --- a/internal/git/source.go +++ b/internal/git/source.go @@ -56,6 +56,23 @@ func normalizeRepositoryRoot(root string) (string, error) { return resolved, nil } +// MetadataDirectories returns the checkout's private and shared Git directories. +func MetadataDirectories(ctx context.Context, directory string) ([]string, error) { + var directories []string + for _, flag := range []string{"--absolute-git-dir", "--git-common-dir"} { + output, err := run(ctx, directory, nil, "rev-parse", "--path-format=absolute", flag) + if err != nil { + return nil, fmt.Errorf("resolve Git metadata directory: %w", err) + } + resolved, err := normalizeRepositoryRoot(output) + if err != nil { + return nil, err + } + directories = append(directories, resolved) + } + return directories, nil +} + // DiscoverRepositoryRoot finds a containing checkout or confirms standalone use. func DiscoverRepositoryRoot(ctx context.Context, directory string) (string, bool, error) { if directory == "" { diff --git a/internal/publish/record.go b/internal/publish/record.go index bdf486b..160b3cd 100644 --- a/internal/publish/record.go +++ b/internal/publish/record.go @@ -1,6 +1,7 @@ package publish import ( + "context" "crypto/rand" "encoding/hex" "encoding/json" @@ -12,6 +13,8 @@ import ( "path/filepath" "strings" "time" + + "go.maximumdirect.net/releasekit/internal/git" ) const recordFormatVersion = 1 @@ -148,6 +151,16 @@ func rejectUnignoredWorktreePath(repositoryDir, root, proposed string) error { if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { return nil } + metadata, err := git.MetadataDirectories(context.Background(), repositoryDir) + if err != nil { + return err + } + for _, directory := range metadata { + rel, err := filepath.Rel(directory, actual) + if err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return nil + } + } cmd := exec.Command("git", "-C", repositoryDir, "check-ignore", "-q", "--", actual) err = cmd.Run() if err == nil { diff --git a/internal/publish/record_test.go b/internal/publish/record_test.go index 9620114..81b9db0 100644 --- a/internal/publish/record_test.go +++ b/internal/publish/record_test.go @@ -142,6 +142,36 @@ func TestSaveEvidenceResolvesRelativeAndIgnoredOverrides(t *testing.T) { } } +func TestSaveEvidenceAllowsExplicitGitMetadataStores(t *testing.T) { + for _, layout := range []string{"ordinary", "linked", "separate"} { + t.Run(layout, func(t *testing.T) { + repo := recordRepo(t) + switch layout { + case "linked": + linked := filepath.Join(t.TempDir(), "linked") + recordGit(t, repo, "worktree", "add", "--detach", linked) + repo = linked + case "separate": + recordGit(t, repo, "init", "--separate-git-dir", filepath.Join(t.TempDir(), "metadata")) + } + defaultPath, err := SaveEvidence(repo, "", Evidence{}) + if err != nil { + t.Fatal(err) + } + privateDir := strings.TrimSuffix(recordGit(t, repo, "rev-parse", "--absolute-git-dir"), "\n") + for _, directory := range []string{filepath.Dir(defaultPath), filepath.Join(privateDir, "other-private-records")} { + path, err := SaveEvidence(repo, directory, Evidence{Tag: "v1.2.3"}) + if err != nil || filepath.Dir(path) != directory { + t.Fatalf("explicit metadata store %q = %q, %v", directory, path, err) + } + } + if status := recordGit(t, repo, "status", "--porcelain"); status != "" { + t.Fatalf("metadata records appeared in candidate: %s", status) + } + }) + } +} + func TestSaveEvidenceRejectsUnignoredOverrideInsideWorktree(t *testing.T) { repo := recordRepo(t) _, err := SaveEvidence(repo, "records", Evidence{Tag: "v1.2.3"})