diff --git a/cmd/status.go b/cmd/status.go index 4c358d7..4c1c1dc 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -3,6 +3,7 @@ package cmd import ( "context" "encoding/json" + "errors" "fmt" "io" "os" @@ -56,6 +57,7 @@ tools can parse.`, RunE: runStatus, } cmd.Flags().Bool("json", false, "Output the status as JSON") + cmd.Flags().Bool("check", false, "Exit with an error when the working tree is not clean") return cmd } @@ -87,12 +89,22 @@ func runStatus(cmd *cobra.Command, _ []string) error { asJSON, _ := cmd.Flags().GetBool("json") if asJSON { - return writeStatusJSON(cmd.OutOrStdout(), report) + if err := writeStatusJSON(cmd.OutOrStdout(), report); err != nil { + return err + } + } else { + writeStatusText(cmd.OutOrStdout(), report) + } + + check, _ := cmd.Flags().GetBool("check") + if check && !report.Clean { + return errStatusDirty } - writeStatusText(cmd.OutOrStdout(), report) return nil } +var errStatusDirty = errors.New("working tree has changes") + // buildStatusReport classifies file entries into staged, unstaged, untracked, // and conflicted buckets. A file may be both staged and unstaged (for example // an index change plus a later worktree edit), in which case it appears in both. diff --git a/cmd/status_test.go b/cmd/status_test.go index 007a3b0..774d626 100644 --- a/cmd/status_test.go +++ b/cmd/status_test.go @@ -19,6 +19,7 @@ func TestNewStatusCmd_Wiring(t *testing.T) { assert.Equal(t, "status", cmd.Use) assert.NotEmpty(t, cmd.Short) require.NotNil(t, cmd.Flags().Lookup("json"), "--json flag should be registered") + require.NotNil(t, cmd.Flags().Lookup("check"), "--check flag should be registered") } func TestBuildStatusReport_Clean(t *testing.T) { @@ -124,6 +125,47 @@ func TestWriteStatusJSON_RoundTrip(t *testing.T) { assert.Equal(t, "A", decoded.Staged[0].Status) } +func TestRunStatus_CheckCleanRepo(t *testing.T) { + if _, err := exec.LookPath("git"); err != nil { + t.Skip("git not available") + } + dir := t.TempDir() + runGit(t, dir, "init", "-b", "main") + runGit(t, dir, "config", "user.email", "test@example.com") + runGit(t, dir, "config", "user.name", "Test") + require.NoError(t, os.WriteFile(filepath.Join(dir, "tracked.txt"), []byte("hi"), 0o644)) + runGit(t, dir, "add", "tracked.txt") + runGit(t, dir, "commit", "-m", "initial") + + t.Chdir(dir) + cmd := newStatusCmd() + var out bytes.Buffer + cmd.SetOut(&out) + require.NoError(t, cmd.Flags().Set("check", "true")) + + require.NoError(t, runStatus(cmd, nil)) + assert.Contains(t, out.String(), "Working tree clean") +} + +func TestRunStatus_CheckDirtyRepo(t *testing.T) { + if _, err := exec.LookPath("git"); err != nil { + t.Skip("git not available") + } + dir := t.TempDir() + runGit(t, dir, "init", "-b", "main") + require.NoError(t, os.WriteFile(filepath.Join(dir, "loose.txt"), []byte("hi"), 0o644)) + + t.Chdir(dir) + cmd := newStatusCmd() + var out bytes.Buffer + cmd.SetOut(&out) + require.NoError(t, cmd.Flags().Set("check", "true")) + + err := runStatus(cmd, nil) + require.ErrorIs(t, err, errStatusDirty) + assert.Contains(t, out.String(), "Untracked (1):") +} + // --------------------------------------------------------------------------- // Integration: runStatus against a real temporary repository. // ---------------------------------------------------------------------------