Skip to content

Commit

Permalink
feat: added --git-type flag
Browse files Browse the repository at this point in the history
Available values:
    go: Uses go-git, a Go native implementation of git. This is compiled with the multi-gitter binary, and no extra dependencies are needed.
    cmd: Calls out to the git command. This requires git to be installed and available with by calling "git".
(default "go")
  • Loading branch information
lindell authored Jun 16, 2021
1 parent 37cd65d commit cb4701e
Show file tree
Hide file tree
Showing 12 changed files with 404 additions and 131 deletions.
11 changes: 8 additions & 3 deletions cmd/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ func PrintCmd() *cobra.Command {
}

cmd.Flags().IntP("concurrent", "C", 1, "The maximum number of concurrent runs")
cmd.Flags().IntP("fetch-depth", "f", 1, "Limit fetching to the specified number of commits. Set to 0 for no limit")
cmd.Flags().StringP("error-output", "E", "-", `The file that the output of the script should be outputted to. "-" means stderr`)
configureGit(cmd)
configurePlatform(cmd)
configureLogging(cmd, "")
cmd.Flags().AddFlagSet(outputFlag())
Expand All @@ -46,7 +46,6 @@ func print(cmd *cobra.Command, args []string) error {
flag := cmd.Flags()

concurrent, _ := flag.GetInt("concurrent")
fetchDepth, _ := flag.GetInt("fetch-depth")
strOutput, _ := flag.GetString("output")
strErrOutput, _ := flag.GetString("error-output")

Expand Down Expand Up @@ -81,6 +80,11 @@ func print(cmd *cobra.Command, args []string) error {
return err
}

gitCreator, err := getGitCreator(flag)
if err != nil {
return err
}

parsedCommand, err := parseCommandLine(command)
if err != nil {
return fmt.Errorf("could not parse command: %s", err)
Expand Down Expand Up @@ -116,8 +120,9 @@ func print(cmd *cobra.Command, args []string) error {
Stdout: output,
Stderr: errOutput,

FetchDepth: fetchDepth,
Concurrent: concurrent,

CreateGit: gitCreator,
}

err = printer.Print(ctx)
Expand Down
38 changes: 38 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"time"

"github.com/lindell/multi-gitter/internal/domain"
"github.com/lindell/multi-gitter/internal/git/cmdgit"
"github.com/lindell/multi-gitter/internal/git/gogit"
"github.com/lindell/multi-gitter/internal/http"
"github.com/lindell/multi-gitter/internal/multigitter"
"github.com/lindell/multi-gitter/internal/scm/gitea"
Expand Down Expand Up @@ -135,6 +137,42 @@ func configurePlatform(cmd *cobra.Command) {
})
}

func configureGit(cmd *cobra.Command) {
cmd.Flags().IntP("fetch-depth", "f", 1, "Limit fetching to the specified number of commits. Set to 0 for no limit")
cmd.Flags().StringP("git-type", "", "go", `The type of git implementation to use.
Available values:
go: Uses go-git, a Go native implementation of git. This is compiled with the multi-gitter binary, and no extra dependencies are needed.
cmd: Calls out to the git command. This requires git to be installed and available with by calling "git".
`)
_ = cmd.RegisterFlagCompletionFunc("git-type", func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"go", "cmd"}, cobra.ShellCompDirectiveDefault
})
}

func getGitCreator(flag *flag.FlagSet) (func(string) multigitter.Git, error) {
fetchDepth, _ := flag.GetInt("fetch-depth")
gitType, _ := flag.GetString("git-type")

switch gitType {
case "go":
return func(path string) multigitter.Git {
return &gogit.Git{
Directory: path,
FetchDepth: fetchDepth,
}
}, nil
case "cmd":
return func(path string) multigitter.Git {
return &cmdgit.Git{
Directory: path,
FetchDepth: fetchDepth,
}
}, nil
}

return nil, errors.Errorf(`could not parse git type "%s"`, gitType)
}

