From 36943e56d66a2d711a6b0c27219ce91a3ddc020a Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 17 Jan 2020 07:03:40 +0100 Subject: [PATCH] Add "Update Branch" button to Pull Requests (#9784) * 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 --- integrations/api_issue_test.go | 6 +- .../user2/repo1.git/info/refs | 2 + .../5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2 | Bin 0 -> 833 bytes .../62/fb502a7172d4453f0322a2cc85bddffa57f07a | Bin 0 -> 839 bytes .../6a/a3a5385611c5eb8986c9961a9c34a93cbaadfb | Bin 0 -> 86 bytes .../7c/4df115542e05c700f297519e906fd63c9c9804 | Bin 0 -> 54 bytes .../94/922e1295c678267de1193b7b84ad8a086c27f9 | Bin 0 -> 54 bytes .../98/5f0301dba5e7b34be866819cd15ad3d8f508ee | Bin 0 -> 842 bytes .../a6/9277c81e90b98a7c0ab25b042a6e296da8eb9a | Bin 0 -> 76 bytes .../a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1d | Bin 0 -> 61 bytes .../b2/60587271671842af0b036e4fe643c9d45b7ddd | Bin 0 -> 20 bytes .../user2/repo1.git/refs/heads/branch2 | 1 + .../user2/repo1.git/refs/heads/pr-to-update | 1 + .../user2/repo1.git/refs/pull/2/head | 1 + .../user2/repo1.git/refs/pull/5/head | 1 + integrations/pull_update_test.go | 136 ++++++++++++++++++ integrations/repo_activity_test.go | 4 +- models/fixtures/issue.yml | 12 ++ models/fixtures/pull_request.yml | 15 +- models/fixtures/repository.yml | 2 +- models/issue_test.go | 8 +- models/issue_user_test.go | 2 +- models/pull.go | 5 + models/pull_test.go | 18 +-- modules/indexer/issues/indexer_test.go | 4 +- options/locale/locale_en-US.ini | 4 + routers/repo/pull.go | 82 ++++++++++- routers/routes/routes.go | 1 + services/pull/merge.go | 100 +++++++------ services/pull/update.go | 125 ++++++++++++++++ templates/repo/issue/view_content/pull.tmpl | 20 +++ web_src/less/_repository.less | 7 + 32 files changed, 489 insertions(+), 68 deletions(-) create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/objects/5c/050d3b6d2db231ab1f64e324f1b6b9a0b181c2 create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/objects/62/fb502a7172d4453f0322a2cc85bddffa57f07a create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/objects/6a/a3a5385611c5eb8986c9961a9c34a93cbaadfb create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/objects/7c/4df115542e05c700f297519e906fd63c9c9804 create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/objects/94/922e1295c678267de1193b7b84ad8a086c27f9 create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/objects/98/5f0301dba5e7b34be866819cd15ad3d8f508ee create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/objects/a6/9277c81e90b98a7c0ab25b042a6e296da8eb9a create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/objects/a7/57c0ea621e63d0fd6fc353a175fdc7199e5d1d create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/objects/b2/60587271671842af0b036e4fe643c9d45b7ddd create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/branch2 create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/refs/heads/pr-to-update create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/2/head create mode 100644 integrations/gitea-repositories-meta/user2/repo1.git/refs/pull/5/head create mode 100644 integrations/pull_update_test.go create mode 100644 services/pull/update.go 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 0000000000000000000000000000000000000000..c0cb626359e600503128d5c65213dd0fcb1c1fcf GIT binary patch literal 833 zcmV-H1HSxt0gY41vf@Sn%h_MicPepvZFg1PRJ?*0zzZ16&2BFk?+XU}^?5V7<}fq6jgcr3p#+l!~A8y&J#%JO}>%$^UdoHa5@luQdAs z7y)5M`c`?cfeeHc`BiMrem_z4#Z(sH&!15At$6_M)&+sNJ;#r+$4d`gIRSeT~dGqh!M1d;D_H44<0IfA% z{pwSywwLMNcl7Af!XB1~Q@ztT>FQkfb~#`%HXP}sux3o$9vvyb`IDfe%=#(j`W58j z^>m%um?fTXsMdzH(EILi501YC%=k&9lZM{vO54?VOd?}84-~LA1hK6eEf1YUN70zy z!{}+)DHvM0q=ZR2)JCp6bd00u*x_}(tDVN~diC(UI6IXBQU~X7yK6M2*Dr-#mgKeA zd(I%;>Kc_j!ciW3E$n))DeKu9%)D7WTgB!&L{(I@6!7fevzD(+>8m?^8qyML{Vj_M z^4SSt%1xJ*#*@Um!(s+Of@kG%uULZNXim~4j1mgyD~_jax4S>{;B`;Nj|i5^;eev z02Id~nj|6Oz>cIyl1P6AoATdxl+SXkO7Q2;lhmVT0{dtKPdl2TAGqqTKLH3(Il3x| zq9lvre5h2Ft-WRpA6I#IT|`+!eflKHZtohMdJr47951PJQm&M#m|{Zf4FS@YqOiw3 zZjN}CUHQIszE>c1OT+r8>``S=V0XLqG8N>;Ch3op6kNq1xRP0@wqXT`?Z!;p*63y~U|(JF3dQgK-~AW-v4`Ff%bxaLdd|)eX-NXV{bwQB;^N;k2HcInV!@^T{jG swRfRvf?OS4d|mZ&Qy7+qA9$4{mwe%G{^8(-rGJl0&Wn`=07oVsJ1R;hi2wiq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3bf67a206abf6a29f1e69ed3c9c1aa119c5581f8 GIT binary patch literal 54 zcmbp-pZ3pI&r&~3 KjKQ-*P#yphauiYk literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..60692df6ec48948b93f23166b72d863bb7a09950 GIT binary patch literal 54 zcmV-60LlM&0V^p=O;s>9XD~D{Ff%bx2y%6F@paY9O<`Czsr-c8gq>YAT$`d)EZH(=IVQ`QO5`!e@)eVdxst)0k(q*}vSq%L5r_iMfj@up-z3Llzchc8 z_%FZ+lENqgloUWi212U%Rcyxp-%&s7sj0#5KXKI0rUx$P3kc1#hoWns|I#TyfyTG= zBSJ?Nq2<)*I{yf}DQ;7hPG3kh6;eDQ91ejy=qJ?ibw3rpp{0zcc_?SCjsiu`Fj*+H zUp}(cK)vrn6jqA97nyX_ks1vT(Fv9CB83d*OggAf^;8u0O@pUF=kZ8^UX3P`4awy} zawkUIw!@g<{JbmORZLCQv^v(%iaPSjA!ccHR4T6kturyiLcaRrQ`k z>bRcvX~jn6@;nZ@Yqozm{ijDbA!=<|;Jf+trek&zQ~b!+Wp{rhka!)fEjnkDX}>n; zt4Ce_=6n)wnjjfLyV*}uYr-dtxtU10*1B6RPlj_PH2u7NQ((}irliv@Q(vj!h&$2( zq=tt*?!PTIZhWa+v^%Mpz?p-%a?KlAOzoU}PMCveup|m}R)4L(EU(A~r_E2CnhQNXDO{KUs9x_+y+2HRA+drsIL zx>u;X=%9ad!yp{q@|DhVS{!8hO74EjVW5*R-JyBRyCoUFl{JIT=Nr6dw_hV~^u`?ra*|TdCj0l}Gcs
l4NIIDvDG~aEKvftHTq2I7s9iVJ9Nv!ac&!038jL} zQfb@{jfQT=ho7v^g&m&6kUDnsZ1wP+1{C0UMR|ud+)2+2>qQwG`*2f&M$~Cz#X~a8 zcot5#dnUhp=JTGEi6*H>+e>#OJ{3~_Sn`IknN^v&etb~iqfuXk`u9z`r~TKLs6J~k UcrINh0J6+wHsg}|2keej8qMsY6aWAK literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..887669883b75550827b0fa1e0f499a8b085e4eed GIT binary patch literal 76 zcmV-S0JHyi0ZYosPf{>9Vo+8nN-fAY=A|ekXC&sO ir-DV3iW2jZGmN+rfx?Lj#i_~pKt_IEDi;6+g&FhS2q5GD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c3111a08b847838e2f8aac8f97e9ab2dad37bad9 GIT binary patch literal 61 zcmV-D0K)%x0ZYosPf{>7Wl&ZqN-fAY=A|ekXC&sO Tr-DV3iW2jZGmN+Zc=HrE6jK^Y literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9182ac038166ac30f7f550603dc8b87c1b102bc9 GIT binary patch literal 20 bcmb 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 {