Skip to content
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
1 change: 1 addition & 0 deletions client/clienter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Clienter interface {
PushRepos(ctx context.Context, repoDirs []string, args ...string) error
Remotes(ctx context.Context, repoDirs []string, args ...string) error
Remove(ctx context.Context, dirs []string, name string) error
ResetRepos(ctx context.Context, repoDirs []string, args ...string) error
SetURLs(ctx context.Context, repoDirs []string, name, baseURL string) error
StageFiles(ctx context.Context, dirs []string, args ...string) error
StashRepos(ctx context.Context, dirs []string, args ...string) error
Expand Down
80 changes: 80 additions & 0 deletions client/repos/reset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package repos

import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"

ctxhelper "github.com/gomicro/align/client/context"
"github.com/gosuri/uiprogress"
)

func (r *Repos) ResetRepos(ctx context.Context, dirs []string, args ...string) error {
count := len(dirs)
args = append([]string{"reset"}, args...)

verbose := ctxhelper.Verbose(ctx)

var bar *uiprogress.Bar
currRepo := ""

if verbose {
r.scrb.BeginDescribe("Command")
defer r.scrb.EndDescribe()

r.scrb.Print(fmt.Sprintf("git %s", strings.Join(args, " ")))

r.scrb.BeginDescribe("directories")
defer r.scrb.EndDescribe()
} else {
bar = uiprogress.AddBar(count).
AppendCompleted().
PrependElapsed().
PrependFunc(func(b *uiprogress.Bar) string {
return fmt.Sprintf("Resetting (%d/%d)", b.Current(), count)
}).
AppendFunc(func(b *uiprogress.Bar) string {
return currRepo
})
}

var errs []error

for _, dir := range dirs {
currRepo = fmt.Sprintf("\nCurrent Repo: %v", dir)

out := &bytes.Buffer{}
errout := &bytes.Buffer{}

cmd := exec.CommandContext(ctx, "git", args...)
cmd.Stdout = out
cmd.Stderr = errout
cmd.Dir = dir

err := cmd.Run()
if verbose {
r.scrb.BeginDescribe(dir)
if err != nil {
r.scrb.Error(err)
r.scrb.PrintLines(errout)
} else {
r.scrb.PrintLines(out)
}

r.scrb.EndDescribe()
} else {
if err != nil {
errs = append(errs, fmt.Errorf("%s: %w: %s", dir, err, strings.TrimSpace(errout.String())))
}

bar.Incr()
}
}

currRepo = ""

return errors.Join(errs...)
}
6 changes: 6 additions & 0 deletions client/testclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ func (c *TestClient) Remotes(ctx context.Context, repoDirs []string, args ...str
return c.Errors["Remotes"]
}

func (c *TestClient) ResetRepos(ctx context.Context, repoDirs []string, args ...string) error {
c.CommandsCalled = append(c.CommandsCalled, "ResetRepos")

return c.Errors["ResetRepos"]
}

func (c *TestClient) SetURLs(ctx context.Context, repoDirs []string, name, baseURL string) error {
c.CommandsCalled = append(c.CommandsCalled, "SetURLs")

Expand Down
105 changes: 105 additions & 0 deletions cmd/reset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package cmd

import (
"context"
"fmt"

ctxhelper "github.com/gomicro/align/client/context"
"github.com/gosuri/uiprogress"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var (
hard bool
soft bool
mixed bool
)

func init() {
RootCmd.AddCommand(resetCmd)

resetCmd.Flags().StringVar(&dir, "dir", ".", "directory to reset repos in")
resetCmd.Flags().BoolVar(&hard, "hard", false, "reset index and working tree, discarding all uncommitted changes")
resetCmd.Flags().BoolVar(&soft, "soft", false, "reset HEAD only, keeping all changes staged")
resetCmd.Flags().BoolVar(&mixed, "mixed", false, "reset HEAD and index, keeping changes in the working tree (unstaged)")

resetCmd.MarkFlagsMutuallyExclusive("hard", "soft", "mixed")
}

var resetCmd = &cobra.Command{
Use: "reset [ref]",
Short: "Reset the current branch across all repos",
Long: `Reset the current branch across all repos in a directory. One of --hard, --soft, or --mixed must be provided.`,
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: resetCmdValidArgsFunc,
PersistentPreRun: setupClient,
RunE: resetFunc,
}

func resetCmdValidArgsFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) >= 1 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

setupClient(cmd, args)

resetDir, err := cmd.Flags().GetString("dir")
if err != nil {
resetDir = "."
}

ctx := context.Background()

repoDirs, err := clt.GetDirs(ctx, resetDir)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}

