loop: set upstream tracking for sketch branches on outie

Fixes boldsoftware/sketch#143

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s782ec3188bf0856ak
diff --git a/dockerimg/dockerimg.go b/dockerimg/dockerimg.go
index 19d90cf..94ba815 100644
--- a/dockerimg/dockerimg.go
+++ b/dockerimg/dockerimg.go
@@ -207,8 +207,15 @@
 	// errCh receives errors from operations that this function calls in separate goroutines.
 	errCh := make(chan error)
 
+	var upstream string
+	if out, err := combinedOutput(ctx, "git", "branch", "--show-current"); err != nil {
+		slog.DebugContext(ctx, "git branch --show-current failed (continuing)", "error", err)
+	} else {
+		upstream = strings.TrimSpace(string(out))
+	}
+
 	// Start the git server
-	gitSrv, err := newGitServer(gitRoot, config.PassthroughUpstream)
+	gitSrv, err := newGitServer(gitRoot, config.PassthroughUpstream, upstream)
 	if err != nil {
 		return fmt.Errorf("failed to start git server: %w", err)
 	}
@@ -241,12 +248,6 @@
 		commit = strings.TrimSpace(string(out))
 	}
 
-	var upstream string
-	if out, err := combinedOutput(ctx, "git", "branch", "--show-current"); err != nil {
-		slog.DebugContext(ctx, "git branch --show-current failed (continuing)", "error", err)
-	} else {
-		upstream = strings.TrimSpace(string(out))
-	}
 	if out, err := combinedOutput(ctx, "git", "config", "http.receivepack", "true"); err != nil {
 		return fmt.Errorf("git config http.receivepack true: %s: %w", out, err)
 	}
@@ -488,7 +489,7 @@
 	return gs.srv.Serve(gs.gitLn)
 }
 
-func newGitServer(gitRoot string, configureUpstreamPassthrough bool) (*gitServer, error) {
+func newGitServer(gitRoot string, configureUpstreamPassthrough bool, upstream string) (*gitServer, error) {
 	ret := &gitServer{
 		pass: rand.Text(),
 	}
@@ -509,7 +510,7 @@
 
 	var hooksDir string
 	if configureUpstreamPassthrough {
-		hooksDir, err = setupHooksDir(gitRoot)
+		hooksDir, err = setupHooksDir(upstream)
 		if err != nil {
 			return nil, fmt.Errorf("failed to setup hooks directory: %w", err)
 		}
diff --git a/dockerimg/githttp.go b/dockerimg/githttp.go
index eaeb23a..fae1678 100644
--- a/dockerimg/githttp.go
+++ b/dockerimg/githttp.go
@@ -1,6 +1,7 @@
 package dockerimg
 
 import (
+	"bytes"
 	"context"
 	"crypto/subtle"
 	_ "embed"
@@ -13,12 +14,16 @@
 	"path/filepath"
 	"runtime"
 	"strings"
+	"text/template"
 	"time"
 )
 
 //go:embed pre-receive.sh
 var preReceiveScript string
 
+//go:embed post-receive.sh
+var postReceiveScript string
+
 type gitHTTP struct {
 	gitRepoRoot string
 	hooksDir    string
@@ -41,7 +46,7 @@
 // Note:
 //   - Error propagation from origin push to user push
 //   - Session isolation with temporary hooks directory
-func setupHooksDir(gitRepoRoot string) (string, error) {
+func setupHooksDir(upstream string) (string, error) {
 	hooksDir, err := os.MkdirTemp("", "sketch-git-hooks-*")
 	if err != nil {
 		return "", fmt.Errorf("failed to create hooks directory: %w", err)
@@ -52,6 +57,21 @@
 		return "", fmt.Errorf("failed to write pre-receive hook: %w", err)
 	}
 
+	if upstream != "" {
+		tmpl, err := template.New("post-receive").Parse(postReceiveScript)
+		if err != nil {
+			return "", fmt.Errorf("failed to parse post-receive template: %w", err)
+		}
+		var buf bytes.Buffer
+		if err := tmpl.Execute(&buf, map[string]string{"Upstream": upstream}); err != nil {
+			return "", fmt.Errorf("failed to execute post-receive template: %w", err)
+		}
+		postReceiveHook := filepath.Join(hooksDir, "post-receive")
+		if err := os.WriteFile(postReceiveHook, buf.Bytes(), 0o755); err != nil {
+			return "", fmt.Errorf("failed to write post-receive hook: %w", err)
+		}
+	}
+
 	return hooksDir, nil
 }
 
@@ -151,11 +171,14 @@
 
 	w.Header().Set("Cache-Control", "no-cache")
 
-	args := []string{"http-backend"}
+	var args []string
 	if g.hooksDir != "" {
-		// Use -c flag to set core.hooksPath for this git command only
-		args = []string{"-c", "core.hooksPath=" + g.hooksDir, "-c", "receive.denyCurrentBranch=refuse", "http-backend"}
+		args = append(args,
+			"-c", "core.hooksPath="+g.hooksDir,
+			"-c", "receive.denyCurrentBranch=refuse",
+		)
 	}
+	args = append(args, "http-backend")
 
 	h := &cgi.Handler{
 		Path: gitBin,
diff --git a/dockerimg/post-receive.sh b/dockerimg/post-receive.sh
new file mode 100644
index 0000000..360600d
--- /dev/null
+++ b/dockerimg/post-receive.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+# Post-receive hook for sketch git http server
+# Sets upstream tracking branch for sketch branches
+
+set -e
+
+while read oldrev newrev refname; do
+	if [[ "$refname" =~ ^refs/heads/sketch/ ]]; then
+		git branch --set-upstream-to="{{ .Upstream }}" "${refname#refs/heads/}"
+	fi
+done
+
+exit 0