Skip to content

Commit 0fcd89a

Browse files
authored
Don't try to open PR if one is already open (#34)
* Don't try to open PR if one is already open * Fix filtering on head branch * Switch to GetOwner() * Track skipped pull requests
1 parent 6f0030e commit 0fcd89a

File tree

5 files changed

+85
-22
lines changed

5 files changed

+85
-22
lines changed

auth/auth.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
// The go-github package satisfies this PullRequest service's interface in production
1515
type githubPullRequestService interface {
1616
Create(ctx context.Context, owner string, name string, pr *github.NewPullRequest) (*github.PullRequest, *github.Response, error)
17+
List(ctx context.Context, owner string, repo string, opts *github.PullRequestListOptions) ([]*github.PullRequest, *github.Response, error)
1718
}
1819

1920
// The go-github package satisfies this Repositories service's interface in production

mocks/mocks.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ func (m mockGithubPullRequestService) Create(ctx context.Context, owner, name st
5353
return m.PullRequest, m.Response, nil
5454
}
5555

56+
func (m mockGithubPullRequestService) List(ctx context.Context, owner string, repo string, opts *github.PullRequestListOptions) ([]*github.PullRequest, *github.Response, error) {
57+
return []*github.PullRequest{m.PullRequest}, m.Response, nil
58+
}
59+
5660
// This mocks the Repositories service in go-github that is used in production to call the associated Github endpoint
5761
type mockGithubRepositoriesService struct {
5862
Repository *github.Repository

repository/process.go

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,22 +78,9 @@ func processRepo(config *config.GitXargsConfig, repo *github.Repository) error {
7878
return commandErr
7979
}
8080

81-
// Commit any untracked files, modified or deleted files that resulted from script execution
82-
commitErr := commitLocalChanges(config, repositoryDir, worktree, repo, localRepository)
83-
if commitErr != nil {
84-
return commitErr
85-
}
86-
87-
// Push the local branch containing all of our changes from executing the supplied command
88-
pushBranchErr := pushLocalBranch(config, repo, localRepository)
89-
if pushBranchErr != nil {
90-
return pushBranchErr
91-
}
92-
93-
// Open a pull request on Github, of the recently pushed branch against the repository default branch
94-
openPullRequestErr := openPullRequest(config, repo, branchName.String())
95-
if openPullRequestErr != nil {
96-
return openPullRequestErr
81+
// Commit and push the changes to Git and open a PR
82+
if err := updateRepo(config, repositoryDir, worktree, repo, localRepository, branchName.String()); err != nil {
83+
return err
9784
}
9885

9986
return nil

repository/repo-operations.go

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,10 @@ func checkoutLocalBranch(config *config.GitXargsConfig, ref *plumbing.Reference,
210210
return branchName, nil
211211
}
212212

213-
// commitLocalChanges will check for any changes in worktree as a result of script execution, and if any are present,
214-
// add any untracked, deleted or modified files and create a commit using the supplied or default commit message.
215-
func commitLocalChanges(config *config.GitXargsConfig, repositoryDir string, worktree *git.Worktree, remoteRepository *github.Repository, localRepository *git.Repository) error {
213+
// updateRepo will check for any changes in worktree as a result of script execution, and if any are present,
214+
// add any untracked, deleted or modified files, create a commit using the supplied or default commit message,
215+
// push the code to the remote repo, and open a pull request.
216+
func updateRepo(config *config.GitXargsConfig, repositoryDir string, worktree *git.Worktree, remoteRepository *github.Repository, localRepository *git.Repository, branchName string) error {
216217
logger := logging.GetLogger("git-xargs")
217218

218219
status, statusErr := worktree.Status()
@@ -241,6 +242,32 @@ func commitLocalChanges(config *config.GitXargsConfig, repositoryDir string, wor
241242
return nil
242243
}
243244

245+
// Commit any untracked files, modified or deleted files that resulted from script execution
246+
commitErr := commitLocalChanges(status, config, repositoryDir, worktree, remoteRepository, localRepository)
247+
if commitErr != nil {
248+
return commitErr
249+
}
250+
251+
// Push the local branch containing all of our changes from executing the supplied command
252+
pushBranchErr := pushLocalBranch(config, remoteRepository, localRepository)
253+
if pushBranchErr != nil {
254+
return pushBranchErr
255+
}
256+
257+
// Open a pull request on Github, of the recently pushed branch against the repository default branch
258+
openPullRequestErr := openPullRequest(config, remoteRepository, branchName)
259+
if openPullRequestErr != nil {
260+
return openPullRequestErr
261+
}
262+
263+
return nil
264+
}
265+
266+
// commitLocalChanges will check for any changes in worktree as a result of script execution, and if any are present,
267+
// add any untracked, deleted or modified files and create a commit using the supplied or default commit message.
268+
func commitLocalChanges(status git.Status, config *config.GitXargsConfig, repositoryDir string, worktree *git.Worktree, remoteRepository *github.Repository, localRepository *git.Repository) error {
269+
logger := logging.GetLogger("git-xargs")
270+
244271
// If there are changes, we need to stage, add and commit them
245272
logger.WithFields(logrus.Fields{
246273
"Repo": remoteRepository.GetName(),
@@ -343,7 +370,6 @@ func pushLocalBranch(config *config.GitXargsConfig, remoteRepository *github.Rep
343370
// Attempt to open a pull request via the Github API, of the supplied branch specific to this tool, against the main
344371
// branch for the remote origin
345372
func openPullRequest(config *config.GitXargsConfig, repo *github.Repository, branch string) error {
346-
347373
logger := logging.GetLogger("git-xargs")
348374

349375
if config.DryRun || config.SkipPullRequests {
@@ -353,6 +379,33 @@ func openPullRequest(config *config.GitXargsConfig, repo *github.Repository, bra
353379
return nil
354380
}
355381

382+
repoDefaultBranch := repo.GetDefaultBranch()
383+
pullRequestAlreadyExists, err := pullRequestAlreadyExistsForBranch(config, repo, branch, repoDefaultBranch)
384+
385+
if err != nil {
386+
logger.WithFields(logrus.Fields{
387+
"Error": err,
388+
"Head": branch,
389+
"Base": repoDefaultBranch,
390+
}).Debug("Error listing pull requests")
391+
392+
// Track pull request open failure
393+
config.Stats.TrackSingle(stats.PullRequestOpenErr, repo)
394+
return errors.WithStackTrace(err)
395+
}
396+
397+
if pullRequestAlreadyExists {
398+
logger.WithFields(logrus.Fields{
399+
"Repo": repo.GetName(),
400+
"Head": branch,
401+
"Base": repoDefaultBranch,
402+
}).Debug("Pull request already exists for this branch, so skipping opening a pull request!")
403+
404+
// Track that we skipped opening a pull request
405+
config.Stats.TrackSingle(stats.PullRequestAlreadyExists, repo)
406+
return nil
407+
}
408+
356409
// If the user only supplies a commit message, use that for both the pull request title and descriptions,
357410
// unless they are provided separately
358411
titleToUse := config.PullRequestTitle
@@ -370,8 +423,6 @@ func openPullRequest(config *config.GitXargsConfig, repo *github.Repository, bra
370423
}
371424
}
372425

373-
repoDefaultBranch := repo.GetDefaultBranch()
374-
375426
// Configure pull request options that the Github client accepts when making calls to open new pull requests
376427
newPR := &github.NewPullRequest{
377428
Title: github.String(titleToUse),
@@ -405,3 +456,20 @@ func openPullRequest(config *config.GitXargsConfig, repo *github.Repository, bra
405456
config.Stats.TrackPullRequest(repo.GetName(), pr.GetHTMLURL())
406457
return nil
407458
}
459+
460+
// Returns true if a pull request already exists in the given repo for the given branch
461+
func pullRequestAlreadyExistsForBranch(config *config.GitXargsConfig, repo *github.Repository, branch string, repoDefaultBranch string) (bool, error) {
462+
opts := &github.PullRequestListOptions{
463+
// Filter pulls by head user or head organization and branch name in the format of user:ref-name or organization:ref-name
464+
// https://docs.github.com/en/rest/reference/pulls#list-pull-requests
465+
Head: fmt.Sprintf("%s:%s", *repo.GetOwner().Login, branch),
466+
Base: repoDefaultBranch,
467+
}
468+
469+
prs, _, err := config.GithubClient.PullRequests.List(context.Background(), *repo.GetOwner().Login, repo.GetName(), opts)
470+
if err != nil {
471+
return false, errors.WithStackTrace(err)
472+
}
473+
474+
return len(prs) > 0, nil
475+
}

stats/stats.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ const (
5454
RepoNotExists types.Event = "repo-not-exists"
5555
// PullRequestOpenErr denotes a repo whose pull request containing config changes could not be made successfully
5656
PullRequestOpenErr types.Event = "pull-request-open-error"
57+
// PullRequestAlreadyExists denotes a repo where the pull request already exists for the requested branch, so we didn't open a new one
58+
PullRequestAlreadyExists types.Event = "pull-request-already-exists"
5759
// CommitsMadeDirectlyToBranch denotes a repo whose local worktree changes were committed directly to the specified branch because the --skip-pull-requests flag was passed
5860
CommitsMadeDirectlyToBranch types.Event = "commits-made-directly-to-branch"
5961
//DirectCommitsPushedToRemoteBranch denotes a repo whose changes were pushed to the remote specified branch because the --skip-pull-requests flag was passed
@@ -84,6 +86,7 @@ var allEvents = []types.AnnotatedEvent{
8486
{Event: PushBranchSkipped, Description: "Repos whose local branch was not pushed because the --dry-run flag was set"},
8587
{Event: RepoNotExists, Description: "Repos that were supplied by user but don't exist (404'd) via Github API"},
8688
{Event: PullRequestOpenErr, Description: "Repos against which pull requests failed to be opened"},
89+
{Event: PullRequestAlreadyExists, Description: "Repos where opening a pull request was skipped because a pull request was already open"},
8790
{Event: CommitsMadeDirectlyToBranch, Description: "Repos whose local changes were committed directly to the specified branch because --skip-pull-requests was passed"},
8891
{Event: DirectCommitsPushedToRemoteBranch, Description: "Repos whose changes were pushed directly to the remote branch because --skip-pull-requests was passed"},
8992
{Event: BranchRemotePullFailed, Description: "Repos whose remote branches could not be successfully pulled"},

0 commit comments

Comments
 (0)