-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement
av stack for-each
command (#212)
Adds a way to execute a custom command on each branch in the stack. Fixes #210. ``` GIT_PAGER='' av stack for-each -- git show --format='subject: %s' --quiet HEAD -- Executing command git show "--format=subject: %s" --quiet HEAD -- for 3 branches: - switching to branch 2023-11-13-one subject: 2023-11-13-one-a - switching to branch 2023-11-13-two subject: 2023-11-13-two-a - switching to branch 2023-11-13-three subject: 2023-11-13-three-a ```
- Loading branch information
Showing
9 changed files
with
201 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |