diff --git a/models/action.go b/models/action.go index b089870c741a7..95ccd7555a8e7 100644 --- a/models/action.go +++ b/models/action.go @@ -758,6 +758,8 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { case ActionDeleteBranch: // Delete Branch isHookEventPush = true + CloseActivePullRequests(pusher, repo, refName) + if err = PrepareWebhooks(repo, HookEventDelete, &api.DeletePayload{ Ref: refName, RefType: "branch", diff --git a/models/fixtures/pull_request.yml b/models/fixtures/pull_request.yml index d8313f9f90585..989282cb05b99 100644 --- a/models/fixtures/pull_request.yml +++ b/models/fixtures/pull_request.yml @@ -22,7 +22,7 @@ head_repo_id: 1 base_repo_id: 1 head_user_name: user1 - head_branch: branch2 + head_branch: develop base_branch: master merge_base: fedcba9876543210 has_merged: false diff --git a/models/issue.go b/models/issue.go index ddc7fa24af8a1..509d21fee7304 100644 --- a/models/issue.go +++ b/models/issue.go @@ -1803,6 +1803,15 @@ func (issue *Issue) getBlockingDependencies(e Engine) (issueDeps []*Issue, err e Find(&issueDeps) } +// Returns true if the issue has any open issue as its dependency. +func (issue *Issue) isBlocked(e Engine) (bool, error) { + count, err := e.Table("issue_dependency"). + Join("INNER", "issue", "issue.id = issue_dependency.issue_id"). + Where("dependency_id = ? AND issue.is_closed = ?", issue.ID, false). + Count() + return count > 0, err +} + // BlockedByDependencies finds all Dependencies an issue is blocked by func (issue *Issue) BlockedByDependencies() ([]*Issue, error) { return issue.getBlockedByDependencies(x) @@ -1812,3 +1821,8 @@ func (issue *Issue) BlockedByDependencies() ([]*Issue, error) { func (issue *Issue) BlockingDependencies() ([]*Issue, error) { return issue.getBlockingDependencies(x) } + +// IsBlocked returns true if the issue has any open issue as its dependency. +func (issue *Issue) IsBlocked() (bool, error) { + return issue.isBlocked(x) +} diff --git a/models/pull.go b/models/pull.go index 71a2439b2c335..2378254482664 100644 --- a/models/pull.go +++ b/models/pull.go @@ -1527,3 +1527,46 @@ func TestPullRequests() { func InitTestPullRequests() { go TestPullRequests() } + +// CloseActivePullRequests closes the issue of all the active pull requests associated with a branch +func CloseActivePullRequests(doer *User, repo *Repository, branchName string) { + baseprs, err := GetUnmergedPullRequestsByBaseInfo(repo.ID, branchName) + if err != nil { + log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repo.ID, branchName, err) + return + } + headprs, err := GetUnmergedPullRequestsByHeadInfo(repo.ID, branchName) + if err != nil { + log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repo.ID, branchName, err) + return + } + prs := append(baseprs, headprs...) + for statusChanged := true; statusChanged; { + statusChanged = false + for _, pr := range prs { + if err = pr.LoadIssue(); err != nil { + log.Error("LoadIssue: %v", err) + } + if pr.Issue.IsClosed { + continue + } + if blocked, err := pr.Issue.IsBlocked(); err != nil { + log.Error("IsBlocked: %v", err) + } else if !blocked { + err = pr.Issue.ChangeStatus(doer, true) + if err != nil { + log.Error("ChangeStatus: %v", err) + } else { + log.Trace("Issue [%d] status changed to closed", pr.IssueID) + statusChanged = true + } + } + } + } + + for _, pr := range prs { + if !pr.Issue.IsClosed { + log.Warn("Issue [%d] status change blocked by existing dependencies.", pr.Issue.ID) + } + } +} diff --git a/models/pull_test.go b/models/pull_test.go index 1dad664077711..53611a5389e5d 100644 --- a/models/pull_test.go +++ b/models/pull_test.go @@ -90,7 +90,7 @@ func TestPullRequestsOldest(t *testing.T) { func TestGetUnmergedPullRequest(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - pr, err := GetUnmergedPullRequest(1, 1, "branch2", "master") + pr, err := GetUnmergedPullRequest(1, 1, "develop", "master") assert.NoError(t, err) assert.Equal(t, int64(2), pr.ID) @@ -101,12 +101,12 @@ func TestGetUnmergedPullRequest(t *testing.T) { func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - prs, err := GetUnmergedPullRequestsByHeadInfo(1, "branch2") + prs, err := GetUnmergedPullRequestsByHeadInfo(1, "develop") assert.NoError(t, err) assert.Len(t, prs, 1) for _, pr := range prs { assert.Equal(t, int64(1), pr.HeadRepoID) - assert.Equal(t, "branch2", pr.HeadBranch) + assert.Equal(t, "develop", pr.HeadBranch) } } @@ -268,3 +268,77 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) { pr.Issue.Title = "[wip] " + original assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix()) } + +func closeActivePullRequests(t *testing.T, pr *PullRequest, repo *Repository, branchName string) { + assert.NoError(t, repo.GetOwner()) + + CloseActivePullRequests(repo.Owner, repo, branchName) + pr.Issue = nil + pr.LoadIssue() + assert.Equal(t, true, pr.Issue.IsClosed) +} + +func TestPullRequest_CloseActivePullRequests(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + // Delete head branch. + pr := AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) + pr.GetHeadRepo() + closeActivePullRequests(t, pr, pr.HeadRepo, pr.HeadBranch) + + // Reopen pull request. + assert.NoError(t, pr.Issue.ChangeStatus(pr.HeadRepo.Owner, false)) + + // Delete base branch. + pr = AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) + pr.GetBaseRepo() + closeActivePullRequests(t, pr, pr.BaseRepo, pr.BaseBranch) +} + +func createTestPullRequest(t *testing.T, repo *Repository, user *User, baseBranch string, headBranch string) *PullRequest { + prIssue := &Issue{ + RepoID: repo.ID, + Repo: repo, + Title: "test", + PosterID: user.ID, + Poster: user, + } + pr := &PullRequest{ + HeadRepoID: repo.ID, + BaseRepoID: repo.ID, + HeadRepo: repo, + BaseRepo: repo, + HeadUserName: user.Name, + HeadBranch: headBranch, + BaseBranch: baseBranch, + Type: PullRequestGitea, + } + labelIDs := make([]int64, 0) + uuids := make([]string, 0) + patch := make([]byte, 0) + assigneeIDs := make([]int64, 0) + err := NewPullRequest(repo, prIssue, labelIDs, uuids, pr, patch, assigneeIDs) + assert.NoError(t, err) + return pr +} + +func TestPullRequest_CloseActivePullRequestsDependent(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + // Create a test pull request. + repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User) + pr1 := AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) + pr2 := createTestPullRequest(t, repo, user, "develop", "master") + pr1.LoadIssue() + pr2.LoadIssue() + + // Add a dependency from pr2 to pr1. + err := CreateIssueDependency(user, pr2.Issue, pr1.Issue) + assert.NoError(t, err) + + closeActivePullRequests(t, pr2, pr2.BaseRepo, pr2.BaseBranch) + pr1.Issue = nil + pr1.LoadIssue() + assert.Equal(t, true, pr1.Issue.IsClosed) +}