Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement av stack for-each command #212

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions cmd/av/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import (
)

var stackCmd = &cobra.Command{
Use: "stack",
Short: "managed stacked pull requests",
Use: "stack",
Aliases: []string{"st"},
Short: "managed stacked pull requests",
}

func init() {
stackCmd.AddCommand(
stackBranchCmd,
stackBranchCommitCmd,
stackDiffCmd,
stackForEachCmd,
stackNextCmd,
stackPrevCmd,
stackReorderCmd,
Expand Down
5 changes: 3 additions & 2 deletions cmd/av/stack_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ var stackBranchFlags struct {
Force bool
}
var stackBranchCmd = &cobra.Command{
Use: "branch [flags] <branch-name>",
Short: "create a new stacked branch",
Use: "branch [flags] <branch-name>",
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
Expand Down
2 changes: 1 addition & 1 deletion cmd/av/stack_branchcommit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
115 changes: 115 additions & 0 deletions cmd/av/stack_foreach.go
Original file line number Diff line number Diff line change
@@ -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] -- <command> [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")
}
5 changes: 3 additions & 2 deletions cmd/av/stack_next.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ var stackNextFlags struct {
}

var stackNextCmd = &cobra.Command{
Use: "next [<n>|--last]",
Short: "checkout the next branch in the stack",
Use: "next [<n>|--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()
Expand Down
5 changes: 3 additions & 2 deletions cmd/av/stack_prev.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ var stackPrevFlags struct {
}

var stackPrevCmd = &cobra.Command{
Use: "prev [<n>|--first]",
Short: "checkout the previous branch in the stack",
Use: "prev [<n>|--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()
Expand Down
5 changes: 3 additions & 2 deletions cmd/av/stack_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
28 changes: 28 additions & 0 deletions e2e_tests/stack_foreach_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
41 changes: 41 additions & 0 deletions internal/utils/executils/executils.go
Original file line number Diff line number Diff line change
@@ -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
}