sketch/loop: handle existing git repos in innie sketch

Check if /app/.git already exists before attempting to clone. If it exists
(e.g., from skaband images or user images with existing git repos), configure
the origin remote and fetch instead of cloning.

This fixes compatibility with skaband dockerfiles that create images with
existing git repositories, and adapts to the object-only approach introduced
in commit 9e8f5c78e8cef4c73e7b2629b2270ab572d530f8.

The implementation uses a helper function upsertRemoteOrigin that handles
both setting the URL for existing origin remotes and adding new ones.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s9625bfa389b6b7dek
diff --git a/dockerimg/dockerimg.go b/dockerimg/dockerimg.go
index 18284b1..2ada9c6 100644
--- a/dockerimg/dockerimg.go
+++ b/dockerimg/dockerimg.go
@@ -809,11 +809,8 @@
 // We also set up fake temporary Go module(s) so we can run "go mod download".
 // TODO: maybe 'go list ./...' and then do a build as well to populate the build cache.
 // TODO: 'npm install', etc? We have the rails for it.
-// This is an ok compromise, but a power user might want
-// less caching or more caching, depending on their use case. One approach we could take
-// is to punt entirely if /app/.git already exists. If the user has provided a -base-image with
-// their git repo, let's assume they know what they're doing, and they've customized their image
-// for their use case.
+// If /app/.git already exists, we fetch from the existing repo instead of cloning.
+// This lets advanced users arrange their git repo exactly as they desire.
 // Note that buildx has some support for conditional COPY, but without buildx, which
 // we can't reliably depend on, we have to run the base image to inspect its file system,
 // and then we can decide what to do.
diff --git a/loop/agent.go b/loop/agent.go
index 1416ab6..c4fc52d 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -1124,11 +1124,20 @@
 
 	// If a remote + commit was specified, clone it.
 	if a.config.Commit != "" && a.gitState.gitRemoteAddr != "" {
-		slog.InfoContext(ctx, "cloning git repo", "commit", a.config.Commit)
-		// TODO: --reference-if-able instead?
-		cmd := exec.CommandContext(ctx, "git", "clone", "--reference", "/git-ref", a.gitState.gitRemoteAddr, "/app")
-		if out, err := cmd.CombinedOutput(); err != nil {
-			return fmt.Errorf("failed to clone repository from %s: %s: %w", a.gitState.gitRemoteAddr, out, err)
+		if _, err := os.Stat("/app/.git"); err == nil {
+			// Already a repo in /app.
+			// Make sure that the remote is configured correctly.
+			// We do a fetch below.
+			if err := upsertRemoteOrigin(ctx, "/app", a.gitState.gitRemoteAddr); err != nil {
+				return err
+			}
+		} else {
+			slog.InfoContext(ctx, "cloning git repo", "commit", a.config.Commit)
+			// TODO: --reference-if-able instead?
+			cmd := exec.CommandContext(ctx, "git", "clone", "--reference", "/git-ref", a.gitState.gitRemoteAddr, "/app")
+			if out, err := cmd.CombinedOutput(); err != nil {
+				return fmt.Errorf("failed to clone repository from %s: %s: %w", a.gitState.gitRemoteAddr, out, err)
+			}
 		}
 	}
 
@@ -2241,6 +2250,25 @@
 	return strings.TrimSpace(string(out)), nil
 }
 
+// upsertRemoteOrigin configures the origin remote to point to the given URL.
+// If the origin remote exists, it updates the URL. If it doesn't exist, it adds it.
+func upsertRemoteOrigin(ctx context.Context, repoDir, remoteURL string) error {
+	// Try to set the URL for existing origin remote
+	cmd := exec.CommandContext(ctx, "git", "remote", "set-url", "origin", remoteURL)
+	cmd.Dir = repoDir
+	if _, err := cmd.CombinedOutput(); err == nil {
+		// Success.
+		return nil
+	}
+	// Origin doesn't exist; add it.
+	cmd = exec.CommandContext(ctx, "git", "remote", "add", "origin", remoteURL)
+	cmd.Dir = repoDir
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to add git remote origin: %s: %w", out, err)
+	}
+	return nil
+}
+
 func resolveRef(ctx context.Context, dir, refName string) (string, error) {
 	cmd := exec.CommandContext(ctx, "git", "rev-parse", refName)
 	stderr := new(strings.Builder)