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 @@ -25,6 +25,7 @@ type Clienter interface {
GetTagNames(ctx context.Context, dirs []string) ([]string, error)
ListTags(ctx context.Context, repoDirs []string, args ...string) error
LogRepos(ctx context.Context, repoDirs []string, ignoreEmtpy bool, args ...string) error
MergeRepos(ctx context.Context, repoDirs []string, args ...string) error
PullRepos(ctx context.Context, repoDirs []string, args ...string) error
PushRepos(ctx context.Context, repoDirs []string, args ...string) error
Remotes(ctx context.Context, repoDirs []string, args ...string) error
Expand Down
80 changes: 80 additions & 0 deletions client/repos/merge.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) MergeRepos(ctx context.Context, dirs []string, args ...string) error {
count := len(dirs)
args = append([]string{"merge"}, 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("Merging (%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 @@ -97,6 +97,12 @@ func (c *TestClient) ListTags(ctx context.Context, repoDirs []string, args ...st
return c.Errors["ListTags"]
}

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

return c.Errors["MergeRepos"]
}

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

Expand Down
103 changes: 103 additions & 0 deletions cmd/merge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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 (
noFF bool
squash bool
abortMerge bool
)

func init() {
RootCmd.AddCommand(mergeCmd)

mergeCmd.Flags().StringVar(&dir, "dir", ".", "directory to merge repos in")
mergeCmd.Flags().BoolVar(&noFF, "no-ff", false, "create a merge commit even when fast-forward is possible")
mergeCmd.Flags().BoolVar(&squash, "squash", false, "squash commits from the branch into a single commit")
mergeCmd.Flags().BoolVar(&abortMerge, "abort", false, "abort an in-progress merge")

mergeCmd.MarkFlagsMutuallyExclusive("squash", "no-ff")
mergeCmd.MarkFlagsMutuallyExclusive("abort", "squash")
mergeCmd.MarkFlagsMutuallyExclusive("abort", "no-ff")
}

var mergeCmd = &cobra.Command{
Use: "merge [branch]",
Short: "Merge a branch into the current branch across all repos",
Long: `Merge a branch into the current branch across all repos in a directory.`,
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: mergeCmdValidArgsFunc,
PersistentPreRun: setupClient,
RunE: mergeFunc,
}

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

setupClient(cmd, args)

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

ctx := context.Background()

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

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

return names, cobra.ShellCompDirectiveNoFileComp
}

func mergeFunc(cmd *cobra.Command, args []string) error {
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)
}

if abortMerge {
args = []string{"--abort"}
} else {
if noFF {
args = append(args, "--no-ff")
}

if squash {
args = append(args, "--squash")
}
}

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

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

import (
"errors"
"testing"

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

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

t.Run("calls expected commands", func(t *testing.T) {
tc := testclient.New()
clt = tc

err := mergeFunc(mergeCmd, []string{"main"})
assert.NoError(t, err)

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

t.Run("aborts in-progress merge", func(t *testing.T) {
abortMerge = true
t.Cleanup(func() { abortMerge = false })

tc := testclient.New()
clt = tc

err := mergeFunc(mergeCmd, []string{})
assert.NoError(t, err)

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

t.Run("returns error on get dirs failure", func(t *testing.T) {
tc := testclient.New()
tc.Errors["GetDirs"] = errors.New("some dirs error")
clt = tc

err := mergeFunc(mergeCmd, []string{"main"})
assert.ErrorContains(t, err, "get dirs")

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

t.Run("returns error on merge repos failure", func(t *testing.T) {
tc := testclient.New()
tc.Errors["MergeRepos"] = errors.New("some merge error")
clt = tc

err := mergeFunc(mergeCmd, []string{"main"})
assert.ErrorContains(t, err, "merge repos")

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