* add Divergence * add Update Button * first working version * re-use code * split raw merge commands and db-change functions (notify, cache, ...) * use rawMerge (remove redundant code) * own function to get Diverging of PRs * use FlashError * correct Error Msg * hook is triggerd ... so remove comment * add "branch2" to "user2/repo1" because it unit-test "TestPullView_ReviewerMissed" use it but dont exist jet :/ * move GetPerm to IsUserAllowedToUpdate * add Flash Success MSG * imprufe code - remove useless js chage * fix-lint * TEST: add PullRequest ID:5 Repo: user2/repo1 Base: branch1 Head: pr-to-update * correct comments * make PR5 outdated * fix Tests * WIP: add pull update test * update revs * update locales * working TEST * update UI * misspell * change style * add 1s delay so rev exist * move row up (before merge row) * fix lint nit * UI remove divider * Update style * nits * do it right * introduce IsSameRepo * remove useless check Co-authored-by: Lauris BH <lauris@nix.lv>tags/v1.21.12.1
| @@ -134,7 +134,7 @@ func TestAPISearchIssue(t *testing.T) { | |||||
| var apiIssues []*api.Issue | var apiIssues []*api.Issue | ||||
| DecodeJSON(t, resp, &apiIssues) | DecodeJSON(t, resp, &apiIssues) | ||||
| assert.Len(t, apiIssues, 8) | |||||
| assert.Len(t, apiIssues, 9) | |||||
| query := url.Values{} | query := url.Values{} | ||||
| query.Add("token", token) | query.Add("token", token) | ||||
| @@ -142,7 +142,7 @@ func TestAPISearchIssue(t *testing.T) { | |||||
| req = NewRequest(t, "GET", link.String()) | req = NewRequest(t, "GET", link.String()) | ||||
| resp = session.MakeRequest(t, req, http.StatusOK) | resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| DecodeJSON(t, resp, &apiIssues) | DecodeJSON(t, resp, &apiIssues) | ||||
| assert.Len(t, apiIssues, 8) | |||||
| assert.Len(t, apiIssues, 9) | |||||
| query.Add("state", "closed") | query.Add("state", "closed") | ||||
| link.RawQuery = query.Encode() | link.RawQuery = query.Encode() | ||||
| @@ -163,5 +163,5 @@ func TestAPISearchIssue(t *testing.T) { | |||||
| req = NewRequest(t, "GET", link.String()) | req = NewRequest(t, "GET", link.String()) | ||||
| resp = session.MakeRequest(t, req, http.StatusOK) | resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| DecodeJSON(t, resp, &apiIssues) | DecodeJSON(t, resp, &apiIssues) | ||||
| assert.Len(t, apiIssues, 0) | |||||
| assert.Len(t, apiIssues, 1) | |||||
| } | } | ||||
| @@ -1 +1,3 @@ | |||||
| 65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/master | 65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/master | ||||
| 985f0301dba5e7b34be866819cd15ad3d8f508ee refs/heads/branch2 | |||||
| 62fb502a7172d4453f0322a2cc85bddffa57f07a refs/heads/pr-to-update | |||||
| @@ -0,0 +1 @@ | |||||
| 985f0301dba5e7b34be866819cd15ad3d8f508ee | |||||
| @@ -0,0 +1 @@ | |||||
| 62fb502a7172d4453f0322a2cc85bddffa57f07a | |||||
| @@ -0,0 +1 @@ | |||||
| 4a357436d925b5c974181ff12a994538ddc5a269 | |||||
| @@ -0,0 +1 @@ | |||||
| 62fb502a7172d4453f0322a2cc85bddffa57f07a | |||||
| @@ -0,0 +1,136 @@ | |||||
| // Copyright 2020 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package integrations | |||||
| import ( | |||||
| "fmt" | |||||
| "net/url" | |||||
| "testing" | |||||
| "time" | |||||
| "code.gitea.io/gitea/models" | |||||
| "code.gitea.io/gitea/modules/repofiles" | |||||
| repo_module "code.gitea.io/gitea/modules/repository" | |||||
| pull_service "code.gitea.io/gitea/services/pull" | |||||
| repo_service "code.gitea.io/gitea/services/repository" | |||||
| "github.com/stretchr/testify/assert" | |||||
| ) | |||||
| func TestPullUpdate(t *testing.T) { | |||||
| onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { | |||||
| //Create PR to test | |||||
| user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) | |||||
| org26 := models.AssertExistsAndLoadBean(t, &models.User{ID: 26}).(*models.User) | |||||
| pr := createOutdatedPR(t, user, org26) | |||||
| //Test GetDiverging | |||||
| diffCount, err := pull_service.GetDiverging(pr) | |||||
| assert.NoError(t, err) | |||||
| assert.EqualValues(t, 1, diffCount.Behind) | |||||
| assert.EqualValues(t, 1, diffCount.Ahead) | |||||
| message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch) | |||||
| err = pull_service.Update(pr, user, message) | |||||
| assert.NoError(t, err) | |||||
| //Test GetDiverging after update | |||||
| diffCount, err = pull_service.GetDiverging(pr) | |||||
| assert.NoError(t, err) | |||||
| assert.EqualValues(t, 0, diffCount.Behind) | |||||
| assert.EqualValues(t, 2, diffCount.Ahead) | |||||
| }) | |||||
| } | |||||
| func createOutdatedPR(t *testing.T, actor, forkOrg *models.User) *models.PullRequest { | |||||
| baseRepo, err := repo_service.CreateRepository(actor, actor, models.CreateRepoOptions{ | |||||
| Name: "repo-pr-update", | |||||
| Description: "repo-tmp-pr-update description", | |||||
| AutoInit: true, | |||||
| Gitignores: "C,C++", | |||||
| License: "MIT", | |||||
| Readme: "Default", | |||||
| IsPrivate: false, | |||||
| }) | |||||
| assert.NoError(t, err) | |||||
| assert.NotEmpty(t, baseRepo) | |||||
| headRepo, err := repo_module.ForkRepository(actor, forkOrg, baseRepo, "repo-pr-update", "desc") | |||||
| assert.NoError(t, err) | |||||
| assert.NotEmpty(t, headRepo) | |||||
| //create a commit on base Repo | |||||
| _, err = repofiles.CreateOrUpdateRepoFile(baseRepo, actor, &repofiles.UpdateRepoFileOptions{ | |||||
| TreePath: "File_A", | |||||
| Message: "Add File A", | |||||
| Content: "File A", | |||||
| IsNewFile: true, | |||||
| OldBranch: "master", | |||||
| NewBranch: "master", | |||||
| Author: &repofiles.IdentityOptions{ | |||||
| Name: actor.Name, | |||||
| Email: actor.Email, | |||||
| }, | |||||
| Committer: &repofiles.IdentityOptions{ | |||||
| Name: actor.Name, | |||||
| Email: actor.Email, | |||||
| }, | |||||
| Dates: &repofiles.CommitDateOptions{ | |||||
| Author: time.Now(), | |||||
| Committer: time.Now(), | |||||
| }, | |||||
| }) | |||||
| assert.NoError(t, err) | |||||
| //create a commit on head Repo | |||||
| _, err = repofiles.CreateOrUpdateRepoFile(headRepo, actor, &repofiles.UpdateRepoFileOptions{ | |||||
| TreePath: "File_B", | |||||
| Message: "Add File on PR branch", | |||||
| Content: "File B", | |||||
| IsNewFile: true, | |||||
| OldBranch: "master", | |||||
| NewBranch: "newBranch", | |||||
| Author: &repofiles.IdentityOptions{ | |||||
| Name: actor.Name, | |||||
| Email: actor.Email, | |||||
| }, | |||||
| Committer: &repofiles.IdentityOptions{ | |||||
| Name: actor.Name, | |||||
| Email: actor.Email, | |||||
| }, | |||||
| Dates: &repofiles.CommitDateOptions{ | |||||
| Author: time.Now(), | |||||
| Committer: time.Now(), | |||||
| }, | |||||
| }) | |||||
| assert.NoError(t, err) | |||||
| //create Pull | |||||
| pullIssue := &models.Issue{ | |||||
| RepoID: baseRepo.ID, | |||||
| Title: "Test Pull -to-update-", | |||||
| PosterID: actor.ID, | |||||
| Poster: actor, | |||||
| IsPull: true, | |||||
| } | |||||
| pullRequest := &models.PullRequest{ | |||||
| HeadRepoID: headRepo.ID, | |||||
| BaseRepoID: baseRepo.ID, | |||||
| HeadBranch: "newBranch", | |||||
| BaseBranch: "master", | |||||
| HeadRepo: headRepo, | |||||
| BaseRepo: baseRepo, | |||||
| Type: models.PullRequestGitea, | |||||
| } | |||||
| err = pull_service.NewPullRequest(baseRepo, pullIssue, nil, nil, pullRequest, nil) | |||||
| assert.NoError(t, err) | |||||
| issue := models.AssertExistsAndLoadBean(t, &models.Issue{Title: "Test Pull -to-update-"}).(*models.Issue) | |||||
| pr, err := models.GetPullRequestByIssueID(issue.ID) | |||||
| assert.NoError(t, err) | |||||
| return pr | |||||
| } | |||||
| @@ -56,9 +56,9 @@ func TestRepoActivity(t *testing.T) { | |||||
| list = htmlDoc.doc.Find("#merged-pull-requests").Next().Find("p.desc") | list = htmlDoc.doc.Find("#merged-pull-requests").Next().Find("p.desc") | ||||
| assert.Len(t, list.Nodes, 1) | assert.Len(t, list.Nodes, 1) | ||||
| // Should be 2 merged proposed pull requests | |||||
| // Should be 3 merged proposed pull requests | |||||
| list = htmlDoc.doc.Find("#proposed-pull-requests").Next().Find("p.desc") | list = htmlDoc.doc.Find("#proposed-pull-requests").Next().Find("p.desc") | ||||
| assert.Len(t, list.Nodes, 2) | |||||
| assert.Len(t, list.Nodes, 3) | |||||
| // Should be 3 new issues | // Should be 3 new issues | ||||
| list = htmlDoc.doc.Find("#new-issues").Next().Find("p.desc") | list = htmlDoc.doc.Find("#new-issues").Next().Find("p.desc") | ||||
| @@ -122,3 +122,15 @@ | |||||
| created_unix: 946684830 | created_unix: 946684830 | ||||
| updated_unix: 999307200 | updated_unix: 999307200 | ||||
| deadline_unix: 1019307200 | deadline_unix: 1019307200 | ||||
| - | |||||
| id: 11 | |||||
| repo_id: 1 | |||||
| index: 5 | |||||
| poster_id: 1 | |||||
| name: pull5 | |||||
| content: content for the a pull request | |||||
| is_closed: false | |||||
| is_pull: true | |||||
| created_unix: 1579194806 | |||||
| updated_unix: 1579194806 | |||||
| @@ -49,4 +49,17 @@ | |||||
| head_branch: branch1 | head_branch: branch1 | ||||
| base_branch: master | base_branch: master | ||||
| merge_base: abcdef1234567890 | merge_base: abcdef1234567890 | ||||
| has_merged: false | |||||
| has_merged: false | |||||
| - | |||||
| id: 5 # this PR is outdated (one commit behind branch1 ) | |||||
| type: 0 # gitea pull request | |||||
| status: 2 # mergable | |||||
| issue_id: 11 | |||||
| index: 5 | |||||
| head_repo_id: 1 | |||||
| base_repo_id: 1 | |||||
| head_branch: pr-to-update | |||||
| base_branch: branch1 | |||||
| merge_base: 1234567890abcdef | |||||
| has_merged: false | |||||
| @@ -7,7 +7,7 @@ | |||||
| is_private: false | is_private: false | ||||
| num_issues: 2 | num_issues: 2 | ||||
| num_closed_issues: 1 | num_closed_issues: 1 | ||||
| num_pulls: 2 | |||||
| num_pulls: 3 | |||||
| num_closed_pulls: 0 | num_closed_pulls: 0 | ||||
| num_milestones: 3 | num_milestones: 3 | ||||
| num_closed_milestones: 1 | num_closed_milestones: 1 | ||||
| @@ -276,8 +276,8 @@ func TestIssue_SearchIssueIDsByKeyword(t *testing.T) { | |||||
| total, ids, err = SearchIssueIDsByKeyword("for", []int64{1}, 10, 0) | total, ids, err = SearchIssueIDsByKeyword("for", []int64{1}, 10, 0) | ||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| assert.EqualValues(t, 4, total) | |||||
| assert.EqualValues(t, []int64{1, 2, 3, 5}, ids) | |||||
| assert.EqualValues(t, 5, total) | |||||
| assert.EqualValues(t, []int64{1, 2, 3, 5, 11}, ids) | |||||
| // issue1's comment id 2 | // issue1's comment id 2 | ||||
| total, ids, err = SearchIssueIDsByKeyword("good", []int64{1}, 10, 0) | total, ids, err = SearchIssueIDsByKeyword("good", []int64{1}, 10, 0) | ||||
| @@ -305,8 +305,8 @@ func testInsertIssue(t *testing.T, title, content string) { | |||||
| assert.True(t, has) | assert.True(t, has) | ||||
| assert.EqualValues(t, issue.Title, newIssue.Title) | assert.EqualValues(t, issue.Title, newIssue.Title) | ||||
| assert.EqualValues(t, issue.Content, newIssue.Content) | assert.EqualValues(t, issue.Content, newIssue.Content) | ||||
| // there are 4 issues and max index is 4 on repository 1, so this one should 5 | |||||
| assert.EqualValues(t, 5, newIssue.Index) | |||||
| // there are 5 issues and max index is 5 on repository 1, so this one should 6 | |||||
| assert.EqualValues(t, 6, newIssue.Index) | |||||
| _, err = x.ID(issue.ID).Delete(new(Issue)) | _, err = x.ID(issue.ID).Delete(new(Issue)) | ||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| @@ -17,7 +17,7 @@ func Test_newIssueUsers(t *testing.T) { | |||||
| newIssue := &Issue{ | newIssue := &Issue{ | ||||
| RepoID: repo.ID, | RepoID: repo.ID, | ||||
| PosterID: 4, | PosterID: 4, | ||||
| Index: 5, | |||||
| Index: 6, | |||||
| Title: "newTestIssueTitle", | Title: "newTestIssueTitle", | ||||
| Content: "newTestIssueContent", | Content: "newTestIssueContent", | ||||
| } | } | ||||
| @@ -742,3 +742,8 @@ func (pr *PullRequest) IsHeadEqualWithBranch(branchName string) (bool, error) { | |||||
| } | } | ||||
| return baseCommit.HasPreviousCommit(headCommit.ID) | return baseCommit.HasPreviousCommit(headCommit.ID) | ||||
| } | } | ||||
| // IsSameRepo returns true if base repo and head repo is the same | |||||
| func (pr *PullRequest) IsSameRepo() bool { | |||||
| return pr.BaseRepoID == pr.HeadRepoID | |||||
| } | |||||
| @@ -61,10 +61,11 @@ func TestPullRequestsNewest(t *testing.T) { | |||||
| Labels: []string{}, | Labels: []string{}, | ||||
| }) | }) | ||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| assert.Equal(t, int64(2), count) | |||||
| if assert.Len(t, prs, 2) { | |||||
| assert.Equal(t, int64(2), prs[0].ID) | |||||
| assert.Equal(t, int64(1), prs[1].ID) | |||||
| assert.EqualValues(t, 3, count) | |||||
| if assert.Len(t, prs, 3) { | |||||
| assert.EqualValues(t, 5, prs[0].ID) | |||||
| assert.EqualValues(t, 2, prs[1].ID) | |||||
| assert.EqualValues(t, 1, prs[2].ID) | |||||
| } | } | ||||
| } | } | ||||
| @@ -77,10 +78,11 @@ func TestPullRequestsOldest(t *testing.T) { | |||||
| Labels: []string{}, | Labels: []string{}, | ||||
| }) | }) | ||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| assert.Equal(t, int64(2), count) | |||||
| if assert.Len(t, prs, 2) { | |||||
| assert.Equal(t, int64(1), prs[0].ID) | |||||
| assert.Equal(t, int64(2), prs[1].ID) | |||||
| assert.EqualValues(t, 3, count) | |||||
| if assert.Len(t, prs, 3) { | |||||
| assert.EqualValues(t, 1, prs[0].ID) | |||||
| assert.EqualValues(t, 2, prs[1].ID) | |||||
| assert.EqualValues(t, 5, prs[2].ID) | |||||
| } | } | ||||
| } | } | ||||
| @@ -65,7 +65,7 @@ func TestBleveSearchIssues(t *testing.T) { | |||||
| ids, err = SearchIssuesByKeyword([]int64{1}, "for") | ids, err = SearchIssuesByKeyword([]int64{1}, "for") | ||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| assert.EqualValues(t, []int64{1, 2, 3, 5}, ids) | |||||
| assert.EqualValues(t, []int64{1, 2, 3, 5, 11}, ids) | |||||
| ids, err = SearchIssuesByKeyword([]int64{1}, "good") | ids, err = SearchIssuesByKeyword([]int64{1}, "good") | ||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| @@ -89,7 +89,7 @@ func TestDBSearchIssues(t *testing.T) { | |||||
| ids, err = SearchIssuesByKeyword([]int64{1}, "for") | ids, err = SearchIssuesByKeyword([]int64{1}, "for") | ||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| assert.EqualValues(t, []int64{1, 2, 3, 5}, ids) | |||||
| assert.EqualValues(t, []int64{1, 2, 3, 5, 11}, ids) | |||||
| ids, err = SearchIssuesByKeyword([]int64{1}, "good") | ids, err = SearchIssuesByKeyword([]int64{1}, "good") | ||||
| assert.NoError(t, err) | assert.NoError(t, err) | ||||
| @@ -1082,6 +1082,10 @@ pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because | |||||
| pulls.status_checking = Some checks are pending | pulls.status_checking = Some checks are pending | ||||
| pulls.status_checks_success = All checks were successful | pulls.status_checks_success = All checks were successful | ||||
| pulls.status_checks_error = Some checks failed | pulls.status_checks_error = Some checks failed | ||||
| pulls.update_branch = Update branch | |||||
| pulls.update_branch_success = Branch update was successful | |||||
| pulls.update_not_allowed = You are not allowed to update branch | |||||
| pulls.outdated_with_base_branch = This branch is out-of-date with the base branch | |||||
| milestones.new = New Milestone | milestones.new = New Milestone | ||||
| milestones.open_tab = %d Open | milestones.open_tab = %d Open | ||||
| @@ -14,6 +14,7 @@ import ( | |||||
| "net/http" | "net/http" | ||||
| "path" | "path" | ||||
| "strings" | "strings" | ||||
| "time" | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/auth" | "code.gitea.io/gitea/modules/auth" | ||||
| @@ -342,8 +343,21 @@ 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("GetDiverging", err) | |||||
| return nil | |||||
| } | |||||
| ctx.Data["UpdateAllowed"] = allowUpdate | |||||
| if err := pull.LoadProtectedBranch(); err != nil { | if err := pull.LoadProtectedBranch(); err != nil { | ||||
| ctx.ServerError("GetLatestCommitStatus", err) | |||||
| ctx.ServerError("LoadProtectedBranch", err) | |||||
| return nil | return nil | ||||
| } | } | ||||
| ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck | ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck | ||||
| @@ -587,6 +601,72 @@ func ViewPullFiles(ctx *context.Context) { | |||||
| ctx.HTML(200, tplPullFiles) | ctx.HTML(200, tplPullFiles) | ||||
| } | } | ||||
| // UpdatePullRequest merge master into PR | |||||
| func UpdatePullRequest(ctx *context.Context) { | |||||
| issue := checkPullInfo(ctx) | |||||
| if ctx.Written() { | |||||
| return | |||||
| } | |||||
| if issue.IsClosed { | |||||
| ctx.NotFound("MergePullRequest", nil) | |||||
| return | |||||
| } | |||||
| if issue.PullRequest.HasMerged { | |||||
| ctx.NotFound("MergePullRequest", nil) | |||||
| return | |||||
| } | |||||
| if err := issue.PullRequest.LoadBaseRepo(); err != nil { | |||||
| ctx.InternalServerError(err) | |||||
| return | |||||
| } | |||||
| if err := issue.PullRequest.LoadHeadRepo(); err != nil { | |||||
| ctx.InternalServerError(err) | |||||
| return | |||||
| } | |||||
| allowedUpdate, err := pull_service.IsUserAllowedToUpdate(issue.PullRequest, ctx.User) | |||||
| if err != nil { | |||||
| ctx.ServerError("IsUserAllowedToMerge", err) | |||||
| return | |||||
| } | |||||
| // ToDo: add check if maintainers are allowed to change branch ... (need migration & co) | |||||
| if !allowedUpdate { | |||||
| ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed")) | |||||
| ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index)) | |||||
| return | |||||
| } | |||||
| // default merge commit message | |||||
| message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch) | |||||
| if err = pull_service.Update(issue.PullRequest, ctx.User, message); err != nil { | |||||
| sanitize := func(x string) string { | |||||
| runes := []rune(x) | |||||
| if len(runes) > 512 { | |||||
| x = "..." + string(runes[len(runes)-512:]) | |||||
| } | |||||
| return strings.Replace(html.EscapeString(x), "\n", "<br>", -1) | |||||
| } | |||||
| if models.IsErrMergeConflicts(err) { | |||||
| conflictError := err.(models.ErrMergeConflicts) | |||||
| ctx.Flash.Error(ctx.Tr("repo.pulls.merge_conflict", sanitize(conflictError.StdErr), sanitize(conflictError.StdOut))) | |||||
| ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index)) | |||||
| return | |||||
| } | |||||
| ctx.Flash.Error(err.Error()) | |||||
| ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index)) | |||||
| } | |||||
| time.Sleep(1 * time.Second) | |||||
| ctx.Flash.Success(ctx.Tr("repo.pulls.update_branch_success")) | |||||
| ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index)) | |||||
| } | |||||
| // MergePullRequest response for merging pull request | // MergePullRequest response for merging pull request | ||||
| func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) { | func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) { | ||||
| issue := checkPullInfo(ctx) | issue := checkPullInfo(ctx) | ||||
| @@ -855,6 +855,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Get(".patch", repo.DownloadPullPatch) | m.Get(".patch", repo.DownloadPullPatch) | ||||
| m.Get("/commits", context.RepoRef(), repo.ViewPullCommits) | m.Get("/commits", context.RepoRef(), repo.ViewPullCommits) | ||||
| m.Post("/merge", context.RepoMustNotBeArchived(), reqRepoPullsWriter, bindIgnErr(auth.MergePullRequestForm{}), repo.MergePullRequest) | m.Post("/merge", context.RepoMustNotBeArchived(), reqRepoPullsWriter, bindIgnErr(auth.MergePullRequestForm{}), repo.MergePullRequest) | ||||
| m.Post("/update", repo.UpdatePullRequest) | |||||
| m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest) | m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest) | ||||
| m.Group("/files", func() { | m.Group("/files", func() { | ||||
| m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles) | m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles) | ||||
| @@ -33,11 +33,6 @@ import ( | |||||
| // Caller should check PR is ready to be merged (review and status checks) | // Caller should check PR is ready to be merged (review and status checks) | ||||
| // FIXME: add repoWorkingPull make sure two merges does not happen at same time. | // FIXME: add repoWorkingPull make sure two merges does not happen at same time. | ||||
| func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repository, mergeStyle models.MergeStyle, message string) (err error) { | func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repository, mergeStyle models.MergeStyle, message string) (err error) { | ||||
| binVersion, err := git.BinVersion() | |||||
| if err != nil { | |||||
| log.Error("git.BinVersion: %v", err) | |||||
| return fmt.Errorf("Unable to get git version: %v", err) | |||||
| } | |||||
| if err = pr.GetHeadRepo(); err != nil { | if err = pr.GetHeadRepo(); err != nil { | ||||
| log.Error("GetHeadRepo: %v", err) | log.Error("GetHeadRepo: %v", err) | ||||
| @@ -63,6 +58,61 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor | |||||
| go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") | go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "") | ||||
| }() | }() | ||||
| if err := rawMerge(pr, doer, mergeStyle, message); err != nil { | |||||
| return err | |||||
| } | |||||
| pr.MergedCommitID, err = baseGitRepo.GetBranchCommitID(pr.BaseBranch) | |||||
| if err != nil { | |||||
| return fmt.Errorf("GetBranchCommit: %v", err) | |||||
| } | |||||
| pr.MergedUnix = timeutil.TimeStampNow() | |||||
| pr.Merger = doer | |||||
| pr.MergerID = doer.ID | |||||
| if err = pr.SetMerged(); err != nil { | |||||
| log.Error("setMerged [%d]: %v", pr.ID, err) | |||||
| } | |||||
| notification.NotifyMergePullRequest(pr, doer) | |||||
| // Reset cached commit count | |||||
| cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true)) | |||||
| // Resolve cross references | |||||
| refs, err := pr.ResolveCrossReferences() | |||||
| if err != nil { | |||||
| log.Error("ResolveCrossReferences: %v", err) | |||||
| return nil | |||||
| } | |||||
| for _, ref := range refs { | |||||
| if err = ref.LoadIssue(); err != nil { | |||||
| return err | |||||
| } | |||||
| if err = ref.Issue.LoadRepo(); err != nil { | |||||
| return err | |||||
| } | |||||
| close := (ref.RefAction == references.XRefActionCloses) | |||||
| if close != ref.Issue.IsClosed { | |||||
| if err = issue_service.ChangeStatus(ref.Issue, doer, close); err != nil { | |||||
| return err | |||||
| } | |||||
| } | |||||
| } | |||||
| return nil | |||||
| } | |||||
| // rawMerge perform the merge operation without changing any pull information in database | |||||
| func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.MergeStyle, message string) (err error) { | |||||
| binVersion, err := git.BinVersion() | |||||
| if err != nil { | |||||
| log.Error("git.BinVersion: %v", err) | |||||
| return fmt.Errorf("Unable to get git version: %v", err) | |||||
| } | |||||
| // Clone base repo. | // Clone base repo. | ||||
| tmpBasePath, err := createTemporaryRepo(pr) | tmpBasePath, err := createTemporaryRepo(pr) | ||||
| if err != nil { | if err != nil { | ||||
| @@ -337,46 +387,6 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor | |||||
| outbuf.Reset() | outbuf.Reset() | ||||
| errbuf.Reset() | errbuf.Reset() | ||||
| pr.MergedCommitID, err = baseGitRepo.GetBranchCommitID(pr.BaseBranch) | |||||
| if err != nil { | |||||
| return fmt.Errorf("GetBranchCommit: %v", err) | |||||
| } | |||||
| pr.MergedUnix = timeutil.TimeStampNow() | |||||
| pr.Merger = doer | |||||
| pr.MergerID = doer.ID | |||||
| if err = pr.SetMerged(); err != nil { | |||||
| log.Error("setMerged [%d]: %v", pr.ID, err) | |||||
| } | |||||
| notification.NotifyMergePullRequest(pr, doer) | |||||
| // Reset cached commit count | |||||
| cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true)) | |||||
| // Resolve cross references | |||||
| refs, err := pr.ResolveCrossReferences() | |||||
| if err != nil { | |||||
| log.Error("ResolveCrossReferences: %v", err) | |||||
| return nil | |||||
| } | |||||
| for _, ref := range refs { | |||||
| if err = ref.LoadIssue(); err != nil { | |||||
| return err | |||||
| } | |||||
| if err = ref.Issue.LoadRepo(); err != nil { | |||||
| return err | |||||
| } | |||||
| close := (ref.RefAction == references.XRefActionCloses) | |||||
| if close != ref.Issue.IsClosed { | |||||
| if err = issue_service.ChangeStatus(ref.Issue, doer, close); err != nil { | |||||
| return err | |||||
| } | |||||
| } | |||||
| } | |||||
| return nil | return nil | ||||
| } | } | ||||
| @@ -0,0 +1,125 @@ | |||||
| // Copyright 2020 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package pull | |||||
| import ( | |||||
| "fmt" | |||||
| "strconv" | |||||
| "strings" | |||||
| "code.gitea.io/gitea/models" | |||||
| "code.gitea.io/gitea/modules/git" | |||||
| "code.gitea.io/gitea/modules/log" | |||||
| ) | |||||
| // Update updates pull request with base branch. | |||||
| func Update(pull *models.PullRequest, doer *models.User, message string) error { | |||||
| //use merge functions but switch repo's and branch's | |||||
| pr := &models.PullRequest{ | |||||
| HeadRepoID: pull.BaseRepoID, | |||||
| BaseRepoID: pull.HeadRepoID, | |||||
| HeadBranch: pull.BaseBranch, | |||||
| BaseBranch: pull.HeadBranch, | |||||
| } | |||||
| if err := pr.LoadHeadRepo(); err != nil { | |||||
| log.Error("LoadHeadRepo: %v", err) | |||||
| return fmt.Errorf("LoadHeadRepo: %v", err) | |||||
| } else if err = pr.LoadBaseRepo(); err != nil { | |||||
| log.Error("LoadBaseRepo: %v", err) | |||||
| return fmt.Errorf("LoadBaseRepo: %v", err) | |||||
| } | |||||
| diffCount, err := GetDiverging(pull) | |||||
| if err != nil { | |||||
| return err | |||||
| } else if diffCount.Behind == 0 { | |||||
| return fmt.Errorf("HeadBranch of PR %d is up to date", pull.Index) | |||||
| } | |||||
| defer func() { | |||||
| go AddTestPullRequestTask(doer, pr.HeadRepo.ID, pr.HeadBranch, false, "", "") | |||||
| }() | |||||
| return rawMerge(pr, doer, models.MergeStyleMerge, message) | |||||
| } | |||||
| // IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections | |||||
| func IsUserAllowedToUpdate(pull *models.PullRequest, user *models.User) (bool, error) { | |||||
| headRepoPerm, err := models.GetUserRepoPermission(pull.HeadRepo, user) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| pr := &models.PullRequest{ | |||||
| HeadRepoID: pull.BaseRepoID, | |||||
| BaseRepoID: pull.HeadRepoID, | |||||
| HeadBranch: pull.BaseBranch, | |||||
| BaseBranch: pull.HeadBranch, | |||||
| } | |||||
| return IsUserAllowedToMerge(pr, headRepoPerm, user) | |||||
| } | |||||
| // GetDiverging determines how many commits a PR is ahead or behind the PR base branch | |||||
| func GetDiverging(pr *models.PullRequest) (*git.DivergeObject, error) { | |||||
| log.Trace("PushToBaseRepo[%d]: pushing commits to base repo '%s'", pr.BaseRepoID, pr.GetGitRefName()) | |||||
| if err := pr.LoadBaseRepo(); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if err := pr.LoadHeadRepo(); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| headRepoPath := pr.HeadRepo.RepoPath() | |||||
| headGitRepo, err := git.OpenRepository(headRepoPath) | |||||
| if err != nil { | |||||
| return nil, fmt.Errorf("OpenRepository: %v", err) | |||||
| } | |||||
| defer headGitRepo.Close() | |||||
| if pr.IsSameRepo() { | |||||
| diff, err := git.GetDivergingCommits(pr.HeadRepo.RepoPath(), pr.BaseBranch, pr.HeadBranch) | |||||
| return &diff, err | |||||
| } | |||||
| tmpRemoteName := fmt.Sprintf("tmp-pull-%d-base", pr.ID) | |||||
| if err = headGitRepo.AddRemote(tmpRemoteName, pr.BaseRepo.RepoPath(), true); err != nil { | |||||
| return nil, fmt.Errorf("headGitRepo.AddRemote: %v", err) | |||||
| } | |||||
| // Make sure to remove the remote even if the push fails | |||||
| defer func() { | |||||
| if err := headGitRepo.RemoveRemote(tmpRemoteName); err != nil { | |||||
| log.Error("CountDiverging: RemoveRemote: %s", err) | |||||
| } | |||||
| }() | |||||
| // $(git rev-list --count tmp-pull-1-base/master..feature) commits ahead of master | |||||
| ahead, errorAhead := checkDivergence(headRepoPath, fmt.Sprintf("%s/%s", tmpRemoteName, pr.BaseBranch), pr.HeadBranch) | |||||
| if errorAhead != nil { | |||||
| return &git.DivergeObject{}, errorAhead | |||||
| } | |||||
| // $(git rev-list --count feature..tmp-pull-1-base/master) commits behind master | |||||
| behind, errorBehind := checkDivergence(headRepoPath, pr.HeadBranch, fmt.Sprintf("%s/%s", tmpRemoteName, pr.BaseBranch)) | |||||
| if errorBehind != nil { | |||||
| return &git.DivergeObject{}, errorBehind | |||||
| } | |||||
| return &git.DivergeObject{Ahead: ahead, Behind: behind}, nil | |||||
| } | |||||
| func checkDivergence(repoPath string, baseBranch string, targetBranch string) (int, error) { | |||||
| branches := fmt.Sprintf("%s..%s", baseBranch, targetBranch) | |||||
| cmd := git.NewCommand("rev-list", "--count", branches) | |||||
| stdout, err := cmd.RunInDir(repoPath) | |||||
| if err != nil { | |||||
| return -1, err | |||||
| } | |||||
| outInteger, errInteger := strconv.Atoi(strings.Trim(stdout, "\n")) | |||||
| if errInteger != nil { | |||||
| return -1, errInteger | |||||
| } | |||||
| return outInteger, nil | |||||
| } | |||||
| @@ -157,6 +157,26 @@ | |||||
| {{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }} | {{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }} | ||||
| </div> | </div> | ||||
| {{end}} | {{end}} | ||||
| {{if and .Divergence (gt .Divergence.Behind 0)}} | |||||
| <div class="ui very compact branch-update grid"> | |||||
| <div class="row"> | |||||
| <div class="item text gray eleven wide left floated column"> | |||||
| <i class="icon icon-octicon"><span class="octicon octicon-alert"></span></i> | |||||
| {{$.i18n.Tr "repo.pulls.outdated_with_base_branch"}} | |||||
| </div> | |||||
| {{if .UpdateAllowed}} | |||||
| <div class="item text five wide right floated column"> | |||||
| <form action="{{.Link}}/update" method="post"> | |||||
| {{.CsrfTokenHtml}} | |||||
| <button class="ui button" data-do="update"> | |||||
| <span class="item text">{{$.i18n.Tr "repo.pulls.update_branch"}}</span> | |||||
| </button> | |||||
| </form> | |||||
| </div> | |||||
| {{end}} | |||||
| </div> | |||||
| </div> | |||||
| {{end}} | |||||
| {{if .AllowMerge}} | {{if .AllowMerge}} | ||||
| {{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}} | {{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}} | ||||
| {{$approvers := .Issue.PullRequest.GetApprovers}} | {{$approvers := .Issue.PullRequest.GetApprovers}} | ||||
| @@ -655,6 +655,13 @@ | |||||
| .icon-octicon { | .icon-octicon { | ||||
| padding-left: 2px; | padding-left: 2px; | ||||
| } | } | ||||
| .branch-update.grid { | |||||
| margin-bottom: -1.5rem; | |||||
| margin-top: -0.5rem; | |||||
| .row { | |||||
| padding-bottom: 0; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| .review-item { | .review-item { | ||||