From fe77834072a7e1db263a1516d15070c3ef1925d8 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint Date: Sun, 9 Aug 2026 09:18:09 -0400 Subject: [PATCH] fix(control): repair repository cache origin --- pkg/control/vcs.go | 32 +++++++++++++++++++++++++-- pkg/control/vcs_test.go | 49 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/pkg/control/vcs.go b/pkg/control/vcs.go index 49445de9..ca618365 100644 --- a/pkg/control/vcs.go +++ b/pkg/control/vcs.go @@ -563,9 +563,9 @@ func prepareRepositoryRevisionAt( return preparedRepositoryRevision{}, fmt.Errorf("clone repository: %w", err) } } - configuredURL, err := gitWithEnvironment(ctx, cachePath, environment, "remote", "get-url", "origin") + configuredURL, err := ensureRepositoryOrigin(ctx, cachePath, environment, repositoryURL) if err != nil { - return preparedRepositoryRevision{}, fmt.Errorf("resolve repository origin: %w", err) + return preparedRepositoryRevision{}, err } if configuredURL != repositoryURL { return preparedRepositoryRevision{}, fmt.Errorf("repository cache origin %q does not match requested source", configuredURL) @@ -624,6 +624,34 @@ func prepareRepositoryRevisionAt( }, nil } +// ensureRepositoryOrigin repairs only the absent-origin case in a Codefly-owned +// cache projection. The request URL is authoritative when no origin exists; +// an existing origin is returned unchanged so the caller can reject a source +// mismatch instead of silently retargeting project state. +func ensureRepositoryOrigin(ctx context.Context, cachePath string, environment []string, repositoryURL string) (string, error) { + remotes, err := gitWithEnvironment(ctx, cachePath, environment, "remote") + if err != nil { + return "", fmt.Errorf("list repository remotes: %w", err) + } + hasOrigin := false + for _, remote := range strings.Split(remotes, "\n") { + if strings.TrimSpace(remote) == "origin" { + hasOrigin = true + break + } + } + if !hasOrigin { + if _, addErr := gitWithEnvironment(ctx, cachePath, environment, "remote", "add", "origin", repositoryURL); addErr != nil { + return "", fmt.Errorf("repair missing repository origin: %w", addErr) + } + } + configuredURL, err := gitWithEnvironment(ctx, cachePath, environment, "remote", "get-url", "origin") + if err != nil { + return "", fmt.Errorf("resolve repository origin: %w", err) + } + return configuredURL, nil +} + func removeIncompleteRepositoryCache(cachePath string) error { info, err := os.Lstat(cachePath) switch { diff --git a/pkg/control/vcs_test.go b/pkg/control/vcs_test.go index 5a437593..4e8e8da1 100644 --- a/pkg/control/vcs_test.go +++ b/pkg/control/vcs_test.go @@ -456,6 +456,55 @@ func TestPublicHTTPSRepositorySnapshotIgnoresAmbientGitRewrite(t *testing.T) { } } +func TestPrepareRepositoryCheckoutRepairsMissingOrigin(t *testing.T) { + source := initGitRepo(t) + serverRoot := t.TempDir() + bare := filepath.Join(serverRoot, "repository.git") + runGit(t, serverRoot, "clone", "--bare", "--", source, bare) + revision, err := git(t.Context(), source, "rev-parse", "HEAD") + if err != nil { + t.Fatal(err) + } + + root := t.TempDir() + request := PrepareRepositoryCheckoutRequest{ + Dir: root, RepositoryURL: "file://" + bare, CacheDirectory: "cache/repository", + Revision: revision, FetchIdentity: "missing-origin-repair", RemoteAccess: RepositoryRemoteAccessLocalFile, + } + if _, err := New().PrepareRepositoryCheckout(t.Context(), request); err != nil { + t.Fatal(err) + } + cachePath := filepath.Join(root, request.CacheDirectory) + runGit(t, cachePath, "remote", "remove", "origin") + junk := filepath.Join(cachePath, "generated.tmp") + if err := os.WriteFile(junk, []byte("remove me"), 0o600); err != nil { + t.Fatal(err) + } + + repaired, err := New().PrepareRepositoryCheckout(t.Context(), request) + if err != nil || repaired.Revision != revision { + t.Fatalf("repaired checkout = %+v, want revision %s (error = %v)", repaired, revision, err) + } + configuredURL, err := git(t.Context(), cachePath, "config", "--get", "remote.origin.url") + if err != nil { + t.Fatalf("read repaired origin: %v", err) + } + if configuredURL != request.RepositoryURL { + t.Fatalf("repaired origin = %q, want %q", configuredURL, request.RepositoryURL) + } + if _, err := os.Stat(junk); !os.IsNotExist(err) { + t.Fatalf("repaired checkout retained generated file (stat error = %v)", err) + } + if _, err := New().PrepareRepositoryCheckout(t.Context(), request); err != nil { + t.Fatalf("retry repaired checkout: %v", err) + } + + runGit(t, cachePath, "remote", "set-url", "origin", "file://"+filepath.Join(serverRoot, "different.git")) + if _, err := New().PrepareRepositoryCheckout(t.Context(), request); err == nil || !strings.Contains(err.Error(), "does not match requested source") { + t.Fatalf("mismatched origin error = %v, want source mismatch", err) + } +} + func writeCommit(t *testing.T, plane Plane, ctx context.Context, dir, name, content, message string) GitCommit { t.Helper() if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil {