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 <lauris@nix.lv> --- 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<}f<w zppvR|kVw}w<qQ}ov46~CmH|ZgG-nt=;Moic1VNay7zqr{FckAt<On8Mf;`p7W7f_< z6mllXS(Fei$q~#&UKEfhNFpa>q6jgcr3p#+l!~A8y&J#%JO}>%$^UdoHa5@luQdAs z7y)5M`c`?cfeeHc`BiMrem_z4#Z(sH&!15At$6_M)&+sNJ;<lCum931K!MV;bVZgG zRhFMdsq6eBsHd=s)J?R=s);+&L)g;(zSRx6=8M$tyqy*?2kjsytW{HhGz=35LUr?G zTlLlZr#%HZ)<5LTxEhT;cage_dT^u0>#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~4j1mgy<LE87g1(-Gm!`4CV%|2LdbGLo zzP#HP=QgCqZEh_~g0Oa<V`1;FMjj56%lI-cLkf&)=sMYM8@@23HD(A|$Dy+=%mPvg ziB%Yz25$0Ar!Z|UH4t6Xr?OY<;?eMaAS*5f-cf%J-%yf^p|IlC+b-{mdQ%Tpye)6T z*zh4Xgq`KQbft;H;eL5`$rIn`fTS}gDk24@E3GKefkw0+O|GHDug4CVT`8Q0a^B5y zHm!(NEum)z*YkbmAz#b;BFLn3s~3I#-sF)-HI3qVxv52rgt|rDeRs14-63*)F-5s& zn&&dDnQ&@VPT0azjl4YN?bwVk9hMX@%dM<PCE9pjUljO4_hU@`_a^<I{oj|U7?6A1 L78&&~lqpX7&|IyK literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ee494a8ca8c4a75d3d5ee3d519428e0a5b0a50d9 GIT binary patch literal 839 zcmV-N1GxNn0gaPMui{n|gtPXqxO<e~@!D6)YsC{_gXa-;*CRF_z$50@=XGbBMW012 zj#N^qN?+QyZ8ZUk6yP7zkmmqm%wUcYI7m?`mnQ-eV2Ti=FiIH~BPLBS&WeP1;vw%Q zz@;QdW=sMzI!kDV!U<JCHfLzIpc0rMOeT~-r8I%(>D~_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>``<rl(wA1jDi=jl=<3a6%wVD#pUJX!F(lvn-$IHM%ph$ zu^s}gJlc8inNz$9jP`gV=wEhNBheTMowUQ|;UGhiZin!skC&}BTLP2{j7Czes^_T? zD41H!dmpCltJR{$($2;CoR2~usVQ}Dr}A{*_~x+n7rBaO|9rix1bDLij0~se#V(Z_ zOj{(h6l0B`7a1`1p2JzaORBwTac6o}OUwHq{;p<pYoE_YO(cL(Cwp~c9p;sqQ~crw zKXEWsFpv^kLt<U4E#LL>S=V0XLqG8N>;Ch3op6kNq1xRP0@w<YkNiO<*HD|-NW84) zZ4g(*86M<N?{1FXaMc{x&z<vHUOpYK^wrj9sgv|vGxd=Gi^Lvy-MI~3B6p{wmza+I z*|#}bl~cSit7*MI8mbFgtg3SA`B+VDHPp)8B}(egHW46X6Adz~>qXT`?Z!;p<BvPV z;rq^R-llhMu&;v9v*Sm|52nc7eCyqW1!O)*J>*63y~U|(JF3dQgK-~<zdsYiHO8F3 zH%W}Trg?~c<uz~?8B&dnJzqrWjsC{&z1*f(%#;N1x?qgGsSE`i&E0mX(NjY<oR=V& zWho70-N;<b$Gh&d)YHg1Mb^Lc)sZq|)H8D?097sb7<Nozg~51c72l}+whWd!bgrnZ zGAqG6_sXq{&MZEAZo3JemDw%^=}`w4z5Z^}<L~d7CTr*GivVBsZt;l!Xi`n(|5ze$ RmVu~`z@ch#;$O#7Rc2yytXu#9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..09aed946f2fa49948e15cc8ccbb0b523f356ad6c GIT binary patch literal 86 zcmV-c0IC0Y0V^p=O;s>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 zcmb<m)YkO!4K*-JH!v|UFg6V2C@L-aSbFueZ(@X8-Gk6cF`ruZ|7>p-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<wDL4_ MSG=AD05swcksbFI2mk;8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..81fd6a50fda008977f5a303d34ff0442fc0a3c18 GIT binary patch literal 842 zcmV-Q1GW5k0gY41j;lrh%$cv~J4)Q_AZ4Po0h@iyVhnR*X)tDK4~qeRecpSy=8!tn zp`<F6N~(hNeKP|FD%@}Lh%r!jg=I0rDncPjGAl|YW*CLbrCjE-QqG_(7OYe$)H56L zFax200xY>)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!ccHR4<xciKQ}H?w3>T6kturyiLcaRrQ`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_E2C<wE(+ zX|lg{w`ED(EwD}DSm4t4#`Ph}O%jXgz`x7~3F>nhQNXDO{KUs9x_+y+2HRA+drsIL zx>u;X=%9ad!yp{q@|DhVS{!8hO74EjVW<hi7`rbDv_W=RdBd8Q6uNRVW)*)xS@SvG zmeLYXTyiAvs<#k@>5*R-JyBRyCoUFl{JIT=Nr6dw_hV~^u`?ra*|TdCj0l}Gcs<DT z!I*=$6K~J*;hxG3oxAv+-<5>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<l=HkElw`VEGWs$&r?XtFM<g>=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<l=HkElw`VEGWs$&r?XtFM<g>=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<m^geacKgb}J<2--(6D9@;XJ%UfOmPNG literal 0 HcmV?d00001 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", "<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 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) }} </div> {{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}} {{$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 {