names, err := clt.GetBranchAndTagNames(ctx, repoDirs)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}

return names, cobra.ShellCompDirectiveNoFileComp
}

func resetFunc(cmd *cobra.Command, args []string) error {
if !hard && !soft && !mixed {
return fmt.Errorf("one of --hard, --soft, or --mixed is required")
}

verbose := viper.GetBool("verbose")
ctx := ctxhelper.WithVerbose(context.Background(), verbose)

if !verbose {
uiprogress.Start()
defer uiprogress.Stop()
}

repoDirs, err := clt.GetDirs(ctx, dir)
if err != nil {
cmd.SilenceUsage = true
return fmt.Errorf("get dirs: %w", err)
}

var modeFlag string
switch {
case hard:
modeFlag = "--hard"
case soft:
modeFlag = "--soft"
case mixed:
modeFlag = "--mixed"
}

args = append([]string{modeFlag}, args...)

err = clt.ResetRepos(ctx, repoDirs, args...)
if err != nil {
cmd.SilenceUsage = true
return fmt.Errorf("reset repos: %w", err)
}

return nil
}
92 changes: 92 additions & 0 deletions cmd/reset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package cmd

import (
"errors"
"testing"

"github.com/gomicro/align/client/testclient"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)

func TestReset(t *testing.T) {
viper.Set("verbose", true)
t.Cleanup(func() { viper.Set("verbose", false) })

t.Run("hard reset calls expected commands", func(t *testing.T) {
hard = true
t.Cleanup(func() { hard = false })

tc := testclient.New()
clt = tc

err := resetFunc(resetCmd, []string{})
assert.NoError(t, err)

tc.AssertCommandsCalled(t, "GetDirs", "ResetRepos")
})

t.Run("soft reset calls expected commands", func(t *testing.T) {
soft = true
t.Cleanup(func() { soft = false })

tc := testclient.New()
clt = tc

err := resetFunc(resetCmd, []string{})
assert.NoError(t, err)

tc.AssertCommandsCalled(t, "GetDirs", "ResetRepos")
})

t.Run("mixed reset calls expected commands", func(t *testing.T) {
mixed = true
t.Cleanup(func() { mixed = false })

tc := testclient.New()
clt = tc

err := resetFunc(resetCmd, []string{})
assert.NoError(t, err)

tc.AssertCommandsCalled(t, "GetDirs", "ResetRepos")
})

t.Run("returns error when no mode flag provided", func(t *testing.T) {
tc := testclient.New()
clt = tc

err := resetFunc(resetCmd, []string{})
assert.ErrorContains(t, err, "--hard, --soft, or --mixed is required")

tc.AssertNoCommandsCalled(t)
})

t.Run("returns error on get dirs failure", func(t *testing.T) {
hard = true
t.Cleanup(func() { hard = false })

tc := testclient.New()
tc.Errors["GetDirs"] = errors.New("some dirs error")
clt = tc

err := resetFunc(resetCmd, []string{})
assert.ErrorContains(t, err, "get dirs")

tc.AssertCommandsCalled(t, "GetDirs")
})

t.Run("returns error on reset repos failure", func(t *testing.T) {
hard = true
t.Cleanup(func() { hard = false })

tc := testclient.New()
tc.Errors["ResetRepos"] = errors.New("some reset error")
clt = tc

err := resetFunc(resetCmd, []string{})
assert.ErrorContains(t, err, "reset repos")

tc.AssertCommandsCalled(t, "GetDirs", "ResetRepos")
})
}
Loading