diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 394918a..23a37a4 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -7,7 +7,7 @@ The eleven [audit reports](audit/) supply evidence and reproduction details; the target document owns the accepted outcomes and policy choices. This plan owns execution order, concrete implementation choices, and completion evidence. -All stages are pending. Follow Stage 1 through Stage 19 in order as one +Follow Stage 1 through Stage 19 in order as one end-to-end sprint. Stages separate work into units suitable for a Luna coding agent. Intermediate stages may leave related code or tests uncompilable; complete the dependencies before evaluating the final implementation. Run @@ -37,7 +37,7 @@ the agent policy; an intermediate compilation dependency is recorded accurately. | Stage | Work items | Status | | --- | --- | --- | | 1 | A01; publication assertion portion of A19 | Complete — regression failed before the fix and passed afterward; focused publication and reconciliation tests passed. | -| 2 | A02, A12: common Git mechanics | Pending | +| 2 | A02, A12: common Git mechanics | Complete — focused Git, candidate, identity, and public build/publication/delivery tests passed; A02 regression evidence follows in Stage 3. | | 3 | A02: source identity regression evidence | Pending | | 4 | A03: buffered command teardown | Pending | | 5 | A03, A18: piped lifetime and queued cancellation | Pending | diff --git a/internal/build/build.go b/internal/build/build.go index af6f328..ef82a70 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -15,6 +15,7 @@ import ( "go.maximumdirect.net/releasekit/internal/artifact" "go.maximumdirect.net/releasekit/internal/candidate" + "go.maximumdirect.net/releasekit/internal/git" "go.maximumdirect.net/releasekit/internal/gotool" "go.maximumdirect.net/releasekit/internal/releaseidentity" "go.maximumdirect.net/releasekit/internal/tempdir" @@ -102,14 +103,14 @@ func BuildResolved(ctx context.Context, request Request) (Result, error) { return result, fmt.Errorf("resolve module directory: %w", err) } result.ModuleDir = moduleDir - commit, err := gitCommit(ctx, repositoryDir) + commit, err := git.CaptureHead(ctx, repositoryDir) if err != nil { - return result, err + return result, fmt.Errorf("read source commit: %w", err) } result.CandidateCommit = commit - cleanBefore, err := cleanSource(ctx, repositoryDir) + statusBefore, err := git.WorktreeStatus(ctx, repositoryDir) if err != nil { - return result, err + return result, fmt.Errorf("inspect build source: %w", err) } if len(result.Targets) == 0 { @@ -192,30 +193,18 @@ func BuildResolved(ctx context.Context, request Request) (Result, error) { if err := checkVersion(ctx, smokePath, settings.BinaryName, identity.Version); err != nil { return result, err } - commitAfter, err := gitCommit(ctx, repositoryDir) + commitAfter, err := git.CaptureHead(ctx, repositoryDir) if err != nil { - return result, err + return result, fmt.Errorf("read source commit: %w", err) } - cleanAfter, err := cleanSource(ctx, repositoryDir) + statusAfter, err := git.WorktreeStatus(ctx, repositoryDir) if err != nil { - return result, err + return result, fmt.Errorf("inspect build source: %w", err) } - result.SourceVerified = cleanBefore && cleanAfter && commitAfter == commit + result.SourceVerified = statusBefore == "" && statusAfter == "" && commitAfter == commit return result, nil } -func gitCommit(ctx context.Context, repositoryDir string) (string, error) { - command := exec.CommandContext(ctx, "git", "-C", repositoryDir, "rev-parse", "--verify", "HEAD^{commit}") - output, err := command.Output() - if err != nil { - if ctx.Err() != nil { - return "", fmt.Errorf("read source commit: %w", ctx.Err()) - } - return "", fmt.Errorf("read source commit: %w", err) - } - return strings.TrimSpace(string(output)), nil -} - func buildTarget(ctx context.Context, goTool *gotool.Tool, moduleDir, mainPackage, outputPath, version string, target Target) error { ldflags := "-s -w -X " + versioncontract.InjectedVersionLinkerSymbol + "=" + version err := goTool.Run(ctx, moduleDir, gotool.Static, gotool.Target{OS: target.OS, Arch: target.Arch}, nil, @@ -252,15 +241,3 @@ func checkVersion(ctx context.Context, binaryPath, binaryName, version string) e } return nil } - -func cleanSource(ctx context.Context, repositoryDir string) (bool, error) { - command := exec.CommandContext(ctx, "git", "-C", repositoryDir, "status", "--porcelain", "--untracked-files=all") - output, err := command.Output() - if err != nil { - if ctx.Err() != nil { - return false, fmt.Errorf("inspect build source: %w", ctx.Err()) - } - return false, fmt.Errorf("inspect build source: %w", err) - } - return len(output) == 0, nil -} diff --git a/internal/candidate/check.go b/internal/candidate/check.go index 2a711a4..aa3dd45 100644 --- a/internal/candidate/check.go +++ b/internal/candidate/check.go @@ -194,18 +194,18 @@ func inspectLocalState(ctx context.Context, repositoryDir string) (localState, e if err != nil { return localState{}, fmt.Errorf("inspect current branch: %w", err) } - commitOutput, err := gitOutput(ctx, repositoryDir, "rev-parse", "--verify", "HEAD^{commit}") + commit, err := git.CaptureHead(ctx, repositoryDir) if err != nil { return localState{}, fmt.Errorf("capture HEAD commit: %w", err) } - statusOutput, err := gitOutput(ctx, repositoryDir, "status", "--porcelain=v1", "--untracked-files=all") + status, err := git.WorktreeStatus(ctx, repositoryDir) if err != nil { return localState{}, fmt.Errorf("inspect worktree and index: %w", err) } return localState{ branch: strings.TrimSpace(string(branchOutput)), - commit: strings.TrimSpace(string(commitOutput)), - status: strings.TrimSpace(string(statusOutput)), + commit: commit, + status: status, }, nil } @@ -215,6 +215,7 @@ func gitOutput(ctx context.Context, repositoryDir string, args ...string) ([]byt err := process.Run(ctx, process.Invocation{ Args: commandArgs, Dir: repositoryDir, + Env: map[string]string{"GIT_NO_REPLACE_OBJECTS": "1"}, Stdout: &stdout, Stderr: &stderr, }) @@ -233,6 +234,7 @@ func hasLocalTag(ctx context.Context, repositoryDir, tag string) (bool, error) { err := process.Run(ctx, process.Invocation{ Args: []string{"git", "show-ref", "--verify", "--quiet", "refs/tags/" + tag}, Dir: repositoryDir, + Env: map[string]string{"GIT_NO_REPLACE_OBJECTS": "1"}, Stderr: &stderr, }) if err == nil { diff --git a/internal/git/blob_reader.go b/internal/git/blob_reader.go index 01fa29a..f455fe4 100644 --- a/internal/git/blob_reader.go +++ b/internal/git/blob_reader.go @@ -128,8 +128,9 @@ func (reader *BlobReader) startLocked() error { Args: []string{"git", "cat-file", "--batch"}, Dir: reader.repositoryDir, Env: map[string]string{ - "GIT_NO_LAZY_FETCH": "1", - "GIT_ALLOW_PROTOCOL": "", + "GIT_NO_LAZY_FETCH": "1", + "GIT_ALLOW_PROTOCOL": "", + "GIT_NO_REPLACE_OBJECTS": "1", }, Stderr: &reader.stderr, }) diff --git a/internal/git/remote.go b/internal/git/remote.go index ccd9834..631a135 100644 --- a/internal/git/remote.go +++ b/internal/git/remote.go @@ -234,6 +234,7 @@ func run(ctx context.Context, dir string, config []string, args ...string) (stri err := process.Run(ctx, process.Invocation{ Args: append([]string{"git"}, commandArgs...), Dir: dir, + Env: map[string]string{"GIT_NO_REPLACE_OBJECTS": "1"}, Stdout: &stdout, Stderr: &stderr, }) diff --git a/internal/git/source.go b/internal/git/source.go index 68340bf..48df854 100644 --- a/internal/git/source.go +++ b/internal/git/source.go @@ -64,7 +64,7 @@ func DiscoverRepositoryRoot(ctx context.Context, directory string) (string, bool var stdout, stderr strings.Builder err := process.Run(ctx, process.Invocation{ Args: []string{"git", "rev-parse", "--show-toplevel"}, Dir: directory, - Env: map[string]string{"LC_ALL": "C"}, Stdout: &stdout, Stderr: &stderr, + Env: map[string]string{"LC_ALL": "C", "GIT_NO_REPLACE_OBJECTS": "1"}, Stdout: &stdout, Stderr: &stderr, }) if err == nil { root, err := normalizeRepositoryRoot(stdout.String()) @@ -199,6 +199,15 @@ func CaptureHead(ctx context.Context, repositoryDir string) (string, error) { return head, nil } +// WorktreeStatus observes repository-wide worktree and index changes. +func WorktreeStatus(ctx context.Context, repositoryDir string) (string, error) { + status, err := run(ctx, repositoryDir, nil, "status", "--porcelain=v1", "--untracked-files=all") + if err != nil { + return "", err + } + return strings.TrimSpace(status), nil +} + // InspectTaggedSource reads the local HEAD, worktree state, and selected tag. // Both lightweight historical tags and annotated tags are supported. func InspectTaggedSource(ctx context.Context, repositoryDir, tag string) (TaggedSource, error) { @@ -206,15 +215,12 @@ func InspectTaggedSource(ctx context.Context, repositoryDir, tag string) (Tagged return TaggedSource{}, err } var source TaggedSource - head, err := run(ctx, repositoryDir, nil, "rev-parse", "--verify", "HEAD^{commit}") + head, err := CaptureHead(ctx, repositoryDir) if err != nil { - return TaggedSource{}, fmt.Errorf("read checkout HEAD: %w", err) + return TaggedSource{}, err } - source.Head = strings.TrimSpace(head) - if !validObjectID(source.Head) { - return TaggedSource{}, fmt.Errorf("Git returned an invalid checkout commit %q", source.Head) - } - status, err := run(ctx, repositoryDir, nil, "status", "--porcelain", "--untracked-files=all") + source.Head = head + status, err := WorktreeStatus(ctx, repositoryDir) if err != nil { return TaggedSource{}, fmt.Errorf("inspect tagged checkout state: %w", err) } @@ -411,7 +417,7 @@ func readLocalObjects(ctx context.Context, repositoryDir string, input io.Reader Args: append([]string{"git"}, args...), Dir: repositoryDir, // Disable transports as well for Git versions predating lazy-fetch control. - Env: map[string]string{"GIT_NO_LAZY_FETCH": "1", "GIT_ALLOW_PROTOCOL": ""}, + Env: map[string]string{"GIT_NO_LAZY_FETCH": "1", "GIT_ALLOW_PROTOCOL": "", "GIT_NO_REPLACE_OBJECTS": "1"}, Stdin: input, Stdout: &stdout, Stderr: &stderr,