loop: on git fetch or checkout failure, retry without hooks
Fix #10 by removing git hooks on git fetch or checkout failure to solve problem where hooks might require dependencies not available in the container (e.g., git-branchless). This prevents errors during container initialization when git operations trigger hooks.
We can revisit this approach if we find users who need their hooks to run in the container environment.
diff --git a/loop/agent.go b/loop/agent.go
index 95a32bb..f654efb 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -11,6 +11,7 @@
"net/http"
"os"
"os/exec"
+ "path/filepath"
"runtime/debug"
"slices"
"strings"
@@ -803,8 +804,27 @@
}
cmd = exec.CommandContext(ctx, "git", "checkout", "-f", ini.Commit)
cmd.Dir = ini.WorkingDir
- if out, err := cmd.CombinedOutput(); err != nil {
- return fmt.Errorf("git checkout %s: %s: %w", ini.Commit, out, err)
+ if checkoutOut, err := cmd.CombinedOutput(); err != nil {
+ // Remove git hooks if they exist and retry
+ // Only try removing hooks if we haven't already removed them during fetch
+ hookPath := filepath.Join(ini.WorkingDir, ".git", "hooks")
+ if _, statErr := os.Stat(hookPath); statErr == nil {
+ slog.WarnContext(ctx, "git checkout failed, removing hooks and retrying",
+ slog.String("error", err.Error()),
+ slog.String("output", string(checkoutOut)))
+ if removeErr := removeGitHooks(ctx, ini.WorkingDir); removeErr != nil {
+ slog.WarnContext(ctx, "failed to remove git hooks", slog.String("error", removeErr.Error()))
+ }
+
+ // Retry the checkout operation
+ cmd = exec.CommandContext(ctx, "git", "checkout", "-f", ini.Commit)
+ cmd.Dir = ini.WorkingDir
+ if retryOut, retryErr := cmd.CombinedOutput(); retryErr != nil {
+ return fmt.Errorf("git checkout %s failed even after removing hooks: %s: %w", ini.Commit, retryOut, retryErr)
+ }
+ } else {
+ return fmt.Errorf("git checkout %s: %s: %w", ini.Commit, checkoutOut, err)
+ }
}
a.lastHEAD = ini.Commit
a.gitRemoteAddr = ini.GitRemoteAddr
@@ -1513,6 +1533,31 @@
return a.initialCommit
}
+// removeGitHooks removes the Git hooks directory from the repository
+func removeGitHooks(_ context.Context, repoPath string) error {
+ hooksDir := filepath.Join(repoPath, ".git", "hooks")
+
+ // Check if hooks directory exists
+ if _, err := os.Stat(hooksDir); os.IsNotExist(err) {
+ // Directory doesn't exist, nothing to do
+ return nil
+ }
+
+ // Remove the hooks directory
+ err := os.RemoveAll(hooksDir)
+ if err != nil {
+ return fmt.Errorf("failed to remove git hooks directory: %w", err)
+ }
+
+ // Create an empty hooks directory to prevent git from recreating default hooks
+ err = os.MkdirAll(hooksDir, 0755)
+ if err != nil {
+ return fmt.Errorf("failed to create empty git hooks directory: %w", err)
+ }
+
+ return nil
+}
+
// handleGitCommits() highlights new commits to the user. When running
// under docker, new HEADs are pushed to a branch according to the title.
func (a *Agent) handleGitCommits(ctx context.Context) ([]*GitCommit, error) {