Skip to content
Open
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
5 changes: 4 additions & 1 deletion cmd/cmd-run.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func RunCmd() *cobra.Command {
cmd.Flags().IntP("concurrent", "C", 1, "The maximum number of concurrent runs.")
cmd.Flags().BoolP("skip-pr", "", false, "Skip pull request and directly push to the branch.")
cmd.Flags().BoolP("push-only", "", false, "Skip pull request and only push the feature branch.")
cmd.Flags().BoolP("manual-commit", "", false, "Don't automatically commit changes. The script must commit the changes itself. Multiple commits are allowed.")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cmd.Flags().BoolP("manual-commit", "", false, "Don't automatically commit changes. The script must commit the changes itself. Multiple commits are allowed.")
cmd.Flags().BoolP("manual-commit", "", false, "Let the script commit the changes, multiple commits are allowed, Multi-gitter will still open a pull request when changes are detected.")

cmd.Flags().BoolP("api-push", "", false, `Push changes through the API instead of git. Only supported for GitHub.
It has the benefit of automatically producing verified commits. However, it is slower and not suited for changes to large files.`)
cmd.Flags().StringSliceP("skip-repo", "s", nil, "Skip changes on specified repositories, the name is including the owner of repository in the format \"ownerName/repoName\".")
Expand Down Expand Up @@ -94,6 +95,7 @@ func run(cmd *cobra.Command, _ []string) error {
concurrent, _ := flag.GetInt("concurrent")
skipPullRequest, _ := flag.GetBool("skip-pr")
pushOnly, _ := flag.GetBool("push-only")
manualCommit, _ := flag.GetBool("manual-commit")
apiPush, _ := flag.GetBool("api-push")
skipRepository, _ := flag.GetStringSlice("skip-repo")
interactive, _ := flag.GetBool("interactive")
Expand Down Expand Up @@ -124,7 +126,7 @@ func run(cmd *cobra.Command, _ []string) error {
}

// Set commit message based on pr title and body or the reverse
if commitMessage == "" && prTitle == "" {
if commitMessage == "" && prTitle == "" && !manualCommit {
return errors.New("pull request title or commit message must be set")
} else if commitMessage == "" {
commitMessage = prTitle
Expand Down Expand Up @@ -257,6 +259,7 @@ func run(cmd *cobra.Command, _ []string) error {
SkipPullRequest: skipPullRequest,
PushOnly: pushOnly,
APIPush: apiPush,
ManualCommit: manualCommit,
SkipRepository: skipRepository,
CommitAuthor: commitAuthor,
BaseBranch: baseBranchName,
Expand Down
7 changes: 5 additions & 2 deletions internal/git/changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package git

// Changes represents the changes made to a repository
type Changes struct {
// Message is the commit message
Message string

// Map of file paths to the changes made to the file
// The key is the file path and the value is the change
Additions map[string][]byte
Expand All @@ -13,6 +16,6 @@ type Changes struct {
OldHash string
}

type LastCommitChecker interface {
LastCommitChanges() (Changes, error)
type ChangeFetcher interface {
CommitChanges(sinceCommitHash string) ([]Changes, error)
}
7 changes: 7 additions & 0 deletions internal/git/cmdgit/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,10 @@ func (g *Git) AddRemote(name, url string) error {
_, err := g.run(cmd)
return err
}

// LatestCommitHash returns the latest commit hash
func (g *Git) LatestCommitHash() (string, error) {
cmd := exec.Command("git", "rev-parse", "HEAD")
stdOut, err := g.run(cmd)
return strings.TrimSpace(stdOut), err
}
55 changes: 44 additions & 11 deletions internal/git/gogit/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"bytes"
"context"
"io"
"slices"
"time"

"github.com/go-git/go-git/v5/config"
Expand Down Expand Up @@ -221,31 +222,62 @@
return err
}

func (g *Git) LastCommitChanges() (internalgit.Changes, error) {
iter, err := g.repo.Log(&git.LogOptions{})
// LatestCommitHash returns the latest commit hash
func (g *Git) LatestCommitHash() (string, error) {
head, err := g.repo.Head()
if err != nil {
return internalgit.Changes{}, err
return "", err
}
return head.Hash().String(), nil
}

current, err := iter.Next()
func (g *Git) CommitChanges(sinceCommitHash string) ([]internalgit.Changes, error) {
iter, err := g.repo.Log(&git.LogOptions{})
if err != nil {
return internalgit.Changes{}, errors.WithMessage(err, "could not get current commit")
return nil, err
}
last, err := iter.Next()

toCommit, err := iter.Next()
if err != nil {
return internalgit.Changes{}, errors.WithMessage(err, "could not get last commit")
return nil, errors.WithMessage(err, "could not get current commit")
}

allChanges := []internalgit.Changes{}
for {
fromCommit, err := iter.Next()
if err != nil {
return nil, errors.WithMessage(err, "could not get last commit")
}

changes, err := g.changesBetweenCommits(context.Background(), fromCommit, toCommit)
if err != nil {
return nil, errors.WithMessage(err, "could not get changes")
}
allChanges = append(allChanges, changes)

if sinceCommitHash == fromCommit.Hash.String() {
break
}
toCommit = fromCommit
}

currentTree, err := current.Tree()
// Reverse the order of the changes to get the earliest commit first
slices.Reverse(allChanges)

return allChanges, nil
}

func (g *Git) changesBetweenCommits(ctx context.Context, from, to *object.Commit) (internalgit.Changes, error) {

Check failure on line 270 in internal/git/gogit/git.go

View workflow job for this annotation

GitHub Actions / golangci

[golangci] internal/git/gogit/git.go#L270

unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
Raw output
internal/git/gogit/git.go:270:37: unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
func (g *Git) changesBetweenCommits(ctx context.Context, from, to *object.Commit) (internalgit.Changes, error) {
                                    ^
toTree, err := to.Tree()
if err != nil {
return internalgit.Changes{}, errors.WithMessage(err, "could not get current tree")
}
lastTree, err := last.Tree()
fromTree, err := from.Tree()
if err != nil {
return internalgit.Changes{}, errors.WithMessage(err, "could not get current tree")
}

changes, err := lastTree.Diff(currentTree)
changes, err := fromTree.Diff(toTree)
if err != nil {
return internalgit.Changes{}, errors.WithMessage(err, "could not get diff")
}
Expand Down Expand Up @@ -281,8 +313,9 @@
}

return internalgit.Changes{
Message: to.Message,
Additions: additions,
Deletions: deletions,
OldHash: last.Hash.String(),
OldHash: from.Hash.String(),
}, nil
}
46 changes: 32 additions & 14 deletions internal/multigitter/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type Runner struct {
PushOnly bool // If set, the script will only publish the feature branch without creating a PR
SkipRepository []string // A list of repositories that run will skip
APIPush bool // Use the SCM API to commit and push the changes instead of git
ManualCommit bool // If set, multi-gitter will not commit the changes left by the script.
RegExIncludeRepository *regexp.Regexp
RegExExcludeRepository *regexp.Regexp

Expand Down Expand Up @@ -263,6 +264,11 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
}
}

commitHashBeforeRun, err := sourceController.LatestCommitHash()
if err != nil {
return nil, err
}

cmd := prepareScriptCommand(ctx, repo, tmpDir, r.ScriptPath, r.Arguments)
if r.DryRun {
cmd.Env = append(cmd.Env, "DRY_RUN=true")
Expand All @@ -279,19 +285,30 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
return nil, transformExecError(err)
}

if changed, err := sourceController.Changes(); err != nil {
return nil, err
} else if !changed {
return nil, errNoChange
}
if !r.ManualCommit {
if changed, err := sourceController.Changes(); err != nil {
return nil, err
} else if !changed {
return nil, errNoChange
}

err = sourceController.Commit(r.CommitAuthor, r.CommitMessage)
if err != nil {
return nil, err
err = sourceController.Commit(r.CommitAuthor, r.CommitMessage)
if err != nil {
return nil, err
}
} else {
commitHashAfterRun, err := sourceController.LatestCommitHash()
if err != nil {
return nil, err
}

if commitHashBeforeRun == commitHashAfterRun {
return nil, errNoChange
}
}

if r.Interactive {
err = r.interactive(tmpDir, repo)
err = r.interactive(tmpDir, repo, commitHashBeforeRun)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -346,18 +363,19 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
return nil, errors.Wrap(err, "could not push changes")
}
} else {
commitChecker, hasCommitChecker := sourceController.(git.LastCommitChecker)
commitChecker, hasCommitChecker := sourceController.(git.ChangeFetcher)
changePusher, hasChangePusher := r.VersionController.(scm.ChangePusher)
if !hasCommitChecker || !hasChangePusher {
return nil, errors.New("the scm implementation does not support committing through the API")
}

changes, err := commitChecker.LastCommitChanges()
// Todo: Change to ChangesSince(commitBeforeRun)
changes, err := commitChecker.CommitChanges(commitHashBeforeRun)
if err != nil {
return nil, errors.Wrap(err, "could not get diff")
}

err = changePusher.Push(ctx, repo, r.CommitMessage, changes, r.FeatureBranch, featureBranchExist, forcePush)
err = changePusher.Push(ctx, repo, changes, r.FeatureBranch, featureBranchExist, forcePush)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -422,7 +440,7 @@ func (r *Runner) ensurePullRequestExists(ctx context.Context, log log.FieldLogge

var interactiveInfo = `(V)iew changes. (A)ccept or (R)eject`

func (r *Runner) interactive(dir string, repo scm.Repository) error {
func (r *Runner) interactive(dir string, repo scm.Repository, oldCommitHash string) error {
fmt.Fprintf(os.Stderr, "Changes were made to %s\n", terminal.Bold(repo.FullName()))
fmt.Fprintln(os.Stderr, interactiveInfo)
for {
Expand All @@ -444,7 +462,7 @@ func (r *Runner) interactive(dir string, repo scm.Repository) error {
switch unicode.ToLower(char) {
case 'v':
fmt.Fprintln(os.Stderr, "Showing changes...")
cmd := exec.Command("git", "diff", "HEAD~1")
cmd := exec.Command("git", "diff", oldCommitHash)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
Expand Down
1 change: 1 addition & 0 deletions internal/multigitter/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Git interface {
BranchExist(remoteName, branchName string) (bool, error)
Push(ctx context.Context, remoteName string, force bool) error
AddRemote(name, url string) error
LatestCommitHash() (string, error)
}

type stackTracer interface {
Expand Down
3 changes: 1 addition & 2 deletions internal/scm/changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ type ChangePusher interface {
Push(
ctx context.Context,
repo Repository,
commitMessage string,
change git.Changes,
change []git.Changes,
featureBranch string,
branchExist bool,
forcePush bool,
Expand Down
52 changes: 36 additions & 16 deletions internal/scm/github/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ var _ scm.ChangePusher = &Github{}
func (g *Github) Push(
ctx context.Context,
r scm.Repository,
commitMessage string,
changes git.Changes,
changes []git.Changes,
featureBranch string,
branchExist bool,
forcePush bool,
Expand All @@ -37,15 +36,26 @@ func (g *Github) Push(
}

if !branchExist {
err := g.CreateBranch(ctx, repo, featureBranch, changes.OldHash)
err := g.CreateBranch(ctx, repo, featureBranch, changes[0].OldHash)
if err != nil {
return err
}
}

err := g.CommitThroughAPI(ctx, repo, featureBranch, commitMessage, changes)
if err != nil {
return err
var err error
newHash := ""
for _, change := range changes {
// If multiple changes are made, the old hash should be the new hash
// from the previous commit. The commit locally won't be exactly the same
// as the commit made through the API
if newHash != "" {
change.OldHash = newHash
}

newHash, err = g.CommitThroughAPI(ctx, repo, featureBranch, change)
if err != nil {
return err
}
}

return nil
Expand All @@ -54,14 +64,13 @@ func (g *Github) Push(
func (g *Github) CommitThroughAPI(ctx context.Context,
repo repository,
branch string,
commitMessage string,
changes git.Changes,
) error {
) (string, error) {
query := `
mutation ($input: CreateCommitOnBranchInput!) {
createCommitOnBranch(input: $input) {
commit {
url
oid
}
}
}`
Expand All @@ -72,7 +81,7 @@ func (g *Github) CommitThroughAPI(ctx context.Context,

v.Input.Branch.BranchName = branch
v.Input.ExpectedHeadOid = changes.OldHash
v.Input.Message.Headline = commitMessage
v.Input.Message.Headline = changes.Message

for path, contents := range changes.Additions {
v.Input.FileChanges.Additions = append(v.Input.FileChanges.Additions, commitAddition{
Expand All @@ -87,14 +96,17 @@ func (g *Github) CommitThroughAPI(ctx context.Context,
})
}

var result map[string]interface{}

var result createCommitOnBranchOutput
err := g.makeGraphQLRequest(ctx, query, v, &result)
if err != nil {
return errors.WithMessage(err, "could not commit changes though API")
return "", errors.WithMessage(err, "could not commit changes though API")
}
oid := result.CreateCommitOnBranch.Commit.Oid
if oid == "" {
return "", errors.New("could not get commit oid")
}

return nil
return oid, nil
}

func (g *Github) CreateBranch(ctx context.Context, repo repository, branchName string, oid string) error {
Expand Down Expand Up @@ -180,12 +192,20 @@ type createCommitOnBranchInput struct {
Headline string `json:"headline"`
} `json:"message"`
FileChanges struct {
Additions []commitAddition `json:"additions"`
Deletions []commitDeletion `json:"deletions"`
Additions []commitAddition `json:"additions,omitempty"`
Deletions []commitDeletion `json:"deletions,omitempty"`
} `json:"fileChanges"`
} `json:"input"`
}

type createCommitOnBranchOutput struct {
CreateCommitOnBranch struct {
Commit struct {
Oid string `json:"oid"`
} `json:"commit"`
} `json:"createCommitOnBranch"`
}

type commitAddition struct {
Path string `json:"path,omitempty"`
Contents string `json:"contents,omitempty"`
Expand Down
Loading
Loading