diff --git a/cmd/av/stack.go b/cmd/av/stack.go index 2365ed4a..588f6414 100644 --- a/cmd/av/stack.go +++ b/cmd/av/stack.go @@ -5,8 +5,9 @@ import ( ) var stackCmd = &cobra.Command{ - Use: "stack", - Short: "managed stacked pull requests", + Use: "stack", + Aliases: []string{"st"}, + Short: "managed stacked pull requests", } func init() { @@ -14,6 +15,7 @@ func init() { stackBranchCmd, stackBranchCommitCmd, stackDiffCmd, + stackForEachCmd, stackNextCmd, stackPrevCmd, stackReorderCmd, diff --git a/cmd/av/stack_branch.go b/cmd/av/stack_branch.go index 1ab5db1d..6a48c31e 100644 --- a/cmd/av/stack_branch.go +++ b/cmd/av/stack_branch.go @@ -28,8 +28,9 @@ var stackBranchFlags struct { Force bool } var stackBranchCmd = &cobra.Command{ - Use: "branch [flags] ", - Short: "create a new stacked branch", + Use: "branch [flags] ", + Aliases: []string{"b", "br"}, + Short: "create a new stacked branch", Long: `Create a new branch that is stacked on the current branch. If the --rename/-m flag is given, the current branch is renamed to the name diff --git a/cmd/av/stack_branchcommit.go b/cmd/av/stack_branchcommit.go index 149d530f..82f3edfa 100644 --- a/cmd/av/stack_branchcommit.go +++ b/cmd/av/stack_branchcommit.go @@ -52,7 +52,7 @@ var stackBranchCommitFlags struct { var stackBranchCommitCmd = &cobra.Command{ Use: "branch-commit [flags]", - Aliases: []string{"bc"}, + Aliases: []string{"branchcommit", "bc"}, Short: "create a new stacked branch and commit staged changes to it", Long: "Create a new branch that is stacked on the current branch and commit all staged changes with the specified arguments.", SilenceUsage: true, diff --git a/cmd/av/stack_foreach.go b/cmd/av/stack_foreach.go new file mode 100644 index 00000000..c5bc3259 --- /dev/null +++ b/cmd/av/stack_foreach.go @@ -0,0 +1,115 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + + "emperror.dev/errors" + "github.com/aviator-co/av/internal/git" + "github.com/aviator-co/av/internal/meta" + "github.com/aviator-co/av/internal/utils/colors" + "github.com/aviator-co/av/internal/utils/executils" + "github.com/spf13/cobra" +) + +var stackForEachFlags struct { + previous bool + subsequent bool +} + +var stackForEachCmd = &cobra.Command{ + Use: "for-each [flags] -- [args...] ggit ", + Aliases: []string{"foreach", "fe"}, + Short: "execute a command for each branch in the current stack", + Long: `Execute a command for each branch in the current stack. + +To prevent flags for the command to be executed from being parsed as flags for +this command, use the "--" separator (see examples below). + +Output from the command will be printed to stdout/stderr as it is generated. + +Examples: + Print the current HEAD commit for each branch in the stack: + $ av stack for-each -- git rev-parse HEAD + + Push every branch in the stack: + $ av stack for-each -- git push --force + Note that the "--" separator is required here to prevent "--force" from being + interpreted as a flag for the "stack for-each" command. +`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + repo, err := getRepo() + if err != nil { + return err + } + db, err := getDB(repo) + if err != nil { + return err + } + tx := db.ReadTx() + currentBranch, err := repo.CurrentBranchName() + if err != nil { + return err + } + + var branches []string + if stackForEachFlags.previous { + branches, err = meta.PreviousBranches(tx, currentBranch) + if err != nil { + return err + } + branches = append(branches, currentBranch) + } else if stackForEachFlags.subsequent { + branches = meta.SubsequentBranches(tx, currentBranch) + branches = append([]string{currentBranch}, branches...) + } else { + branches, err = meta.PreviousBranches(tx, currentBranch) + if err != nil { + return err + } + branches = append(branches, currentBranch) + branches = append(branches, meta.SubsequentBranches(tx, currentBranch)...) + } + + _, _ = fmt.Fprint(os.Stderr, + "Executing command ", colors.CliCmd(executils.FormatCommandLine(args)), + " for ", colors.UserInput(len(branches)), " branches:\n", + ) + for _, branch := range branches { + _, _ = fmt.Fprint(os.Stderr, + " - switching to branch ", colors.UserInput(branch), "\n", + ) + if _, err := repo.CheckoutBranch(&git.CheckoutBranch{Name: branch}); err != nil { + return errors.Wrapf(err, "failed to switch to branch %q", branch) + } + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return errors.Wrapf(err, "failed to execute command for branch %q", branch) + } + } + + // Switch back to the original branch. + // We only do this on success, because on failure, it's likely that the + // user will want to be on the branch that had issues. + if _, err := repo.CheckoutBranch(&git.CheckoutBranch{Name: currentBranch}); err != nil { + return errors.Wrapf(err, "failed to switch back to branch %q", currentBranch) + } + return nil + }, +} + +func init() { + stackForEachCmd.Flags().BoolVar( + &stackForEachFlags.previous, "previous", false, + "apply the command only to the current branch and all previous branches in the stack", + ) + stackForEachCmd.Flags().BoolVar( + &stackForEachFlags.subsequent, "subsequent", false, + "apply the command only to the current branch and all subsequent branches in the stack", + ) + stackForEachCmd.MarkFlagsMutuallyExclusive("previous", "subsequent") +} diff --git a/cmd/av/stack_next.go b/cmd/av/stack_next.go index ffd63b50..1efe36e2 100644 --- a/cmd/av/stack_next.go +++ b/cmd/av/stack_next.go @@ -18,8 +18,9 @@ var stackNextFlags struct { } var stackNextCmd = &cobra.Command{ - Use: "next [|--last]", - Short: "checkout the next branch in the stack", + Use: "next [|--last]", + Aliases: []string{"n"}, + Short: "checkout the next branch in the stack", RunE: func(cmd *cobra.Command, args []string) error { // Get the subsequent branches so we can checkout the nth one repo, err := getRepo() diff --git a/cmd/av/stack_prev.go b/cmd/av/stack_prev.go index 8b756fb2..9926afcd 100644 --- a/cmd/av/stack_prev.go +++ b/cmd/av/stack_prev.go @@ -18,8 +18,9 @@ var stackPrevFlags struct { } var stackPrevCmd = &cobra.Command{ - Use: "prev [|--first]", - Short: "checkout the previous branch in the stack", + Use: "prev [|--first]", + Aliases: []string{"p"}, + Short: "checkout the previous branch in the stack", RunE: func(cmd *cobra.Command, args []string) error { // Get the previous branches so we can checkout the nth one repo, err := getRepo() diff --git a/cmd/av/stack_tree.go b/cmd/av/stack_tree.go index de328b2c..7762a04d 100644 --- a/cmd/av/stack_tree.go +++ b/cmd/av/stack_tree.go @@ -11,8 +11,9 @@ import ( ) var stackTreeCmd = &cobra.Command{ - Use: "tree", - Short: "show the tree of stacked branches", + Use: "tree", + Aliases: []string{"t"}, + Short: "show the tree of stacked branches", RunE: func(cmd *cobra.Command, args []string) error { repo, err := getRepo() if err != nil { diff --git a/e2e_tests/stack_foreach_test.go b/e2e_tests/stack_foreach_test.go new file mode 100644 index 00000000..3366e1fb --- /dev/null +++ b/e2e_tests/stack_foreach_test.go @@ -0,0 +1,28 @@ +package e2e_tests + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aviator-co/av/internal/git/gittest" +) + +func TestStackForEach(t *testing.T) { + repo := gittest.NewTempRepo(t) + Chdir(t, repo.Dir()) + + // Create a stack of three branches + RequireAv(t, "stack", "branch", "stack-1") + gittest.CommitFile(t, repo, "my-file", []byte("1a\n"), gittest.WithMessage("Commit 1a")) + RequireAv(t, "stack", "branch", "stack-2") + gittest.CommitFile(t, repo, "my-file", []byte("2a\n"), gittest.WithMessage("Commit 2a")) + RequireAv(t, "stack", "branch", "stack-3") + gittest.CommitFile(t, repo, "my-file", []byte("3a\n"), gittest.WithMessage("Commit 3a")) + + out := RequireAv(t, + "stack", "for-each", "--", + "git", "show", "--format=%s", "--quiet", "HEAD", "--", + ) + require.Equal(t, "Commit 1a\nCommit 2a\nCommit 3a\n", out.Stdout) +} diff --git a/internal/utils/executils/executils.go b/internal/utils/executils/executils.go new file mode 100644 index 00000000..fd6d6913 --- /dev/null +++ b/internal/utils/executils/executils.go @@ -0,0 +1,41 @@ +package executils + +import ( + "fmt" + "strings" +) + +// FormatCommandLine formats a command line for display. +// This is meant to prevent confusing output when a command line contains +// arguments with spaces or other special characters. +func FormatCommandLine(args []string) string { + // NB: strings.Builder never returns an error while writing, so we suppress + // the error return values below. + sb := strings.Builder{} + for i, arg := range args { + if i > 0 { + _, _ = sb.WriteString(" ") + } + if cliArgumentNeedsQuoting(arg) { + _, _ = fmt.Fprintf(&sb, "%q", arg) + } else { + _, _ = sb.WriteString(arg) + } + } + return sb.String() +} + +func cliArgumentNeedsQuoting(arg string) bool { + if arg == "" { + return true + } + for _, r := range arg { + isAllowedRune := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || + (r >= '0' && r <= '9') || + (r == '-') + if !isAllowedRune { + return true + } + } + return false +}