func configureLogging(cmd *cobra.Command, logFile string) {
flags := cmd.Flags()

Expand Down
11 changes: 8 additions & 3 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ func RunCmd() *cobra.Command {
cmd.Flags().StringSliceP("reviewers", "r", nil, "The username of the reviewers to be added on the pull request.")
cmd.Flags().IntP("max-reviewers", "M", 0, "If this value is set, reviewers will be randomized")
cmd.Flags().IntP("concurrent", "C", 1, "The maximum number of concurrent runs")
cmd.Flags().IntP("fetch-depth", "f", 1, "Limit fetching to the specified number of commits. Set to 0 for no limit")
cmd.Flags().BoolP("skip-pr", "", false, "Skip pull request and directly push to the branch")
cmd.Flags().BoolP("dry-run", "d", false, "Run without pushing changes or creating pull requests")
cmd.Flags().BoolP("fork", "", false, "Fork the repository instead of creating a new branch on the same owner")
cmd.Flags().StringP("fork-owner", "", "", "If set, make the fork to defined one. Default behavior is for the fork to be on the logged in user.")
cmd.Flags().StringP("author-name", "", "", "Name of the committer. If not set, the global git config setting will be used.")
cmd.Flags().StringP("author-email", "", "", "Email of the committer. If not set, the global git config setting will be used.")
configureGit(cmd)
configurePlatform(cmd)
configureLogging(cmd, "-")
cmd.Flags().AddFlagSet(outputFlag())
Expand All @@ -66,7 +66,6 @@ func run(cmd *cobra.Command, args []string) error {
commitMessage, _ := flag.GetString("commit-message")
reviewers, _ := flag.GetStringSlice("reviewers")
maxReviewers, _ := flag.GetInt("max-reviewers")
fetchDepth, _ := flag.GetInt("fetch-depth")
concurrent, _ := flag.GetInt("concurrent")
skipPullRequest, _ := flag.GetBool("skip-pr")
dryRun, _ := flag.GetBool("dry-run")
Expand Down Expand Up @@ -134,6 +133,11 @@ func run(cmd *cobra.Command, args []string) error {
return err
}

gitCreator, err := getGitCreator(flag)
if err != nil {
return err
}

parsedCommand, err := parseCommandLine(command)
if err != nil {
return fmt.Errorf("could not parse command: %s", err)
Expand Down Expand Up @@ -184,8 +188,9 @@ func run(cmd *cobra.Command, args []string) error {
CommitAuthor: commitAuthor,
BaseBranch: baseBranchName,

FetchDepth: fetchDepth,
Concurrent: concurrent,

CreateGit: gitCreator,
}

err = runner.Run(ctx)
Expand Down
9 changes: 9 additions & 0 deletions internal/domain/git.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package domain

// GitConfig is configration for any git implementation
type GitConfig struct {
// Absolute path to the directory
Directory string
// The fetch depth used when cloning, if set to 0, the entire history will be used
FetchDepth int
}
141 changes: 141 additions & 0 deletions internal/git/cmdgit/git.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package cmdgit

import (
"bytes"
"fmt"
"os/exec"
"regexp"
"strings"

"github.com/pkg/errors"
log "github.com/sirupsen/logrus"

"github.com/lindell/multi-gitter/internal/domain"
)

// Git is an implementation of git that executes git as commands
type Git struct {
Directory string // The (temporary) directory that should be worked within
FetchDepth int // Limit fetching to the specified number of commits
}

var errRe = regexp.MustCompile(`(^|\n)(error|fatal): (.+)`)

func (g *Git) run(cmd *exec.Cmd) (string, error) {
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}

cmd.Dir = g.Directory
cmd.Stderr = stderr
cmd.Stdout = stdout

err := cmd.Run()
if err != nil {
matches := errRe.FindStringSubmatch(stderr.String())
if matches != nil {
return "", errors.New(matches[3])
}

msg := fmt.Sprintf(`git command existed with %d`,
cmd.ProcessState.ExitCode(),
)

return "", errors.New(msg)
}
return stdout.String(), nil
}

// Clone a repository
func (g *Git) Clone(url string, baseName string) error {
args := []string{"clone", url, "--branch", baseName, "--single-branch"}
if g.FetchDepth > 0 {
args = append(args, "--depth", fmt.Sprint(g.FetchDepth))
}
args = append(args, g.Directory)

cmd := exec.Command("git", args...)
_, err := g.run(cmd)
return err
}

// ChangeBranch changes the branch
func (g *Git) ChangeBranch(branchName string) error {
cmd := exec.Command("git", "checkout", "-b", branchName)
_, err := g.run(cmd)
return err
}

// Changes detect if any changes has been made in the directory
func (g *Git) Changes() (bool, error) {
cmd := exec.Command("git", "status", "-s")
stdOut, err := g.run(cmd)
return len(stdOut) > 0, err
}

// Commit and push all changes
func (g *Git) Commit(commitAuthor *domain.CommitAuthor, commitMessage string) error {
cmd := exec.Command("git", "add", ".")
_, err := g.run(cmd)
if err != nil {
return err
}

cmd = exec.Command("git", "commit", "-m", commitMessage)

if commitAuthor != nil {
cmd.Env = append(cmd.Env,
"GIT_AUTHOR_NAME="+commitAuthor.Name,
"GIT_AUTHOR_EMAIL="+commitAuthor.Email,
"GIT_COMMITTER_NAME="+commitAuthor.Name,
"GIT_COMMITTER_EMAIL="+commitAuthor.Email,
)
}

_, err = g.run(cmd)

if err := g.logDiff(); err != nil {
return err
}

return err
}

func (g *Git) logDiff() error {
if !log.IsLevelEnabled(log.DebugLevel) {
return nil
}

cmd := exec.Command("git", "diff", "HEAD~1")
stdout, err := g.run(cmd)
if err != nil {
return err
}

log.Debug(stdout)

return nil
}

// BranchExist checks if the new branch exists
func (g *Git) BranchExist(remoteName, branchName string) (bool, error) {
cmd := exec.Command("git", "ls-remote", "-q", "-h")
stdOut, err := g.run(cmd)
if err != nil {
return false, err
}
return strings.Contains(stdOut, fmt.Sprintf("refs/heads/%s", branchName)), nil
}

// Push the committed changes to the remote
func (g *Git) Push(remoteName string) error {
cmd := exec.Command("git", "push", remoteName, "HEAD")
_, err := g.run(cmd)
return err
}

// AddRemote adds a new remote
func (g *Git) AddRemote(name, url string) error {
cmd := exec.Command("git", "remote", "add", name, url)
_, err := g.run(cmd)
return err
}
10 changes: 4 additions & 6 deletions internal/git/git.go → internal/git/gogit/git.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package git
package gogit

import (
"bytes"
Expand All @@ -15,20 +15,18 @@ import (
log "github.com/sirupsen/logrus"
)

// Git is an implementation of git that executes git as a command
// This has drawbacks, but the big benefit is that the configuration probably already present can be reused
// Git is an implementation of git that used go-git
type Git struct {
Directory string // The (temporary) directory that should be worked within
Repo string // The "url" to the repo, any format can be used as long as it's pushable
FetchDepth int // Limit fetching to the specified number of commits

repo *git.Repository // The repository after the clone has been made
}

// Clone a repository
func (g *Git) Clone(baseName, headName string) error {
func (g *Git) Clone(url string, baseName string) error {
r, err := git.PlainClone(g.Directory, false, &git.CloneOptions{
URL: g.Repo,
URL: url,
RemoteName: "origin",
Depth: g.FetchDepth,
ReferenceName: plumbing.NewBranchReferenceName(baseName),
Expand Down
12 changes: 4 additions & 8 deletions internal/multigitter/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
log "github.com/sirupsen/logrus"

"github.com/lindell/multi-gitter/internal/domain"
"github.com/lindell/multi-gitter/internal/git"
"github.com/lindell/multi-gitter/internal/multigitter/repocounter"
)

Expand All @@ -26,8 +25,9 @@ type Printer struct {
Stdout io.Writer
Stderr io.Writer

FetchDepth int // Limit fetching to the specified number of commits. Set to 0 for no limit
Concurrent int

CreateGit func(dir string) Git
}

// Print runs a script for multiple repositories and print the output of each run
Expand Down Expand Up @@ -77,13 +77,9 @@ func (r Printer) runSingleRepo(ctx context.Context, repo domain.Repository) erro
}
defer os.RemoveAll(tmpDir)

sourceController := &git.Git{
Directory: tmpDir,
Repo: repo.URL(r.Token),
FetchDepth: r.FetchDepth,
}
sourceController := r.CreateGit(tmpDir)

err = sourceController.Clone(repo.DefaultBranch(), "")
err = sourceController.Clone(repo.URL(r.Token), repo.DefaultBranch())
if err != nil {
return err
}
Expand Down
12 changes: 4 additions & 8 deletions internal/multigitter/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
log "github.com/sirupsen/logrus"

"github.com/lindell/multi-gitter/internal/domain"
"github.com/lindell/multi-gitter/internal/git"
"github.com/lindell/multi-gitter/internal/multigitter/logger"
"github.com/lindell/multi-gitter/internal/multigitter/repocounter"
)
Expand Down Expand Up @@ -49,12 +48,13 @@ type Runner struct {
CommitAuthor *domain.CommitAuthor
BaseBranch string // The base branch of the PR, use default branch if not set

FetchDepth int // Limit fetching to the specified number of commits. Set to 0 for no limit
Concurrent int
SkipPullRequest bool // If set, the script will run directly on the base-branch without creating any PR

Fork bool // If set, create a fork and make the pull request from it
ForkOwner string // The owner of the new fork. If empty, the fork should happen on the logged in user

CreateGit func(dir string) Git
}

var errAborted = errors.New("run was never started because of aborted execution")
Expand Down Expand Up @@ -169,18 +169,14 @@ func (r Runner) runSingleRepo(ctx context.Context, repo domain.Repository) (doma
}
defer os.RemoveAll(tmpDir)

sourceController := &git.Git{
Directory: tmpDir,
Repo: repo.URL(r.Token),
FetchDepth: r.FetchDepth,
}
sourceController := r.CreateGit(tmpDir)

baseBranch := r.BaseBranch
if baseBranch == "" {
baseBranch = repo.DefaultBranch()
}

err = sourceController.Clone(baseBranch, r.FeatureBranch)
err = sourceController.Clone(repo.URL(r.Token), baseBranch)
if err != nil {
return nil, err
}
Expand Down
Loading

0 comments on commit cb4701e

Please sign in to comment.