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/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)