From e0dc047eb74ac2407295210e07860b6f9c1aafba Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Mon, 23 Feb 2026 16:53:31 +0100 Subject: [PATCH] make git hooks a noop if entire is not enabled in the repo Entire-Checkpoint: db193847867e --- cmd/entire/cli/hook_registry_test.go | 6 ++ cmd/entire/cli/hooks_git_cmd.go | 35 ++++++++++++ cmd/entire/cli/hooks_git_cmd_test.go | 82 ++++++++++++++++++++++++++++ cmd/entire/cli/settings/settings.go | 26 +++++++++ 4 files changed, 149 insertions(+) diff --git a/cmd/entire/cli/hook_registry_test.go b/cmd/entire/cli/hook_registry_test.go index 7d13a27da..b8cd95d75 100644 --- a/cmd/entire/cli/hook_registry_test.go +++ b/cmd/entire/cli/hook_registry_test.go @@ -60,6 +60,12 @@ func TestNewAgentHookVerbCmd_LogsInvocation(t *testing.T) { t.Fatalf("failed to create .entire directory: %v", err) } + // Create settings.json to indicate Entire is set up in this repo + settingsFile := filepath.Join(entireDir, "settings.json") + if err := os.WriteFile(settingsFile, []byte(`{"enabled":true,"strategy":"manual-commit"}`), 0o644); err != nil { + t.Fatalf("failed to create settings file: %v", err) + } + // Create logs directory logsDir := filepath.Join(entireDir, "logs") if err := os.MkdirAll(logsDir, 0o755); err != nil { diff --git a/cmd/entire/cli/hooks_git_cmd.go b/cmd/entire/cli/hooks_git_cmd.go index e97156676..2655478cd 100644 --- a/cmd/entire/cli/hooks_git_cmd.go +++ b/cmd/entire/cli/hooks_git_cmd.go @@ -6,6 +6,7 @@ import ( "time" "github.com/entireio/cli/cmd/entire/cli/logging" + "github.com/entireio/cli/cmd/entire/cli/settings" "github.com/entireio/cli/cmd/entire/cli/strategy" "github.com/spf13/cobra" @@ -13,6 +14,10 @@ import ( const unknownStrategyName = "unknown" +// gitHooksDisabled is set by PersistentPreRunE when Entire is not set up or disabled. +// When true, all git hook commands return early without doing any work. +var gitHooksDisabled bool + // gitHookContext holds common state for git hook logging. type gitHookContext struct { hookName string @@ -59,7 +64,14 @@ func (g *gitHookContext) logCompleted(err error, extraAttrs ...any) { // initHookLogging initializes logging for hooks by finding the most recent session. // Returns a cleanup function that should be deferred. +// If Entire is not set up or disabled, returns a no-op to avoid creating files. func initHookLogging() func() { + // Don't create any files if Entire is not set up or disabled. + // This is checked here as defense-in-depth (also checked in PersistentPreRunE). + if !settings.IsSetUpAndEnabled() { + return func() {} + } + // Set up log level getter so logging can read from settings logging.SetLogLevelGetter(GetLogLevel) @@ -83,6 +95,13 @@ func newHooksGitCmd() *cobra.Command { Long: "Commands called by git hooks. These delegate to the current strategy.", Hidden: true, // Internal command, not for direct user use PersistentPreRunE: func(_ *cobra.Command, _ []string) error { + // Check if Entire is set up and enabled before doing any work. + // This prevents global git hooks from doing anything in repos where + // Entire was never enabled or has been disabled. + if !settings.IsSetUpAndEnabled() { + gitHooksDisabled = true + return nil + } hookLogCleanup = initHookLogging() return nil }, @@ -108,6 +127,10 @@ func newHooksGitPrepareCommitMsgCmd() *cobra.Command { Short: "Handle prepare-commit-msg git hook", Args: cobra.RangeArgs(1, 2), RunE: func(_ *cobra.Command, args []string) error { + if gitHooksDisabled { + return nil + } + commitMsgFile := args[0] var source string if len(args) > 1 { @@ -133,6 +156,10 @@ func newHooksGitCommitMsgCmd() *cobra.Command { Short: "Handle commit-msg git hook", Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { + if gitHooksDisabled { + return nil + } + commitMsgFile := args[0] g := newGitHookContext("commit-msg") @@ -155,6 +182,10 @@ func newHooksGitPostCommitCmd() *cobra.Command { Short: "Handle post-commit git hook", Args: cobra.NoArgs, RunE: func(_ *cobra.Command, _ []string) error { + if gitHooksDisabled { + return nil + } + g := newGitHookContext("post-commit") g.logInvoked() @@ -174,6 +205,10 @@ func newHooksGitPrePushCmd() *cobra.Command { Short: "Handle pre-push git hook", Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { + if gitHooksDisabled { + return nil + } + remote := args[0] g := newGitHookContext("pre-push") diff --git a/cmd/entire/cli/hooks_git_cmd_test.go b/cmd/entire/cli/hooks_git_cmd_test.go index d06bfa875..3741d42be 100644 --- a/cmd/entire/cli/hooks_git_cmd_test.go +++ b/cmd/entire/cli/hooks_git_cmd_test.go @@ -28,6 +28,16 @@ func TestInitHookLogging(t *testing.T) { } t.Run("returns cleanup func when no session state exists", func(t *testing.T) { + // Create settings.json to indicate Entire is set up + entireDir := filepath.Join(tmpDir, paths.EntireDir) + if err := os.MkdirAll(entireDir, 0o755); err != nil { + t.Fatalf("failed to create .entire directory: %v", err) + } + settingsFile := filepath.Join(entireDir, "settings.json") + if err := os.WriteFile(settingsFile, []byte(`{"enabled":true}`), 0o644); err != nil { + t.Fatalf("failed to create settings file: %v", err) + } + cleanup := initHookLogging() if cleanup == nil { t.Fatal("expected cleanup function, got nil") @@ -42,6 +52,12 @@ func TestInitHookLogging(t *testing.T) { t.Fatalf("failed to create .entire directory: %v", err) } + // Create settings.json to indicate Entire is set up in this repo + settingsFile := filepath.Join(entireDir, "settings.json") + if err := os.WriteFile(settingsFile, []byte(`{"enabled":true,"strategy":"manual-commit"}`), 0o644); err != nil { + t.Fatalf("failed to create settings file: %v", err) + } + // Create session state file in .git/entire-sessions/ sessionID := "test-session-12345" stateDir := filepath.Join(tmpDir, ".git", session.SessionStateDirName) @@ -85,3 +101,69 @@ func TestInitHookLogging(t *testing.T) { } }) } + +// TestInitHookLogging_SkipsWhenNotSetUp tests that initHookLogging() does not +// create .entire/logs/ in repos where Entire has not been set up. +// This is a separate test because it needs its own t.Chdir() to a different directory. +func TestInitHookLogging_SkipsWhenNotSetUp(t *testing.T) { + // Create a temp directory without .entire/settings.json + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + // Initialize git repo + gitInit := exec.CommandContext(context.Background(), "git", "init") + gitInit.Dir = tmpDir + if err := gitInit.Run(); err != nil { + t.Fatalf("failed to init git repo: %v", err) + } + + // Do NOT create .entire/settings.json - simulating a repo where Entire is not set up + + cleanup := initHookLogging() + if cleanup == nil { + t.Fatal("expected cleanup function, got nil") + } + cleanup() // Should not panic + + // Verify .entire/logs was NOT created + logsDir := filepath.Join(tmpDir, ".entire", "logs") + if _, err := os.Stat(logsDir); !os.IsNotExist(err) { + t.Errorf("expected .entire/logs to NOT be created when Entire is not set up, but it exists") + } +} + +// TestInitHookLogging_SkipsWhenDisabled tests that initHookLogging() does not +// create .entire/logs/ when Entire is set up but disabled. +func TestInitHookLogging_SkipsWhenDisabled(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + // Initialize git repo + gitInit := exec.CommandContext(context.Background(), "git", "init") + gitInit.Dir = tmpDir + if err := gitInit.Run(); err != nil { + t.Fatalf("failed to init git repo: %v", err) + } + + // Create .entire/settings.json with enabled: false + entireDir := filepath.Join(tmpDir, paths.EntireDir) + if err := os.MkdirAll(entireDir, 0o755); err != nil { + t.Fatalf("failed to create .entire directory: %v", err) + } + settingsFile := filepath.Join(entireDir, "settings.json") + if err := os.WriteFile(settingsFile, []byte(`{"enabled":false,"strategy":"manual-commit"}`), 0o644); err != nil { + t.Fatalf("failed to create settings file: %v", err) + } + + cleanup := initHookLogging() + if cleanup == nil { + t.Fatal("expected cleanup function, got nil") + } + cleanup() // Should not panic + + // Verify .entire/logs was NOT created + logsDir := filepath.Join(tmpDir, ".entire", "logs") + if _, err := os.Stat(logsDir); !os.IsNotExist(err) { + t.Errorf("expected .entire/logs to NOT be created when Entire is disabled, but it exists") + } +} diff --git a/cmd/entire/cli/settings/settings.go b/cmd/entire/cli/settings/settings.go index 381c9993a..1249eb01c 100644 --- a/cmd/entire/cli/settings/settings.go +++ b/cmd/entire/cli/settings/settings.go @@ -213,6 +213,32 @@ func applyDefaults(settings *EntireSettings) { } } +// IsSetUp returns true if Entire has been set up in the current repository. +// This checks if .entire/settings.json exists. +// Use this to avoid creating files/directories in repos where Entire was never enabled. +func IsSetUp() bool { + settingsFileAbs, err := paths.AbsPath(EntireSettingsFile) + if err != nil { + return false + } + _, err = os.Stat(settingsFileAbs) + return err == nil +} + +// IsSetUpAndEnabled returns true if Entire is both set up and enabled. +// This checks if .entire/settings.json exists AND has enabled: true. +// Use this for hooks that should be no-ops when Entire is not active. +func IsSetUpAndEnabled() bool { + if !IsSetUp() { + return false + } + s, err := Load() + if err != nil { + return false + } + return s.Enabled +} + // IsSummarizeEnabled checks if auto-summarize is enabled in settings. // Returns false by default if settings cannot be loaded or the key is missing. func IsSummarizeEnabled() bool {