loop/agent: add git commit hooks when running inside container
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s8d03af4abcafd1dbk
diff --git a/loop/agent.go b/loop/agent.go
index a8721a7..8992bb3 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -785,6 +785,9 @@
}
ctx := a.config.Context
if ini.InDocker {
+ if err := setupGitHooks(ini.WorkingDir); err != nil {
+ slog.WarnContext(ctx, "failed to set up git hooks", "err", err)
+ }
cmd := exec.CommandContext(ctx, "git", "stash")
cmd.Dir = ini.WorkingDir
if out, err := cmd.CombinedOutput(); err != nil {
@@ -2014,3 +2017,138 @@
unsubscribe: unsubscribe,
}
}
+
+// setupGitHooks creates or updates git hooks in the specified working directory.
+func setupGitHooks(workingDir string) error {
+ hooksDir := filepath.Join(workingDir, ".git", "hooks")
+
+ _, err := os.Stat(hooksDir)
+ if os.IsNotExist(err) {
+ return fmt.Errorf("git hooks directory does not exist: %s", hooksDir)
+ }
+ if err != nil {
+ return fmt.Errorf("error checking git hooks directory: %w", err)
+ }
+
+ // Define the post-commit hook content
+ postCommitHook := `#!/bin/bash
+echo "<post_commit_hook>"
+echo "Please review this commit message and fix it if it is incorrect."
+echo "This hook only echos the commit message; it does not modify it."
+echo "Bash escaping is a common source of issues; to fix that, create a temp file and use 'git commit --amend -F COMMIT_MSG_FILE'."
+echo "<last_commit_message>"
+git log -1 --pretty=%B
+echo "</last_commit_message>"
+echo "</post_commit_hook>"
+`
+
+ // Define the prepare-commit-msg hook content
+ prepareCommitMsgHook := `#!/bin/bash
+# Add Co-Authored-By and Change-ID trailers to commit messages
+# Check if these trailers already exist before adding them
+
+commit_file="$1"
+COMMIT_SOURCE="$2"
+
+# Skip for merges, squashes, or when using a commit template
+if [ "$COMMIT_SOURCE" = "template" ] || [ "$COMMIT_SOURCE" = "merge" ] || \
+ [ "$COMMIT_SOURCE" = "squash" ]; then
+ exit 0
+fi
+
+commit_msg=$(cat "$commit_file")
+
+needs_co_author=true
+needs_change_id=true
+
+# Check if commit message already has Co-Authored-By trailer
+if grep -q "Co-Authored-By: sketch <hello@sketch.dev>" "$commit_file"; then
+ needs_co_author=false
+fi
+
+# Check if commit message already has Change-ID trailer
+if grep -q "Change-ID: s[a-f0-9]\+k" "$commit_file"; then
+ needs_change_id=false
+fi
+
+# Only modify if at least one trailer needs to be added
+if [ "$needs_co_author" = true ] || [ "$needs_change_id" = true ]; then
+ # Ensure there's a blank line before trailers
+ if [ -s "$commit_file" ] && [ "$(tail -1 "$commit_file" | tr -d '\n')" != "" ]; then
+ echo "" >> "$commit_file"
+ fi
+
+ # Add trailers if needed
+ if [ "$needs_co_author" = true ]; then
+ echo "Co-Authored-By: sketch <hello@sketch.dev>" >> "$commit_file"
+ fi
+
+ if [ "$needs_change_id" = true ]; then
+ change_id=$(openssl rand -hex 8)
+ echo "Change-ID: s${change_id}k" >> "$commit_file"
+ fi
+fi
+`
+
+ // Update or create the post-commit hook
+ err = updateOrCreateHook(filepath.Join(hooksDir, "post-commit"), postCommitHook, "<last_commit_message>")
+ if err != nil {
+ return fmt.Errorf("failed to set up post-commit hook: %w", err)
+ }
+
+ // Update or create the prepare-commit-msg hook
+ err = updateOrCreateHook(filepath.Join(hooksDir, "prepare-commit-msg"), prepareCommitMsgHook, "Add Co-Authored-By and Change-ID trailers")
+ if err != nil {
+ return fmt.Errorf("failed to set up prepare-commit-msg hook: %w", err)
+ }
+
+ return nil
+}
+
+// updateOrCreateHook creates a new hook file or updates an existing one
+// by appending the new content if it doesn't already contain it.
+func updateOrCreateHook(hookPath, content, distinctiveLine string) error {
+ // Check if the hook already exists
+ buf, err := os.ReadFile(hookPath)
+ if os.IsNotExist(err) {
+ // Hook doesn't exist, create it
+ err = os.WriteFile(hookPath, []byte(content), 0o755)
+ if err != nil {
+ return fmt.Errorf("failed to create hook: %w", err)
+ }
+ return nil
+ }
+ if err != nil {
+ return fmt.Errorf("error reading existing hook: %w", err)
+ }
+
+ // Hook exists, check if our content is already in it by looking for a distinctive line
+ code := string(buf)
+ if strings.Contains(code, distinctiveLine) {
+ // Already contains our content, nothing to do
+ return nil
+ }
+
+ // Append our content to the existing hook
+ f, err := os.OpenFile(hookPath, os.O_APPEND|os.O_WRONLY, 0o755)
+ if err != nil {
+ return fmt.Errorf("failed to open hook for appending: %w", err)
+ }
+ defer f.Close()
+
+ // Ensure there's a newline at the end of the existing content if needed
+ if len(code) > 0 && !strings.HasSuffix(code, "\n") {
+ _, err = f.WriteString("\n")
+ if err != nil {
+ return fmt.Errorf("failed to add newline to hook: %w", err)
+ }
+ }
+
+ // Add a separator before our content
+ _, err = f.WriteString("\n# === Added by Sketch ===\n" + content)
+ if err != nil {
+ return fmt.Errorf("failed to append to hook: %w", err)
+ }
+
+ return nil
+}