Skip to content

Commit 70d7bc7

Browse files
feat: add api-push (#548)
Co-authored-by: ChrisStatham <[email protected]>
1 parent 0e96e75 commit 70d7bc7

File tree

10 files changed

+428
-3
lines changed

10 files changed

+428
-3
lines changed

cmd/cmd-run.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ func RunCmd() *cobra.Command {
4949
cmd.Flags().IntP("concurrent", "C", 1, "The maximum number of concurrent runs.")
5050
cmd.Flags().BoolP("skip-pr", "", false, "Skip pull request and directly push to the branch.")
5151
cmd.Flags().BoolP("push-only", "", false, "Skip pull request and only push the feature branch.")
52+
cmd.Flags().BoolP("api-push", "", false, `Push changes through the API instead of git. Only supported for GitHub.
53+
It has the benefit of automatically producing verified commits. However, it is slower and not suited for changes to large files.`)
5254
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\".")
5355
cmd.Flags().BoolP("interactive", "i", false, "Take manual decision before committing any change. Requires git to be installed.")
5456
cmd.Flags().BoolP("dry-run", "d", false, "Run without pushing changes or creating pull requests.")
@@ -92,6 +94,7 @@ func run(cmd *cobra.Command, _ []string) error {
9294
concurrent, _ := flag.GetInt("concurrent")
9395
skipPullRequest, _ := flag.GetBool("skip-pr")
9496
pushOnly, _ := flag.GetBool("push-only")
97+
apiPush, _ := flag.GetBool("api-push")
9598
skipRepository, _ := flag.GetStringSlice("skip-repo")
9699
interactive, _ := flag.GetBool("interactive")
97100
dryRun, _ := flag.GetBool("dry-run")
@@ -108,6 +111,9 @@ func run(cmd *cobra.Command, _ []string) error {
108111
repoInclude, _ := flag.GetString("repo-include")
109112
repoExclude, _ := flag.GetString("repo-exclude")
110113

114+
platform, _ := flag.GetString("platform")
115+
gitType, _ := flag.GetString("git-type")
116+
111117
if concurrent < 1 {
112118
return errors.New("concurrent runs can't be less than one")
113119
}
@@ -149,6 +155,15 @@ func run(cmd *cobra.Command, _ []string) error {
149155
return errors.New("--concurrent and --interactive can't be used at the same time")
150156
}
151157

158+
if apiPush {
159+
if platform != "github" {
160+
return errors.New("api-push is only supported for GitHub")
161+
}
162+
if gitType != "go" {
163+
return errors.New("api-push only works with go-git")
164+
}
165+
}
166+
152167
// Parse commit author data
153168
var commitAuthor *git.CommitAuthor
154169
if authorName != "" || authorEmail != "" {
@@ -241,6 +256,7 @@ func run(cmd *cobra.Command, _ []string) error {
241256
ForkOwner: forkOwner,
242257
SkipPullRequest: skipPullRequest,
243258
PushOnly: pushOnly,
259+
APIPush: apiPush,
244260
SkipRepository: skipRepository,
245261
CommitAuthor: commitAuthor,
246262
BaseBranch: baseBranchName,

internal/git/changes.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package git
2+
3+
// Changes represents the changes made to a repository
4+
type Changes struct {
5+
// Map of file paths to the changes made to the file
6+
// The key is the file path and the value is the change
7+
Additions map[string][]byte
8+
9+
// List of file paths that were deleted
10+
Deletions []string
11+
12+
// OldHash is the hash of the previous commit
13+
OldHash string
14+
}
15+
16+
type LastCommitChecker interface {
17+
LastCommitChanges() (Changes, error)
18+
}

internal/git/gogit/git.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package gogit
33
import (
44
"bytes"
55
"context"
6+
"io"
67
"time"
78

89
"github.com/go-git/go-git/v5/config"
910
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
1011
"github.com/go-git/go-git/v5/plumbing/object"
12+
"github.com/go-git/go-git/v5/utils/merkletrie"
1113
internalgit "github.com/lindell/multi-gitter/internal/git"
1214
"github.com/pkg/errors"
1315

@@ -218,3 +220,69 @@ func (g *Git) AddRemote(name, url string) error {
218220
})
219221
return err
220222
}
223+
224+
func (g *Git) LastCommitChanges() (internalgit.Changes, error) {
225+
iter, err := g.repo.Log(&git.LogOptions{})
226+
if err != nil {
227+
return internalgit.Changes{}, err
228+
}
229+
230+
current, err := iter.Next()
231+
if err != nil {
232+
return internalgit.Changes{}, errors.WithMessage(err, "could not get current commit")
233+
}
234+
last, err := iter.Next()
235+
if err != nil {
236+
return internalgit.Changes{}, errors.WithMessage(err, "could not get last commit")
237+
}
238+
239+
currentTree, err := current.Tree()
240+
if err != nil {
241+
return internalgit.Changes{}, errors.WithMessage(err, "could not get current tree")
242+
}
243+
lastTree, err := last.Tree()
244+
if err != nil {
245+
return internalgit.Changes{}, errors.WithMessage(err, "could not get current tree")
246+
}
247+
248+
changes, err := lastTree.Diff(currentTree)
249+
if err != nil {
250+
return internalgit.Changes{}, errors.WithMessage(err, "could not get diff")
251+
}
252+
253+
additions := map[string][]byte{}
254+
deletions := []string{}
255+
for _, change := range changes {
256+
action, err := change.Action()
257+
if err != nil {
258+
return internalgit.Changes{}, errors.WithMessage(err, "could not get action")
259+
}
260+
261+
if action == merkletrie.Insert || action == merkletrie.Modify {
262+
_, to, err := change.Files()
263+
if err != nil {
264+
return internalgit.Changes{}, errors.WithMessage(err, "could not get files")
265+
}
266+
267+
reader, err := to.Reader()
268+
if err != nil {
269+
return internalgit.Changes{}, errors.WithMessage(err, "could not get reader")
270+
}
271+
bytes, err := io.ReadAll(reader)
272+
reader.Close()
273+
if err != nil {
274+
return internalgit.Changes{}, errors.WithMessage(err, "could not read file")
275+
}
276+
277+
additions[to.Name] = bytes
278+
} else if action == merkletrie.Delete {
279+
deletions = append(deletions, change.From.Name)
280+
}
281+
}
282+
283+
return internalgit.Changes{
284+
Additions: additions,
285+
Deletions: deletions,
286+
OldHash: last.Hash.String(),
287+
}, nil
288+
}

internal/multigitter/run.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type Runner struct {
6161
SkipPullRequest bool // If set, the script will run directly on the base-branch without creating any PR
6262
PushOnly bool // If set, the script will only publish the feature branch without creating a PR
6363
SkipRepository []string // A list of repositories that run will skip
64+
APIPush bool // Use the SCM API to commit and push the changes instead of git
6465
RegExIncludeRepository *regexp.Regexp
6566
RegExExcludeRepository *regexp.Regexp
6667

@@ -338,9 +339,28 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
338339

339340
log.Info("Pushing changes to remote")
340341
forcePush := featureBranchExist && r.ConflictStrategy == ConflictStrategyReplace
341-
err = sourceController.Push(ctx, remoteName, forcePush)
342-
if err != nil {
343-
return nil, errors.Wrap(err, "could not push changes")
342+
343+
if !r.APIPush {
344+
err = sourceController.Push(ctx, remoteName, forcePush)
345+
if err != nil {
346+
return nil, errors.Wrap(err, "could not push changes")
347+
}
348+
} else {
349+
commitChecker, hasCommitChecker := sourceController.(git.LastCommitChecker)
350+
changePusher, hasChangePusher := r.VersionController.(scm.ChangePusher)
351+
if !hasCommitChecker || !hasChangePusher {
352+
return nil, errors.New("the scm implementation does not support committing through the API")
353+
}
354+
355+
changes, err := commitChecker.LastCommitChanges()
356+
if err != nil {
357+
return nil, errors.Wrap(err, "could not get diff")
358+
}
359+
360+
err = changePusher.Push(ctx, repo, r.CommitMessage, changes, r.FeatureBranch, featureBranchExist, forcePush)
361+
if err != nil {
362+
return nil, err
363+
}
344364
}
345365

346366
if r.PushOnly {

internal/scm/changes.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package scm
2+
3+
import (
4+
"context"
5+
6+
"github.com/lindell/multi-gitter/internal/git"
7+
)
8+
9+
// ChangePusher makes a commit through the API
10+
type ChangePusher interface {
11+
Push(
12+
ctx context.Context,
13+
repo Repository,
14+
commitMessage string,
15+
change git.Changes,
16+
featureBranch string,
17+
branchExist bool,
18+
forcePush bool,
19+
) error
20+
}

0 commit comments

Comments
 (0)