* fix pull view when head repository or head branch missed and close related pull requests when delete branch * fix pull view broken when head repository deleted * close pull requests when head repositories deleted * Add tests for broken pull request head repository or branch * fix typo * ignore special error when close pull request Co-authored-by: Lauris BH <lauris@nix.lv>tags/v1.21.12.1
| @@ -106,3 +106,57 @@ func TestPullCreate_TitleEscape(t *testing.T) { | |||||
| assert.Equal(t, "<u>XSS PR</u>", titleHTML) | assert.Equal(t, "<u>XSS PR</u>", titleHTML) | ||||
| }) | }) | ||||
| } | } | ||||
| func testUIDeleteBranch(t *testing.T, session *TestSession, ownerName, repoName, branchName string) { | |||||
| relURL := "/" + path.Join(ownerName, repoName, "branches") | |||||
| req := NewRequest(t, "GET", relURL) | |||||
| resp := session.MakeRequest(t, req, http.StatusOK) | |||||
| htmlDoc := NewHTMLParser(t, resp.Body) | |||||
| req = NewRequestWithValues(t, "POST", relURL+"/delete", map[string]string{ | |||||
| "_csrf": getCsrf(t, htmlDoc.doc), | |||||
| "name": branchName, | |||||
| }) | |||||
| session.MakeRequest(t, req, http.StatusOK) | |||||
| } | |||||
| func testDeleteRepository(t *testing.T, session *TestSession, ownerName, repoName string) { | |||||
| relURL := "/" + path.Join(ownerName, repoName, "settings") | |||||
| req := NewRequest(t, "GET", relURL) | |||||
| resp := session.MakeRequest(t, req, http.StatusOK) | |||||
| htmlDoc := NewHTMLParser(t, resp.Body) | |||||
| req = NewRequestWithValues(t, "POST", relURL+"?action=delete", map[string]string{ | |||||
| "_csrf": getCsrf(t, htmlDoc.doc), | |||||
| "repo_name": repoName, | |||||
| }) | |||||
| session.MakeRequest(t, req, http.StatusFound) | |||||
| } | |||||
| func TestPullBranchDelete(t *testing.T) { | |||||
| onGiteaRun(t, func(t *testing.T, u *url.URL) { | |||||
| defer prepareTestEnv(t)() | |||||
| session := loginUser(t, "user1") | |||||
| testRepoFork(t, session, "user2", "repo1", "user1", "repo1") | |||||
| testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusFound) | |||||
| testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n") | |||||
| resp := testPullCreate(t, session, "user1", "repo1", "master1", "This is a pull title") | |||||
| // check the redirected URL | |||||
| url := resp.HeaderMap.Get("Location") | |||||
| assert.Regexp(t, "^/user2/repo1/pulls/[0-9]*$", url) | |||||
| req := NewRequest(t, "GET", url) | |||||
| session.MakeRequest(t, req, http.StatusOK) | |||||
| // delete head branch and confirm pull page is ok | |||||
| testUIDeleteBranch(t, session, "user1", "repo1", "master1") | |||||
| req = NewRequest(t, "GET", url) | |||||
| session.MakeRequest(t, req, http.StatusOK) | |||||
| // delete head repository and confirm pull page is ok | |||||
| testDeleteRepository(t, session, "user1", "repo1") | |||||
| req = NewRequest(t, "GET", url) | |||||
| session.MakeRequest(t, req, http.StatusOK) | |||||
| }) | |||||
| } | |||||
| @@ -67,7 +67,11 @@ type PullRequest struct { | |||||
| // MustHeadUserName returns the HeadRepo's username if failed return blank | // MustHeadUserName returns the HeadRepo's username if failed return blank | ||||
| func (pr *PullRequest) MustHeadUserName() string { | func (pr *PullRequest) MustHeadUserName() string { | ||||
| if err := pr.LoadHeadRepo(); err != nil { | if err := pr.LoadHeadRepo(); err != nil { | ||||
| log.Error("LoadHeadRepo: %v", err) | |||||
| if !IsErrRepoNotExist(err) { | |||||
| log.Error("LoadHeadRepo: %v", err) | |||||
| } else { | |||||
| log.Warn("LoadHeadRepo %d but repository does not exist: %v", pr.HeadRepoID, err) | |||||
| } | |||||
| return "" | return "" | ||||
| } | } | ||||
| return pr.HeadRepo.OwnerName | return pr.HeadRepo.OwnerName | ||||
| @@ -495,9 +495,18 @@ func PushUpdate(repo *models.Repository, branch string, opts PushUpdateOptions) | |||||
| return err | return err | ||||
| } | } | ||||
| log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) | |||||
| if !isDelRef { | |||||
| if err = models.RemoveDeletedBranch(repo.ID, opts.Branch); err != nil { | |||||
| log.Error("models.RemoveDeletedBranch %s/%s failed: %v", repo.ID, opts.Branch, err) | |||||
| } | |||||
| log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) | |||||
| go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID) | |||||
| go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID) | |||||
| // close all related pulls | |||||
| } else if err = pull_service.CloseBranchPulls(pusher, repo.ID, branch); err != nil { | |||||
| log.Error("close related pull request failed: %v", err) | |||||
| } | |||||
| if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil { | if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil { | ||||
| log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err) | log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err) | ||||
| @@ -544,11 +553,14 @@ func PushUpdates(repo *models.Repository, optsList []*PushUpdateOptions) error { | |||||
| if err = models.RemoveDeletedBranch(repo.ID, opts.Branch); err != nil { | if err = models.RemoveDeletedBranch(repo.ID, opts.Branch); err != nil { | ||||
| log.Error("models.RemoveDeletedBranch %s/%s failed: %v", repo.ID, opts.Branch, err) | log.Error("models.RemoveDeletedBranch %s/%s failed: %v", repo.ID, opts.Branch, err) | ||||
| } | } | ||||
| } | |||||
| log.Trace("TriggerTask '%s/%s' by %s", repo.Name, opts.Branch, pusher.Name) | |||||
| log.Trace("TriggerTask '%s/%s' by %s", repo.Name, opts.Branch, pusher.Name) | |||||
| go pull_service.AddTestPullRequestTask(pusher, repo.ID, opts.Branch, true, opts.OldCommitID, opts.NewCommitID) | |||||
| go pull_service.AddTestPullRequestTask(pusher, repo.ID, opts.Branch, true, opts.OldCommitID, opts.NewCommitID) | |||||
| // close all related pulls | |||||
| } else if err = pull_service.CloseBranchPulls(pusher, repo.ID, opts.Branch); err != nil { | |||||
| log.Error("close related pull request failed: %v", err) | |||||
| } | |||||
| if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil { | if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil { | ||||
| log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err) | log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err) | ||||
| @@ -343,19 +343,6 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare | |||||
| setMergeTarget(ctx, pull) | setMergeTarget(ctx, pull) | ||||
| divergence, err := pull_service.GetDiverging(pull) | |||||
| if err != nil { | |||||
| ctx.ServerError("GetDiverging", err) | |||||
| return nil | |||||
| } | |||||
| ctx.Data["Divergence"] = divergence | |||||
| allowUpdate, err := pull_service.IsUserAllowedToUpdate(pull, ctx.User) | |||||
| if err != nil { | |||||
| ctx.ServerError("IsUserAllowedToUpdate", err) | |||||
| return nil | |||||
| } | |||||
| ctx.Data["UpdateAllowed"] = allowUpdate | |||||
| if err := pull.LoadProtectedBranch(); err != nil { | if err := pull.LoadProtectedBranch(); err != nil { | ||||
| ctx.ServerError("LoadProtectedBranch", err) | ctx.ServerError("LoadProtectedBranch", err) | ||||
| return nil | return nil | ||||
| @@ -392,6 +379,22 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare | |||||
| } | } | ||||
| } | } | ||||
| if headBranchExist { | |||||
| allowUpdate, err := pull_service.IsUserAllowedToUpdate(pull, ctx.User) | |||||
| if err != nil { | |||||
| ctx.ServerError("IsUserAllowedToUpdate", err) | |||||
| return nil | |||||
| } | |||||
| ctx.Data["UpdateAllowed"] = allowUpdate | |||||
| divergence, err := pull_service.GetDiverging(pull) | |||||
| if err != nil { | |||||
| ctx.ServerError("GetDiverging", err) | |||||
| return nil | |||||
| } | |||||
| ctx.Data["Divergence"] = divergence | |||||
| } | |||||
| sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName()) | sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName()) | ||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err) | ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err) | ||||
| @@ -355,3 +355,79 @@ func PushToBaseRepo(pr *models.PullRequest) (err error) { | |||||
| return nil | return nil | ||||
| } | } | ||||
| type errlist []error | |||||
| func (errs errlist) Error() string { | |||||
| if len(errs) > 0 { | |||||
| var buf strings.Builder | |||||
| for i, err := range errs { | |||||
| if i > 0 { | |||||
| buf.WriteString(", ") | |||||
| } | |||||
| buf.WriteString(err.Error()) | |||||
| } | |||||
| return buf.String() | |||||
| } | |||||
| return "" | |||||
| } | |||||
| // CloseBranchPulls close all the pull requests who's head branch is the branch | |||||
| func CloseBranchPulls(doer *models.User, repoID int64, branch string) error { | |||||
| prs, err := models.GetUnmergedPullRequestsByHeadInfo(repoID, branch) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| prs2, err := models.GetUnmergedPullRequestsByBaseInfo(repoID, branch) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| prs = append(prs, prs2...) | |||||
| if err := models.PullRequestList(prs).LoadAttributes(); err != nil { | |||||
| return err | |||||
| } | |||||
| var errs errlist | |||||
| for _, pr := range prs { | |||||
| if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !models.IsErrIssueWasClosed(err) { | |||||
| errs = append(errs, err) | |||||
| } | |||||
| } | |||||
| if len(errs) > 0 { | |||||
| return errs | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // CloseRepoBranchesPulls close all pull requests which head branches are in the given repository | |||||
| func CloseRepoBranchesPulls(doer *models.User, repo *models.Repository) error { | |||||
| branches, err := git.GetBranchesByPath(repo.RepoPath()) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| var errs errlist | |||||
| for _, branch := range branches { | |||||
| prs, err := models.GetUnmergedPullRequestsByHeadInfo(repo.ID, branch.Name) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| if err = models.PullRequestList(prs).LoadAttributes(); err != nil { | |||||
| return err | |||||
| } | |||||
| for _, pr := range prs { | |||||
| if err = issue_service.ChangeStatus(pr.Issue, doer, true); err != nil && !models.IsErrIssueWasClosed(err) { | |||||
| errs = append(errs, err) | |||||
| } | |||||
| } | |||||
| } | |||||
| if len(errs) > 0 { | |||||
| return errs | |||||
| } | |||||
| return nil | |||||
| } | |||||
| @@ -11,6 +11,7 @@ import ( | |||||
| "code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
| "code.gitea.io/gitea/modules/notification" | "code.gitea.io/gitea/modules/notification" | ||||
| repo_module "code.gitea.io/gitea/modules/repository" | repo_module "code.gitea.io/gitea/modules/repository" | ||||
| pull_service "code.gitea.io/gitea/services/pull" | |||||
| ) | ) | ||||
| // CreateRepository creates a repository for the user/organization. | // CreateRepository creates a repository for the user/organization. | ||||
| @@ -49,6 +50,10 @@ func ForkRepository(doer, u *models.User, oldRepo *models.Repository, name, desc | |||||
| // DeleteRepository deletes a repository for a user or organization. | // DeleteRepository deletes a repository for a user or organization. | ||||
| func DeleteRepository(doer *models.User, repo *models.Repository) error { | func DeleteRepository(doer *models.User, repo *models.Repository) error { | ||||
| if err := pull_service.CloseRepoBranchesPulls(doer, repo); err != nil { | |||||
| log.Error("CloseRepoBranchesPulls failed: %v", err) | |||||
| } | |||||
| if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil { | if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil { | ||||
| return err | return err | ||||
| } | } | ||||