diff --git a/integrations/api_issue_test.go b/integrations/api_issue_test.go index ce1c4b7d33..906dbb2dc7 100644 --- a/integrations/api_issue_test.go +++ b/integrations/api_issue_test.go @@ -134,7 +134,7 @@ func TestAPISearchIssue(t *testing.T) { var apiIssues []*api.Issue DecodeJSON(t, resp, &apiIssues) - assert.Len(t, apiIssues, 8) + assert.Len(t, apiIssues, 9) query := url.Values{} query.Add("token", token) @@ -142,7 +142,7 @@ func TestAPISearchIssue(t *testing.T) { req = NewRequest(t, "GET", link.String()) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.Len(t, apiIssues, 8) + assert.Len(t, apiIssues, 9) query.Add("state", "closed") link.RawQuery = query.Encode() @@ -163,5 +163,5 @@ func TestAPISearchIssue(t *testing.T) { req = NewRequest(t, "GET", link.String()) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.Len(t, apiIssues, 0) + assert.Len(t, apiIssues, 1) } diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/info/refs b/integrations/gitea-repositories-meta/user2/repo1.git/info/refs index ca1df85e2e..fa3009793d 100644 --- a/integrations/gitea-repositories-meta/user2/repo1.git/info/refs +++ b/integrations/gitea-repositories-meta/user2/repo1.git/info/refs @@ -1 +1,3 @@ 65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/master +985f0301dba5e7b34be866819cd15ad3d8f508ee refs/heads/branch2 +62fb502a7172d4453f0322a2cc85bddffa57f07a refs/heads/pr-to-update diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2 b/integrations/gitea-repositories-meta/user2/repo1.git/objects/5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2 new file mode 100644 index 0000000000..c0cb626359 Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2 differ diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/62/fb502a7172d4453f0322a2cc85bddffa57f07a b/integrations/gitea-repositories-meta/user2/repo1.git/objects/62/fb502a7172d4453f0322a2cc85bddffa57f07a new file mode 100644 index 0000000000..ee494a8ca8 Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/62/fb502a7172d4453f0322a2cc85bddffa57f07a differ diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/6a/a3a5385611c5eb8986c9961a9c34a93cbaadfb b/integrations/gitea-repositories-meta/user2/repo1.git/objects/6a/a3a5385611c5eb8986c9961a9c34a93cbaadfb new file mode 100644 index 0000000000..09aed946f2 Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/6a/a3a5385611c5eb8986c9961a9c34a93cbaadfb differ diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/7c/4df115542e05c700f297519e906fd63c9c9804 b/integrations/gitea-repositories-meta/user2/repo1.git/objects/7c/4df115542e05c700f297519e906fd63c9c9804 new file mode 100644 index 0000000000..3bf67a206a Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/7c/4df115542e05c700f297519e906fd63c9c9804 differ diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/94/922e1295c678267de1193b7b84ad8a086c27f9 b/integrations/gitea-repositories-meta/user2/repo1.git/objects/94/922e1295c678267de1193b7b84ad8a086c27f9 new file mode 100644 index 0000000000..60692df6ec Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/94/922e1295c678267de1193b7b84ad8a086c27f9 differ diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/98/5f0301dba5e7b34be866819cd15ad3d8f508ee b/integrations/gitea-repositories-meta/user2/repo1.git/objects/98/5f0301dba5e7b34be866819cd15ad3d8f508ee new file mode 100644 index 0000000000..81fd6a50fd Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/98/5f0301dba5e7b34be866819cd15ad3d8f508ee differ diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/a6/9277c81e90b98a7c0ab25b042a6e296da8eb9a b/integrations/gitea-repositories-meta/user2/repo1.git/objects/a6/9277c81e90b98a7c0ab25b042a6e296da8eb9a new file mode 100644 index 0000000000..887669883b Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/a6/9277c81e90b98a7c0ab25b042a6e296da8eb9a differ diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1d b/integrations/gitea-repositories-meta/user2/repo1.git/objects/a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1d new file mode 100644 index 0000000000..c3111a08b8 Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1d differ diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/objects/b2/60587271671842af0b036e4fe643c9d45b7ddd b/integrations/gitea-repositories-meta/user2/repo1.git/objects/b2/60587271671842af0b036e4fe643c9d45b7ddd new file mode 100644 index 0000000000..9182ac0381 Binary files /dev/null and b/integrations/gitea-repositories-meta/user2/repo1.git/objects/b2/60587271671842af0b036e4fe643c9d45b7ddd differ diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/branch2 b/integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/branch2 new file mode 100644 index 0000000000..5add7256cd --- /dev/null +++ b/integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/branch2 @@ -0,0 +1 @@ +985f0301dba5e7b34be866819cd15ad3d8f508ee diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/pr-to-update b/integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/pr-to-update new file mode 100644 index 0000000000..e0ee44dd14 --- /dev/null +++ b/integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/pr-to-update @@ -0,0 +1 @@ +62fb502a7172d4453f0322a2cc85bddffa57f07a diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/2/head b/integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/2/head new file mode 100644 index 0000000000..98593d6537 --- /dev/null +++ b/integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/2/head @@ -0,0 +1 @@ +4a357436d925b5c974181ff12a994538ddc5a269 diff --git a/integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/5/head b/integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/5/head new file mode 100644 index 0000000000..e0ee44dd14 --- /dev/null +++ b/integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/5/head @@ -0,0 +1 @@ +62fb502a7172d4453f0322a2cc85bddffa57f07a diff --git a/integrations/pull_update_test.go b/integrations/pull_update_test.go new file mode 100644 index 0000000000..484390001c --- /dev/null +++ b/integrations/pull_update_test.go @@ -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 +} diff --git a/integrations/repo_activity_test.go b/integrations/repo_activity_test.go index cec5c79c4d..e21f27893d 100644 --- a/integrations/repo_activity_test.go +++ b/integrations/repo_activity_test.go @@ -56,9 +56,9 @@ func TestRepoActivity(t *testing.T) { list = htmlDoc.doc.Find("#merged-pull-requests").Next().Find("p.desc") 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") - assert.Len(t, list.Nodes, 2) + assert.Len(t, list.Nodes, 3) // Should be 3 new issues list = htmlDoc.doc.Find("#new-issues").Next().Find("p.desc") diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml index ecee7499f6..e52a23a46b 100644 --- a/models/fixtures/issue.yml +++ b/models/fixtures/issue.yml @@ -122,3 +122,15 @@ created_unix: 946684830 updated_unix: 999307200 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 diff --git a/models/fixtures/pull_request.yml b/models/fixtures/pull_request.yml index e8d81a0007..da9566bc48 100644 --- a/models/fixtures/pull_request.yml +++ b/models/fixtures/pull_request.yml @@ -49,4 +49,17 @@ head_branch: branch1 base_branch: master merge_base: abcdef1234567890 - has_merged: false \ No newline at end of file + 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 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index a68e63e309..05989d9030 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -7,7 +7,7 @@ is_private: false num_issues: 2 num_closed_issues: 1 - num_pulls: 2 + num_pulls: 3 num_closed_pulls: 0 num_milestones: 3 num_closed_milestones: 1 diff --git a/models/issue_test.go b/models/issue_test.go index ec4867d075..d65345a508 100644 --- a/models/issue_test.go +++ b/models/issue_test.go @@ -276,8 +276,8 @@ func TestIssue_SearchIssueIDsByKeyword(t *testing.T) { total, ids, err = SearchIssueIDsByKeyword("for", []int64{1}, 10, 0) 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 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.EqualValues(t, issue.Title, newIssue.Title) 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)) assert.NoError(t, err) diff --git a/models/issue_user_test.go b/models/issue_user_test.go index a57ab33f9e..01e0bdc644 100644 --- a/models/issue_user_test.go +++ b/models/issue_user_test.go @@ -17,7 +17,7 @@ func Test_newIssueUsers(t *testing.T) { newIssue := &Issue{ RepoID: repo.ID, PosterID: 4, - Index: 5, + Index: 6, Title: "newTestIssueTitle", Content: "newTestIssueContent", } diff --git a/models/pull.go b/models/pull.go index 1edd890035..fcfcd221c4 100644 --- a/models/pull.go +++ b/models/pull.go @@ -742,3 +742,8 @@ func (pr *PullRequest) IsHeadEqualWithBranch(branchName string) (bool, error) { } 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 +} diff --git a/models/pull_test.go b/models/pull_test.go index 9c27b603aa..6ceeae6653 100644 --- a/models/pull_test.go +++ b/models/pull_test.go @@ -61,10 +61,11 @@ func TestPullRequestsNewest(t *testing.T) { Labels: []string{}, }) 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{}, }) 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) } } diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 4028a6c8b5..8a54c200ff 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -65,7 +65,7 @@ func TestBleveSearchIssues(t *testing.T) { ids, err = SearchIssuesByKeyword([]int64{1}, "for") 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") assert.NoError(t, err) @@ -89,7 +89,7 @@ func TestDBSearchIssues(t *testing.T) { ids, err = SearchIssuesByKeyword([]int64{1}, "for") 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") assert.NoError(t, err) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 60df796e07..9a4f0535e8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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_checks_success = All checks were successful 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.open_tab = %d Open diff --git a/routers/repo/pull.go b/routers/repo/pull.go index 901ab48856..fc0012ffbe 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -14,6 +14,7 @@ import ( "net/http" "path" "strings" + "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" @@ -342,8 +343,21 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare 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 { - ctx.ServerError("GetLatestCommitStatus", err) + ctx.ServerError("LoadProtectedBranch", err) return nil } ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck @@ -587,6 +601,72 @@ func ViewPullFiles(ctx *context.Context) { 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", "
", -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 func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) { issue := checkPullInfo(ctx) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 58a2da82fc..7e81f55de6 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -855,6 +855,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get(".patch", repo.DownloadPullPatch) m.Get("/commits", context.RepoRef(), repo.ViewPullCommits) 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.Group("/files", func() { m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.ViewPullFiles) diff --git a/services/pull/merge.go b/services/pull/merge.go index b423c663ea..26c9ab3d1c 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -33,11 +33,6 @@ import ( // 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. 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 { 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, "", "") }() + 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. tmpBasePath, err := createTemporaryRepo(pr) if err != nil { @@ -337,46 +387,6 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor outbuf.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 } diff --git a/services/pull/update.go b/services/pull/update.go new file mode 100644 index 0000000000..5f055827e1 --- /dev/null +++ b/services/pull/update.go @@ -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 +} diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index f8a82f1a0f..d15237137d 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -157,6 +157,26 @@ {{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }} {{end}} + {{if and .Divergence (gt .Divergence.Behind 0)}} +
+
+
+ + {{$.i18n.Tr "repo.pulls.outdated_with_base_branch"}} +
+ {{if .UpdateAllowed}} +
+
+ {{.CsrfTokenHtml}} + +
+
+ {{end}} +
+
+ {{end}} {{if .AllowMerge}} {{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}} {{$approvers := .Issue.PullRequest.GetApprovers}} diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index 27a0698f7b..a1b55e86aa 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -655,6 +655,13 @@ .icon-octicon { padding-left: 2px; } + .branch-update.grid { + margin-bottom: -1.5rem; + margin-top: -0.5rem; + .row { + padding-bottom: 0; + } + } } .review-item {