Skip to content

Commit

Permalink
feat: added --conflict-strategy (#210)
Browse files Browse the repository at this point in the history
  • Loading branch information
lindell committed Nov 8, 2021
1 parent a463709 commit 5dfd6d9
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 76 deletions.
15 changes: 15 additions & 0 deletions cmd/cmd-run.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ func RunCmd() *cobra.Command {
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\".")
cmd.Flags().BoolP("interactive", "i", false, "Take manual decision before committing any change. Requires git to be installed.")
cmd.Flags().BoolP("dry-run", "d", false, "Run without pushing changes or creating pull requests.")
cmd.Flags().StringP("conflict-strategy", "", "skip", `What should happen if the branch already exist.
Available values:
skip: Skip making any changes to the existing branch and do not create a new pull request.
replace: Replace the existing content of the branch by force pushing any new changes, then reuse any existing pull request, or create a new one if none exist.
`)
_ = cmd.RegisterFlagCompletionFunc("conflict-strategy", func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"skip", "replace"}, cobra.ShellCompDirectiveNoFileComp
})
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)
Expand Down Expand Up @@ -74,6 +82,7 @@ func run(cmd *cobra.Command, args []string) error {
dryRun, _ := flag.GetBool("dry-run")
forkMode, _ := flag.GetBool("fork")
forkOwner, _ := flag.GetString("fork-owner")
conflictStrategyStr, _ := flag.GetString("conflict-strategy")
authorName, _ := flag.GetString("author-name")
authorEmail, _ := flag.GetString("author-email")
strOutput, _ := flag.GetString("output")
Expand Down Expand Up @@ -139,6 +148,11 @@ func run(cmd *cobra.Command, args []string) error {
return err
}

conflictStrategy, err := multigitter.ParseConflictStrategy(conflictStrategyStr)
if err != nil {
return err
}

// Set up signal listening to cancel the context and let started runs finish gracefully
ctx, cancel := context.WithCancel(context.Background())
c := make(chan os.Signal, 1)
Expand Down Expand Up @@ -174,6 +188,7 @@ func run(cmd *cobra.Command, args []string) error {
CommitAuthor: commitAuthor,
BaseBranch: baseBranchName,
Assignees: assignees,
ConflictStrategy: conflictStrategy,

Concurrent: concurrent,

Expand Down
10 changes: 8 additions & 2 deletions internal/git/cmdgit/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,14 @@ func (g *Git) BranchExist(remoteName, branchName string) (bool, error) {
}

// Push the committed changes to the remote
func (g *Git) Push(remoteName string) error {
cmd := exec.Command("git", "push", "--no-verify", remoteName, "HEAD")
func (g *Git) Push(remoteName string, force bool) error {
args := []string{"push", "--no-verify", remoteName}
if force {
args = append(args, "--force")
}
args = append(args, "HEAD")

cmd := exec.Command("git", args...)
_, err := g.run(cmd)
return err
}
Expand Down
3 changes: 2 additions & 1 deletion internal/git/gogit/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,10 @@ func (g *Git) BranchExist(remoteName, branchName string) (bool, error) {
}

// Push the committed changes to the remote
func (g *Git) Push(remoteName string) error {
func (g *Git) Push(remoteName string, force bool) error {
return g.repo.Push(&git.PushOptions{
RemoteName: remoteName,
Force: force,
})
}

Expand Down
50 changes: 36 additions & 14 deletions internal/multigitter/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type VersionController interface {
GetRepositories(ctx context.Context) ([]scm.Repository, error)
CreatePullRequest(ctx context.Context, repo scm.Repository, prRepo scm.Repository, newPR scm.NewPullRequest) (scm.PullRequest, error)
GetPullRequests(ctx context.Context, branchName string) ([]scm.PullRequest, error)
GetOpenPullRequest(ctx context.Context, repo scm.Repository, branchName string) (scm.PullRequest, error)
MergePullRequest(ctx context.Context, pr scm.PullRequest) error
ClosePullRequest(ctx context.Context, pr scm.PullRequest) error
ForkRepository(ctx context.Context, repo scm.Repository, newOwner string) (scm.Repository, error)
Expand Down Expand Up @@ -59,6 +60,8 @@ type Runner struct {
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

ConflictStrategy ConflictStrategy // Defines what will happen if a branch does already exist

Interactive bool // If set, interactive mode is activated and the user will be asked to verify every change

CreateGit func(dir string) Git
Expand Down Expand Up @@ -275,17 +278,20 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
remoteName = "fork"
}

// Determine if a branch already exist and (depending on the conflict strategy) skip making changes
featureBranchExist := false
if !r.SkipPullRequest {
featureBranchExist, err := sourceController.BranchExist(remoteName, r.FeatureBranch)
featureBranchExist, err = sourceController.BranchExist(remoteName, r.FeatureBranch)
if err != nil {
return nil, errors.Wrap(err, "could not verify if branch already exist")
} else if featureBranchExist {
} else if featureBranchExist && r.ConflictStrategy == ConflictStrategySkip {
return nil, errBranchExist
}
}

log.Info("Pushing changes to remote")
err = sourceController.Push(remoteName)
forcePush := featureBranchExist && r.ConflictStrategy == ConflictStrategyReplace
err = sourceController.Push(remoteName, forcePush)
if err != nil {
return nil, errors.Wrap(err, "could not push changes")
}
Expand All @@ -294,17 +300,33 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
return nil, nil
}

log.Info("Creating pull request")
pr, err := r.VersionController.CreatePullRequest(ctx, repo, prRepo, scm.NewPullRequest{
Title: r.PullRequestTitle,
Body: r.PullRequestBody,
Head: r.FeatureBranch,
Base: baseBranch,
Reviewers: getReviewers(r.Reviewers, r.MaxReviewers),
Assignees: r.Assignees,
})
if err != nil {
return nil, err
// Fetching any potentially existing pull request
var existingPullRequest scm.PullRequest = nil
if featureBranchExist {
pr, err := r.VersionController.GetOpenPullRequest(ctx, repo, r.FeatureBranch)
if err != nil {
return nil, err
}
existingPullRequest = pr
}

var pr scm.PullRequest
if existingPullRequest != nil {
log.Info("Skip creating pull requests since one is already open")
pr = existingPullRequest
} else {
log.Info("Creating pull request")
pr, err = r.VersionController.CreatePullRequest(ctx, repo, prRepo, scm.NewPullRequest{
Title: r.PullRequestTitle,
Body: r.PullRequestBody,
Head: r.FeatureBranch,
Base: baseBranch,
Reviewers: getReviewers(r.Reviewers, r.MaxReviewers),
Assignees: r.Assignees,
})
if err != nil {
return nil, err
}
}

return pr, nil
Expand Down
24 changes: 23 additions & 1 deletion internal/multigitter/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Git interface {
Changes() (bool, error)
Commit(commitAuthor *git.CommitAuthor, commitMessage string) error
BranchExist(remoteName, branchName string) (bool, error)
Push(remoteName string) error
Push(remoteName string, force bool) error
AddRemote(name, url string) error
}

Expand All @@ -47,3 +47,25 @@ func getStackTrace(err error) string {
}
return ""
}

// ConflictStrategy define how a conflict of an already existing branch should be handled
type ConflictStrategy int

const (
// ConflictStrategySkip will skip the run for if the branch does already exist
ConflictStrategySkip ConflictStrategy = iota + 1
// ConflictStrategyReplace will ignore any existing branch and replace it with new changes
ConflictStrategyReplace
)

// ParseConflictStrategy parses a conflict strategy from a string
func ParseConflictStrategy(str string) (ConflictStrategy, error) {
switch str {
default:
return ConflictStrategy(0), fmt.Errorf("could not parse \"%s\" as conflict strategy", str)
case "skip":
return ConflictStrategySkip, nil
case "replace":
return ConflictStrategyReplace, nil
}
}
70 changes: 51 additions & 19 deletions internal/scm/bitbucketserver/bitbucket_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,44 +318,53 @@ func (b *BitbucketServer) GetPullRequests(ctx context.Context, branchName string

var prs []scm.PullRequest
for _, repo := range repositories {
pr, getPullRequestErr := b.getPullRequest(client, branchName, repo)
pr, getPullRequestErr := b.getPullRequest(client, branchName, repo.Project.Key, repo.Slug)
if getPullRequestErr != nil {
return nil, getPullRequestErr
}
if pr == nil {
continue
}

status, pullRequestStatusErr := b.pullRequestStatus(client, repo, pr)
if pullRequestStatusErr != nil {
return nil, pullRequestStatusErr
convertedPR, err := b.convertPullRequest(client, repo.Project.Key, repo.Slug, branchName, pr)
if err != nil {
return nil, err
}

prs = append(prs, pullRequest{
repoName: repo.Slug,
project: repo.Project.Key,
branchName: branchName,
prProject: pr.FromRef.Repository.Project.Key,
prRepoName: pr.FromRef.Repository.Slug,
number: pr.ID,
version: pr.Version,
guiURL: pr.Links.Self[0].Href,
status: status,
})
prs = append(prs, convertedPR)
}

return prs, nil
}

func (b *BitbucketServer) pullRequestStatus(client *bitbucketv1.APIClient, repo *bitbucketv1.Repository, pr *bitbucketv1.PullRequest) (scm.PullRequestStatus, error) {
func (b *BitbucketServer) convertPullRequest(client *bitbucketv1.APIClient, project, repoName, branchName string, pr *bitbucketv1.PullRequest) (pullRequest, error) {
status, err := b.pullRequestStatus(client, project, repoName, pr)
if err != nil {
return pullRequest{}, err
}

return pullRequest{
repoName: repoName,
project: project,
branchName: branchName,
prProject: pr.FromRef.Repository.Project.Key,
prRepoName: pr.FromRef.Repository.Slug,
number: pr.ID,
version: pr.Version,
guiURL: pr.Links.Self[0].Href,
status: status,
}, nil
}

func (b *BitbucketServer) pullRequestStatus(client *bitbucketv1.APIClient, project, repoName string, pr *bitbucketv1.PullRequest) (scm.PullRequestStatus, error) {
switch pr.State {
case stateMerged:
return scm.PullRequestStatusMerged, nil
case stateDeclined:
return scm.PullRequestStatusClosed, nil
}

response, err := client.DefaultApi.CanMerge(repo.Project.Key, repo.Slug, int64(pr.ID))
response, err := client.DefaultApi.CanMerge(project, repoName, int64(pr.ID))
if err != nil {
return scm.PullRequestStatusUnknown, err
}
Expand All @@ -377,12 +386,12 @@ func (b *BitbucketServer) pullRequestStatus(client *bitbucketv1.APIClient, repo
return scm.PullRequestStatusSuccess, nil
}

func (b *BitbucketServer) getPullRequest(client *bitbucketv1.APIClient, branchName string, repo *bitbucketv1.Repository) (*bitbucketv1.PullRequest, error) {
func (b *BitbucketServer) getPullRequest(client *bitbucketv1.APIClient, branchName, project, repoName string) (*bitbucketv1.PullRequest, error) {
params := map[string]interface{}{"start": 0, "limit": 25}

var pullRequests []bitbucketv1.PullRequest
for {
response, err := client.DefaultApi.GetPullRequestsPage(repo.Project.Key, repo.Slug, params)
response, err := client.DefaultApi.GetPullRequestsPage(project, repoName, params)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -411,6 +420,29 @@ func (b *BitbucketServer) getPullRequest(client *bitbucketv1.APIClient, branchNa
return nil, nil
}

// GetOpenPullRequest gets a pull request for one specific repository
func (b *BitbucketServer) GetOpenPullRequest(ctx context.Context, repo scm.Repository, branchName string) (scm.PullRequest, error) {
r := repo.(repository)

client := newClient(ctx, b.config)

pr, err := b.getPullRequest(client, branchName, r.project, r.name)
if err != nil {
return nil, err
}

if pr == nil {
return nil, nil
}

convertedPR, err := b.convertPullRequest(client, r.project, r.name, branchName, pr)
if err != nil {
return nil, err
}

return convertedPR, nil
}

// MergePullRequest Merges a pull request, the pr parameter will always originate from the same package
func (b *BitbucketServer) MergePullRequest(ctx context.Context, pr scm.PullRequest) error {
bitbucketPR := pr.(pullRequest)
Expand Down

0 comments on commit 5dfd6d9

Please sign in to comment.