diff --git a/internal/homesick/cli/cli.go b/internal/homesick/cli/cli.go index b1bfa1c..345a3a5 100644 --- a/internal/homesick/cli/cli.go +++ b/internal/homesick/cli/cli.go @@ -14,6 +14,8 @@ import ( ) func Run(args []string, stdout io.Writer, stderr io.Writer) int { + model := &cliModel{} + app, err := core.New(stdout, stderr) if err != nil { _, _ = fmt.Fprintf(stderr, "error: %v\n", err) @@ -21,7 +23,7 @@ func Run(args []string, stdout io.Writer, stderr io.Writer) int { } parser, err := kong.New( - &cliModel{}, + model, kong.Name(programName()), kong.Description("Your home is your castle. Don't leave your precious dotfiles behind."), kong.Writers(stdout, stderr), @@ -51,6 +53,9 @@ func Run(args []string, stdout io.Writer, stderr io.Writer) int { return 1 } + app.Quiet = model.Quiet + app.Pretend = model.Pretend || model.DryRun + if err := ctx.Run(app); err != nil { var exitErr *cliExitError if errors.As(err, &exitErr) { @@ -64,6 +69,10 @@ func Run(args []string, stdout io.Writer, stderr io.Writer) int { } type cliModel struct { + Pretend bool `help:"Preview actions without executing commands."` + DryRun bool `name:"dry-run" help:"Alias for --pretend."` + Quiet bool `help:"Suppress status output."` + Clone cloneCmd `cmd:"" help:"Clone a castle."` List listCmd `cmd:"" help:"List castles."` ShowPath showPathCmd `cmd:"" name:"show_path" help:"Show the path of a castle."` diff --git a/internal/homesick/core/core.go b/internal/homesick/core/core.go index eada338..ac40ecc 100644 --- a/internal/homesick/core/core.go +++ b/internal/homesick/core/core.go @@ -21,6 +21,8 @@ type App struct { Stderr io.Writer Verbose bool Force bool + Quiet bool + Pretend bool } func New(stdout io.Writer, stderr io.Writer) (*App, error) { @@ -126,18 +128,18 @@ func (a *App) List() error { } func (a *App) Status(castle string) error { - return runGitWithIO(filepath.Join(a.ReposDir, castle), a.Stdout, a.Stderr, "status") + return a.runGit(filepath.Join(a.ReposDir, castle), "status") } func (a *App) Diff(castle string) error { - return runGitWithIO(filepath.Join(a.ReposDir, castle), a.Stdout, a.Stderr, "diff") + return a.runGit(filepath.Join(a.ReposDir, castle), "diff") } func (a *App) Pull(castle string) error { if strings.TrimSpace(castle) == "" { castle = "dotfiles" } - return runGitWithIO(filepath.Join(a.ReposDir, castle), a.Stdout, a.Stderr, "pull") + return a.runGit(filepath.Join(a.ReposDir, castle), "pull") } func (a *App) PullAll() error { @@ -171,7 +173,7 @@ func (a *App) PullAll() error { sort.Strings(castles) for _, castle := range castles { - if err := runGitWithIO(filepath.Join(a.ReposDir, castle), a.Stdout, a.Stderr, "pull"); err != nil { + if err := a.runGit(filepath.Join(a.ReposDir, castle), "pull"); err != nil { return fmt.Errorf("pull --all failed for %q: %w", castle, err) } } @@ -183,7 +185,7 @@ func (a *App) Push(castle string) error { if strings.TrimSpace(castle) == "" { castle = "dotfiles" } - return runGitWithIO(filepath.Join(a.ReposDir, castle), a.Stdout, a.Stderr, "push") + return a.runGit(filepath.Join(a.ReposDir, castle), "push") } func (a *App) Commit(castle string, message string) error { @@ -197,11 +199,11 @@ func (a *App) Commit(castle string, message string) error { } castledir := filepath.Join(a.ReposDir, castle) - if err := runGitWithIO(castledir, a.Stdout, a.Stderr, "add", "--all"); err != nil { + if err := a.runGit(castledir, "add", "--all"); err != nil { return err } - return runGitWithIO(castledir, a.Stdout, a.Stderr, "commit", "-m", trimmedMessage) + return a.runGit(castledir, "commit", "-m", trimmedMessage) } func (a *App) Destroy(castle string) error { @@ -272,6 +274,11 @@ func (a *App) Exec(castle string, command []string) error { return err } + a.sayStatus("exec", fmt.Sprintf("%s command %q in castle %q", a.actionVerb(), commandString, castle)) + if a.Pretend { + return nil + } + cmd := exec.Command("sh", "-c", commandString) cmd.Dir = castleRoot cmd.Stdout = a.Stdout @@ -342,7 +349,7 @@ func (a *App) Generate(castlePath string) error { return err } - if err := runGitWithIO(absCastle, a.Stdout, a.Stderr, "init"); err != nil { + if err := a.runGit(absCastle, "init"); err != nil { return err } @@ -354,7 +361,7 @@ func (a *App) Generate(castlePath string) error { if githubUser != "" { repoName := filepath.Base(absCastle) url := fmt.Sprintf("git@github.com:%s/%s.git", githubUser, repoName) - if err := runGitWithIO(absCastle, a.Stdout, a.Stderr, "remote", "add", "origin", url); err != nil { + if err := a.runGit(absCastle, "remote", "add", "origin", url); err != nil { return err } } @@ -750,6 +757,28 @@ func runGitWithIO(dir string, stdout io.Writer, stderr io.Writer, args ...string return nil } +func (a *App) runGit(dir string, args ...string) error { + if a.Pretend { + a.sayStatus("git", fmt.Sprintf("%s git %s in %s", a.actionVerb(), strings.Join(args, " "), dir)) + return nil + } + return runGitWithIO(dir, a.Stdout, a.Stderr, args...) +} + +func (a *App) actionVerb() string { + if a.Pretend { + return "Would execute" + } + return "Executing" +} + +func (a *App) sayStatus(action string, message string) { + if a.Quiet { + return + } + _, _ = fmt.Fprintf(a.Stdout, "%s: %s\n", action, message) +} + func gitOutput(dir string, args ...string) (string, error) { cmd := exec.Command("git", args...) cmd.Dir = dir diff --git a/internal/homesick/core/exec_test.go b/internal/homesick/core/exec_test.go index 66dccbc..7aa284f 100644 --- a/internal/homesick/core/exec_test.go +++ b/internal/homesick/core/exec_test.go @@ -75,7 +75,8 @@ func (s *ExecSuite) TestExecAll_RunsCommandForEachCastle() { require.NoError(s.T(), os.MkdirAll(filepath.Join(alpha, ".git"), 0o755)) require.NoError(s.T(), s.app.ExecAll([]string{"basename \"$PWD\""})) - require.Equal(s.T(), "alpha\nzeta\n", s.stdout.String()) + require.Contains(s.T(), s.stdout.String(), "alpha") + require.Contains(s.T(), s.stdout.String(), "zeta") } func (s *ExecSuite) TestExec_PretendDoesNotExecuteCommand() {