gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package integration
  4. import (
  5. "bytes"
  6. "fmt"
  7. "net/http"
  8. "net/http/httptest"
  9. "net/url"
  10. "os"
  11. "path"
  12. "path/filepath"
  13. "strconv"
  14. "strings"
  15. "testing"
  16. "time"
  17. auth_model "code.gitea.io/gitea/models/auth"
  18. git_model "code.gitea.io/gitea/models/git"
  19. issues_model "code.gitea.io/gitea/models/issues"
  20. pull_model "code.gitea.io/gitea/models/pull"
  21. repo_model "code.gitea.io/gitea/models/repo"
  22. "code.gitea.io/gitea/models/unittest"
  23. user_model "code.gitea.io/gitea/models/user"
  24. "code.gitea.io/gitea/models/webhook"
  25. "code.gitea.io/gitea/modules/commitstatus"
  26. "code.gitea.io/gitea/modules/git"
  27. "code.gitea.io/gitea/modules/git/gitcmd"
  28. "code.gitea.io/gitea/modules/gitrepo"
  29. "code.gitea.io/gitea/modules/queue"
  30. "code.gitea.io/gitea/modules/setting"
  31. api "code.gitea.io/gitea/modules/structs"
  32. "code.gitea.io/gitea/modules/test"
  33. "code.gitea.io/gitea/modules/translation"
  34. "code.gitea.io/gitea/services/automerge"
  35. "code.gitea.io/gitea/services/automergequeue"
  36. pull_service "code.gitea.io/gitea/services/pull"
  37. repo_service "code.gitea.io/gitea/services/repository"
  38. commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
  39. files_service "code.gitea.io/gitea/services/repository/files"
  40. "github.com/stretchr/testify/assert"
  41. )
  42. func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle, deleteBranch bool) *httptest.ResponseRecorder {
  43. req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
  44. resp := session.MakeRequest(t, req, http.StatusOK)
  45. htmlDoc := NewHTMLParser(t, resp.Body)
  46. link := path.Join(user, repo, "pulls", pullnum, "merge")
  47. options := map[string]string{
  48. "_csrf": htmlDoc.GetCSRF(),
  49. "do": string(mergeStyle),
  50. }
  51. if deleteBranch {
  52. options["delete_branch_after_merge"] = "on"
  53. }
  54. req = NewRequestWithValues(t, "POST", link, options)
  55. resp = session.MakeRequest(t, req, http.StatusOK)
  56. respJSON := struct {
  57. Redirect string
  58. }{}
  59. DecodeJSON(t, resp, &respJSON)
  60. assert.Equal(t, fmt.Sprintf("/%s/%s/pulls/%s", user, repo, pullnum), respJSON.Redirect)
  61. return resp
  62. }
  63. func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder {
  64. req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
  65. resp := session.MakeRequest(t, req, http.StatusOK)
  66. // Click the little button to create a pull
  67. htmlDoc := NewHTMLParser(t, resp.Body)
  68. link, exists := htmlDoc.doc.Find(".timeline-item .delete-button").Attr("data-url")
  69. assert.True(t, exists, "The template has changed, can not find delete button url")
  70. req = NewRequestWithValues(t, "POST", link, map[string]string{
  71. "_csrf": htmlDoc.GetCSRF(),
  72. })
  73. resp = session.MakeRequest(t, req, http.StatusOK)
  74. return resp
  75. }
  76. func TestPullMerge(t *testing.T) {
  77. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  78. hookTasks, err := webhook.HookTasks(t.Context(), 1, 1) // Retrieve previous hook number
  79. assert.NoError(t, err)
  80. hookTasksLenBefore := len(hookTasks)
  81. session := loginUser(t, "user1")
  82. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  83. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  84. resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
  85. elem := strings.Split(test.RedirectURL(resp), "/")
  86. assert.Equal(t, "pulls", elem[3])
  87. testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
  88. hookTasks, err = webhook.HookTasks(t.Context(), 1, 1)
  89. assert.NoError(t, err)
  90. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  91. })
  92. }
  93. func TestPullRebase(t *testing.T) {
  94. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  95. hookTasks, err := webhook.HookTasks(t.Context(), 1, 1) // Retrieve previous hook number
  96. assert.NoError(t, err)
  97. hookTasksLenBefore := len(hookTasks)
  98. session := loginUser(t, "user1")
  99. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  100. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  101. resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
  102. elem := strings.Split(test.RedirectURL(resp), "/")
  103. assert.Equal(t, "pulls", elem[3])
  104. testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebase, false)
  105. hookTasks, err = webhook.HookTasks(t.Context(), 1, 1)
  106. assert.NoError(t, err)
  107. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  108. })
  109. }
  110. func TestPullRebaseMerge(t *testing.T) {
  111. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  112. hookTasks, err := webhook.HookTasks(t.Context(), 1, 1) // Retrieve previous hook number
  113. assert.NoError(t, err)
  114. hookTasksLenBefore := len(hookTasks)
  115. session := loginUser(t, "user1")
  116. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  117. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  118. resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
  119. elem := strings.Split(test.RedirectURL(resp), "/")
  120. assert.Equal(t, "pulls", elem[3])
  121. testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebaseMerge, false)
  122. hookTasks, err = webhook.HookTasks(t.Context(), 1, 1)
  123. assert.NoError(t, err)
  124. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  125. })
  126. }
  127. func TestPullSquash(t *testing.T) {
  128. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  129. hookTasks, err := webhook.HookTasks(t.Context(), 1, 1) // Retrieve previous hook number
  130. assert.NoError(t, err)
  131. hookTasksLenBefore := len(hookTasks)
  132. session := loginUser(t, "user1")
  133. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  134. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  135. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
  136. resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
  137. elem := strings.Split(test.RedirectURL(resp), "/")
  138. assert.Equal(t, "pulls", elem[3])
  139. testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleSquash, false)
  140. hookTasks, err = webhook.HookTasks(t.Context(), 1, 1)
  141. assert.NoError(t, err)
  142. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  143. })
  144. }
  145. func TestPullCleanUpAfterMerge(t *testing.T) {
  146. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  147. session := loginUser(t, "user1")
  148. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  149. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n")
  150. resp := testPullCreate(t, session, "user1", "repo1", false, "master", "feature/test", "This is a pull title")
  151. elem := strings.Split(test.RedirectURL(resp), "/")
  152. assert.Equal(t, "pulls", elem[3])
  153. testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
  154. // Check PR branch deletion
  155. resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4])
  156. respJSON := struct {
  157. Redirect string
  158. }{}
  159. DecodeJSON(t, resp, &respJSON)
  160. assert.NotEmpty(t, respJSON.Redirect, "Redirected URL is not found")
  161. elem = strings.Split(respJSON.Redirect, "/")
  162. assert.Equal(t, "pulls", elem[3])
  163. // Check branch deletion result
  164. req := NewRequest(t, "GET", respJSON.Redirect)
  165. resp = session.MakeRequest(t, req, http.StatusOK)
  166. htmlDoc := NewHTMLParser(t, resp.Body)
  167. resultMsg := htmlDoc.doc.Find(".ui.message>p").Text()
  168. assert.Equal(t, "Branch \"user1/repo1:feature/test\" has been deleted.", resultMsg)
  169. })
  170. }
  171. func TestCantMergeWorkInProgress(t *testing.T) {
  172. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  173. session := loginUser(t, "user1")
  174. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  175. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  176. resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "[wip] This is a pull title")
  177. req := NewRequest(t, "GET", test.RedirectURL(resp))
  178. resp = session.MakeRequest(t, req, http.StatusOK)
  179. htmlDoc := NewHTMLParser(t, resp.Body)
  180. text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text())
  181. assert.NotEmpty(t, text, "Can't find WIP text")
  182. assert.Contains(t, text, translation.NewLocale("en-US").TrString("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
  183. assert.Contains(t, text, "[wip]", "Unable to find WIP text")
  184. })
  185. }
  186. func TestCantMergeConflict(t *testing.T) {
  187. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  188. session := loginUser(t, "user1")
  189. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  190. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
  191. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
  192. // Use API to create a conflicting pr
  193. token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
  194. req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
  195. Head: "conflict",
  196. Base: "base",
  197. Title: "create a conflicting pr",
  198. }).AddTokenAuth(token)
  199. session.MakeRequest(t, req, http.StatusCreated)
  200. // Now this PR will be marked conflict - or at least a race will do - so drop down to pure code at this point...
  201. user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  202. Name: "user1",
  203. })
  204. repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
  205. OwnerID: user1.ID,
  206. Name: "repo1",
  207. })
  208. pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
  209. HeadRepoID: repo1.ID,
  210. BaseRepoID: repo1.ID,
  211. HeadBranch: "conflict",
  212. BaseBranch: "base",
  213. })
  214. gitRepo, err := gitrepo.OpenRepository(t.Context(), repo1)
  215. assert.NoError(t, err)
  216. err = pull_service.Merge(t.Context(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "CONFLICT", false)
  217. assert.Error(t, err, "Merge should return an error due to conflict")
  218. assert.True(t, pull_service.IsErrMergeConflicts(err), "Merge error is not a conflict error")
  219. err = pull_service.Merge(t.Context(), pr, user1, gitRepo, repo_model.MergeStyleRebase, "", "CONFLICT", false)
  220. assert.Error(t, err, "Merge should return an error due to conflict")
  221. assert.True(t, pull_service.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
  222. gitRepo.Close()
  223. })
  224. }
  225. func TestCantMergeUnrelated(t *testing.T) {
  226. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  227. session := loginUser(t, "user1")
  228. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  229. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
  230. // Now we want to create a commit on a branch that is totally unrelated to our current head
  231. // Drop down to pure code at this point
  232. user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  233. Name: "user1",
  234. })
  235. repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
  236. OwnerID: user1.ID,
  237. Name: "repo1",
  238. })
  239. path := repo_model.RepoPath(user1.Name, repo1.Name)
  240. err := gitcmd.NewCommand("read-tree", "--empty").Run(t.Context(), &gitcmd.RunOpts{Dir: path})
  241. assert.NoError(t, err)
  242. stdin := strings.NewReader("Unrelated File")
  243. var stdout strings.Builder
  244. err = gitcmd.NewCommand("hash-object", "-w", "--stdin").Run(t.Context(), &gitcmd.RunOpts{
  245. Dir: path,
  246. Stdin: stdin,
  247. Stdout: &stdout,
  248. })
  249. assert.NoError(t, err)
  250. sha := strings.TrimSpace(stdout.String())
  251. _, _, err = gitcmd.NewCommand("update-index", "--add", "--replace", "--cacheinfo").AddDynamicArguments("100644", sha, "somewher-over-the-rainbow").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: path})
  252. assert.NoError(t, err)
  253. treeSha, _, err := gitcmd.NewCommand("write-tree").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: path})
  254. assert.NoError(t, err)
  255. treeSha = strings.TrimSpace(treeSha)
  256. commitTimeStr := time.Now().Format(time.RFC3339)
  257. doerSig := user1.NewGitSig()
  258. env := append(os.Environ(),
  259. "GIT_AUTHOR_NAME="+doerSig.Name,
  260. "GIT_AUTHOR_EMAIL="+doerSig.Email,
  261. "GIT_AUTHOR_DATE="+commitTimeStr,
  262. "GIT_COMMITTER_NAME="+doerSig.Name,
  263. "GIT_COMMITTER_EMAIL="+doerSig.Email,
  264. "GIT_COMMITTER_DATE="+commitTimeStr,
  265. )
  266. messageBytes := new(bytes.Buffer)
  267. _, _ = messageBytes.WriteString("Unrelated")
  268. _, _ = messageBytes.WriteString("\n")
  269. stdout.Reset()
  270. err = gitcmd.NewCommand("commit-tree").AddDynamicArguments(treeSha).
  271. Run(t.Context(), &gitcmd.RunOpts{
  272. Env: env,
  273. Dir: path,
  274. Stdin: messageBytes,
  275. Stdout: &stdout,
  276. })
  277. assert.NoError(t, err)
  278. commitSha := strings.TrimSpace(stdout.String())
  279. _, _, err = gitcmd.NewCommand("branch", "unrelated").AddDynamicArguments(commitSha).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: path})
  280. assert.NoError(t, err)
  281. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
  282. // Use API to create a conflicting pr
  283. token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
  284. req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
  285. Head: "unrelated",
  286. Base: "base",
  287. Title: "create an unrelated pr",
  288. }).AddTokenAuth(token)
  289. session.MakeRequest(t, req, http.StatusCreated)
  290. // Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point...
  291. gitRepo, err := gitrepo.OpenRepository(t.Context(), repo1)
  292. assert.NoError(t, err)
  293. pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
  294. HeadRepoID: repo1.ID,
  295. BaseRepoID: repo1.ID,
  296. HeadBranch: "unrelated",
  297. BaseBranch: "base",
  298. })
  299. err = pull_service.Merge(t.Context(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED", false)
  300. assert.Error(t, err, "Merge should return an error due to unrelated")
  301. assert.True(t, pull_service.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
  302. gitRepo.Close()
  303. })
  304. }
  305. func TestFastForwardOnlyMerge(t *testing.T) {
  306. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  307. session := loginUser(t, "user1")
  308. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  309. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "update", "README.md", "Hello, World 2\n")
  310. // Use API to create a pr from update to master
  311. token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
  312. req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
  313. Head: "update",
  314. Base: "master",
  315. Title: "create a pr that can be fast-forward-only merged",
  316. }).AddTokenAuth(token)
  317. session.MakeRequest(t, req, http.StatusCreated)
  318. user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  319. Name: "user1",
  320. })
  321. repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
  322. OwnerID: user1.ID,
  323. Name: "repo1",
  324. })
  325. pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
  326. HeadRepoID: repo1.ID,
  327. BaseRepoID: repo1.ID,
  328. HeadBranch: "update",
  329. BaseBranch: "master",
  330. })
  331. gitRepo, err := git.OpenRepository(t.Context(), repo_model.RepoPath(user1.Name, repo1.Name))
  332. assert.NoError(t, err)
  333. err = pull_service.Merge(t.Context(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false)
  334. assert.NoError(t, err)
  335. gitRepo.Close()
  336. })
  337. }
  338. func TestCantFastForwardOnlyMergeDiverging(t *testing.T) {
  339. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  340. session := loginUser(t, "user1")
  341. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  342. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "diverging", "README.md", "Hello, World diverged\n")
  343. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World 2\n")
  344. // Use API to create a pr from diverging to update
  345. token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
  346. req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
  347. Head: "diverging",
  348. Base: "master",
  349. Title: "create a pr from a diverging branch",
  350. }).AddTokenAuth(token)
  351. session.MakeRequest(t, req, http.StatusCreated)
  352. user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  353. Name: "user1",
  354. })
  355. repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
  356. OwnerID: user1.ID,
  357. Name: "repo1",
  358. })
  359. pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
  360. HeadRepoID: repo1.ID,
  361. BaseRepoID: repo1.ID,
  362. HeadBranch: "diverging",
  363. BaseBranch: "master",
  364. })
  365. gitRepo, err := git.OpenRepository(t.Context(), repo_model.RepoPath(user1.Name, repo1.Name))
  366. assert.NoError(t, err)
  367. err = pull_service.Merge(t.Context(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "DIVERGING", false)
  368. assert.Error(t, err, "Merge should return an error due to being for a diverging branch")
  369. assert.True(t, pull_service.IsErrMergeDivergingFastForwardOnly(err), "Merge error is not a diverging fast-forward-only error")
  370. gitRepo.Close()
  371. })
  372. }
  373. func TestConflictChecking(t *testing.T) {
  374. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  375. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
  376. // Create new clean repo to test conflict checking.
  377. baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
  378. Name: "conflict-checking",
  379. Description: "Tempo repo",
  380. AutoInit: true,
  381. Readme: "Default",
  382. DefaultBranch: "main",
  383. })
  384. assert.NoError(t, err)
  385. assert.NotEmpty(t, baseRepo)
  386. // create a commit on new branch.
  387. _, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, user, &files_service.ChangeRepoFilesOptions{
  388. Files: []*files_service.ChangeRepoFile{
  389. {
  390. Operation: "create",
  391. TreePath: "important_file",
  392. ContentReader: strings.NewReader("Just a non-important file"),
  393. },
  394. },
  395. Message: "Add a important file",
  396. OldBranch: "main",
  397. NewBranch: "important-secrets",
  398. })
  399. assert.NoError(t, err)
  400. // create a commit on main branch.
  401. _, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, user, &files_service.ChangeRepoFilesOptions{
  402. Files: []*files_service.ChangeRepoFile{
  403. {
  404. Operation: "create",
  405. TreePath: "important_file",
  406. ContentReader: strings.NewReader("Not the same content :P"),
  407. },
  408. },
  409. Message: "Add a important file",
  410. OldBranch: "main",
  411. NewBranch: "main",
  412. })
  413. assert.NoError(t, err)
  414. // create Pull to merge the important-secrets branch into main branch.
  415. pullIssue := &issues_model.Issue{
  416. RepoID: baseRepo.ID,
  417. Title: "PR with conflict!",
  418. PosterID: user.ID,
  419. Poster: user,
  420. IsPull: true,
  421. }
  422. pullRequest := &issues_model.PullRequest{
  423. HeadRepoID: baseRepo.ID,
  424. BaseRepoID: baseRepo.ID,
  425. HeadBranch: "important-secrets",
  426. BaseBranch: "main",
  427. HeadRepo: baseRepo,
  428. BaseRepo: baseRepo,
  429. Type: issues_model.PullRequestGitea,
  430. }
  431. prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
  432. err = pull_service.NewPullRequest(t.Context(), prOpts)
  433. assert.NoError(t, err)
  434. issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
  435. assert.NoError(t, issue.LoadPullRequest(t.Context()))
  436. conflictingPR := issue.PullRequest
  437. // Ensure conflictedFiles is populated.
  438. assert.Len(t, conflictingPR.ConflictedFiles, 1)
  439. // Check if status is correct.
  440. assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
  441. // Ensure that mergeable returns false
  442. assert.False(t, conflictingPR.Mergeable(t.Context()))
  443. })
  444. }
  445. func TestPullRetargetChildOnBranchDelete(t *testing.T) {
  446. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  447. session := loginUser(t, "user1")
  448. testEditFileToNewBranch(t, session, "user2", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n")
  449. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  450. testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n(Edited - TestPullRetargetOnCleanup - child PR)")
  451. respBasePR := testPullCreate(t, session, "user2", "repo1", true, "master", "base-pr", "Base Pull Request")
  452. elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/")
  453. assert.Equal(t, "pulls", elemBasePR[3])
  454. respChildPR := testPullCreate(t, session, "user1", "repo1", false, "base-pr", "child-pr", "Child Pull Request")
  455. elemChildPR := strings.Split(test.RedirectURL(respChildPR), "/")
  456. assert.Equal(t, "pulls", elemChildPR[3])
  457. testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
  458. repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
  459. branchBasePR := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "base-pr"})
  460. assert.True(t, branchBasePR.IsDeleted)
  461. // Check child PR
  462. req := NewRequest(t, "GET", test.RedirectURL(respChildPR))
  463. resp := session.MakeRequest(t, req, http.StatusOK)
  464. htmlDoc := NewHTMLParser(t, resp.Body)
  465. targetBranch := htmlDoc.doc.Find("#branch_target>a").Text()
  466. prStatus := strings.TrimSpace(htmlDoc.doc.Find(".issue-title-meta>.issue-state-label").Text())
  467. assert.Equal(t, "master", targetBranch)
  468. assert.Equal(t, "Open", prStatus)
  469. })
  470. }
  471. func TestPullDontRetargetChildOnWrongRepo(t *testing.T) {
  472. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  473. session := loginUser(t, "user1")
  474. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  475. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n")
  476. testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n(Edited - TestPullDontRetargetChildOnWrongRepo - child PR)")
  477. respBasePR := testPullCreate(t, session, "user1", "repo1", false, "master", "base-pr", "Base Pull Request")
  478. elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/")
  479. assert.Equal(t, "pulls", elemBasePR[3])
  480. respChildPR := testPullCreate(t, session, "user1", "repo1", true, "base-pr", "child-pr", "Child Pull Request")
  481. elemChildPR := strings.Split(test.RedirectURL(respChildPR), "/")
  482. assert.Equal(t, "pulls", elemChildPR[3])
  483. defer test.MockVariableValue(&setting.Repository.PullRequest.RetargetChildrenOnMerge, false)()
  484. testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
  485. repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"})
  486. branchBasePR := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "base-pr"})
  487. assert.True(t, branchBasePR.IsDeleted)
  488. // Check child PR
  489. req := NewRequest(t, "GET", test.RedirectURL(respChildPR))
  490. resp := session.MakeRequest(t, req, http.StatusOK)
  491. htmlDoc := NewHTMLParser(t, resp.Body)
  492. // the branch has been deleted, so there is no a html tag instead of span
  493. targetBranch := htmlDoc.doc.Find("#branch_target>span").Text()
  494. prStatus := strings.TrimSpace(htmlDoc.doc.Find(".issue-title-meta>.issue-state-label").Text())
  495. assert.Equal(t, "base-pr", targetBranch)
  496. assert.Equal(t, "Closed", prStatus)
  497. })
  498. }
  499. func TestPullRequestMergedWithNoPermissionDeleteBranch(t *testing.T) {
  500. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  501. session := loginUser(t, "user4")
  502. testRepoFork(t, session, "user2", "repo1", "user4", "repo1", "")
  503. testEditFileToNewBranch(t, session, "user4", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n")
  504. respBasePR := testPullCreate(t, session, "user4", "repo1", false, "master", "base-pr", "Base Pull Request")
  505. elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/")
  506. assert.Equal(t, "pulls", elemBasePR[3])
  507. // user2 has no permission to delete branch of repo user1/repo1
  508. session2 := loginUser(t, "user2")
  509. testPullMerge(t, session2, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
  510. repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user4", Name: "repo1"})
  511. branchBasePR := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "base-pr"})
  512. // branch has not been deleted
  513. assert.False(t, branchBasePR.IsDeleted)
  514. })
  515. }
  516. func TestPullMergeIndexerNotifier(t *testing.T) {
  517. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  518. // create a pull request
  519. session := loginUser(t, "user1")
  520. testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
  521. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  522. createPullResp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "Indexer notifier test pull")
  523. assert.NoError(t, queue.GetManager().FlushAll(t.Context(), 0))
  524. time.Sleep(time.Second)
  525. repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
  526. OwnerName: "user2",
  527. Name: "repo1",
  528. })
  529. issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
  530. RepoID: repo1.ID,
  531. Title: "Indexer notifier test pull",
  532. IsPull: true,
  533. IsClosed: false,
  534. })
  535. // build the request for searching issues
  536. link, _ := url.Parse("/api/v1/repos/issues/search")
  537. query := url.Values{}
  538. query.Add("state", "closed")
  539. query.Add("type", "pulls")
  540. query.Add("q", "notifier")
  541. link.RawQuery = query.Encode()
  542. // search issues
  543. searchIssuesResp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
  544. var apiIssuesBefore []*api.Issue
  545. DecodeJSON(t, searchIssuesResp, &apiIssuesBefore)
  546. assert.Empty(t, apiIssuesBefore)
  547. // merge the pull request
  548. elem := strings.Split(test.RedirectURL(createPullResp), "/")
  549. assert.Equal(t, "pulls", elem[3])
  550. testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
  551. // check if the issue is closed
  552. issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
  553. ID: issue.ID,
  554. })
  555. assert.True(t, issue.IsClosed)
  556. assert.NoError(t, queue.GetManager().FlushAll(t.Context(), 0))
  557. time.Sleep(time.Second)
  558. // search issues again
  559. searchIssuesResp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
  560. var apiIssuesAfter []*api.Issue
  561. DecodeJSON(t, searchIssuesResp, &apiIssuesAfter)
  562. if assert.Len(t, apiIssuesAfter, 1) {
  563. assert.Equal(t, issue.ID, apiIssuesAfter[0].ID)
  564. }
  565. })
  566. }
  567. func testResetRepo(t *testing.T, repoPath, branch, commitID string) {
  568. f, err := os.OpenFile(filepath.Join(repoPath, "refs", "heads", branch), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
  569. assert.NoError(t, err)
  570. _, err = f.WriteString(commitID + "\n")
  571. assert.NoError(t, err)
  572. f.Close()
  573. repo, err := git.OpenRepository(t.Context(), repoPath)
  574. assert.NoError(t, err)
  575. defer repo.Close()
  576. id, err := repo.GetBranchCommitID(branch)
  577. assert.NoError(t, err)
  578. assert.Equal(t, commitID, id)
  579. }
  580. func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
  581. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  582. // create a pull request
  583. session := loginUser(t, "user1")
  584. user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
  585. forkedName := "repo1-1"
  586. testRepoFork(t, session, "user2", "repo1", "user1", forkedName, "")
  587. defer func() {
  588. testDeleteRepository(t, session, "user1", forkedName)
  589. }()
  590. testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n")
  591. testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull")
  592. baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
  593. forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName})
  594. pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
  595. BaseRepoID: baseRepo.ID,
  596. BaseBranch: "master",
  597. HeadRepoID: forkedRepo.ID,
  598. HeadBranch: "master",
  599. })
  600. // add protected branch for commit status
  601. csrf := GetUserCSRFToken(t, session)
  602. // Change the "master" branch to "protected"
  603. req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
  604. "_csrf": csrf,
  605. "rule_name": "master",
  606. "enable_push": "true",
  607. "enable_status_check": "true",
  608. "status_check_contexts": "gitea/actions",
  609. })
  610. session.MakeRequest(t, req, http.StatusSeeOther)
  611. oldAutoMergeAddToQueue := automergequeue.AddToQueue
  612. addToQueueShaChan := make(chan string, 1)
  613. automergequeue.AddToQueue = func(pr *issues_model.PullRequest, sha string) {
  614. addToQueueShaChan <- sha
  615. }
  616. // first time insert automerge record, return true
  617. scheduled, err := automerge.ScheduleAutoMerge(t.Context(), user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
  618. assert.NoError(t, err)
  619. assert.True(t, scheduled)
  620. // and the pr should be added to automergequeue, in case it is already "mergeable"
  621. select {
  622. case <-addToQueueShaChan:
  623. case <-time.After(time.Second):
  624. assert.FailNow(t, "Timeout: nothing was added to automergequeue")
  625. }
  626. automergequeue.AddToQueue = oldAutoMergeAddToQueue
  627. // second time insert automerge record, return false because it does exist
  628. scheduled, err = automerge.ScheduleAutoMerge(t.Context(), user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
  629. assert.Error(t, err)
  630. assert.False(t, scheduled)
  631. // reload pr again
  632. pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
  633. assert.False(t, pr.HasMerged)
  634. assert.Empty(t, pr.MergedCommitID)
  635. // update commit status to success, then it should be merged automatically
  636. baseGitRepo, err := gitrepo.OpenRepository(t.Context(), baseRepo)
  637. assert.NoError(t, err)
  638. sha, err := baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName())
  639. assert.NoError(t, err)
  640. masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
  641. assert.NoError(t, err)
  642. branches, _, err := baseGitRepo.GetBranchNames(0, 100)
  643. assert.NoError(t, err)
  644. assert.ElementsMatch(t, []string{"sub-home-md-img-check", "home-md-img-check", "pr-to-update", "branch2", "DefaultBranch", "develop", "feature/1", "master"}, branches)
  645. baseGitRepo.Close()
  646. defer func() {
  647. testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
  648. }()
  649. err = commitstatus_service.CreateCommitStatus(t.Context(), baseRepo, user1, sha, &git_model.CommitStatus{
  650. State: commitstatus.CommitStatusSuccess,
  651. TargetURL: "https://gitea.com",
  652. Context: "gitea/actions",
  653. })
  654. assert.NoError(t, err)
  655. assert.Eventually(t, func() bool {
  656. pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
  657. return pr.HasMerged
  658. }, 2*time.Second, 100*time.Millisecond)
  659. assert.NotEmpty(t, pr.MergedCommitID)
  660. unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
  661. })
  662. }
  663. func TestPullAutoMergeAfterCommitStatusSucceedAndApproval(t *testing.T) {
  664. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  665. // create a pull request
  666. session := loginUser(t, "user1")
  667. user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
  668. forkedName := "repo1-2"
  669. testRepoFork(t, session, "user2", "repo1", "user1", forkedName, "")
  670. defer func() {
  671. testDeleteRepository(t, session, "user1", forkedName)
  672. }()
  673. testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n")
  674. testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull")
  675. baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
  676. forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName})
  677. pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
  678. BaseRepoID: baseRepo.ID,
  679. BaseBranch: "master",
  680. HeadRepoID: forkedRepo.ID,
  681. HeadBranch: "master",
  682. })
  683. // add protected branch for commit status
  684. csrf := GetUserCSRFToken(t, session)
  685. // Change master branch to protected
  686. req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
  687. "_csrf": csrf,
  688. "rule_name": "master",
  689. "enable_push": "true",
  690. "enable_status_check": "true",
  691. "status_check_contexts": "gitea/actions",
  692. "required_approvals": "1",
  693. })
  694. session.MakeRequest(t, req, http.StatusSeeOther)
  695. // first time insert automerge record, return true
  696. scheduled, err := automerge.ScheduleAutoMerge(t.Context(), user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
  697. assert.NoError(t, err)
  698. assert.True(t, scheduled)
  699. // second time insert automerge record, return false because it does exist
  700. scheduled, err = automerge.ScheduleAutoMerge(t.Context(), user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
  701. assert.Error(t, err)
  702. assert.False(t, scheduled)
  703. // reload pr again
  704. pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
  705. assert.False(t, pr.HasMerged)
  706. assert.Empty(t, pr.MergedCommitID)
  707. // update commit status to success, then it should be merged automatically
  708. baseGitRepo, err := gitrepo.OpenRepository(t.Context(), baseRepo)
  709. assert.NoError(t, err)
  710. sha, err := baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName())
  711. assert.NoError(t, err)
  712. masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
  713. assert.NoError(t, err)
  714. baseGitRepo.Close()
  715. defer func() {
  716. testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
  717. }()
  718. err = commitstatus_service.CreateCommitStatus(t.Context(), baseRepo, user1, sha, &git_model.CommitStatus{
  719. State: commitstatus.CommitStatusSuccess,
  720. TargetURL: "https://gitea.com",
  721. Context: "gitea/actions",
  722. })
  723. assert.NoError(t, err)
  724. time.Sleep(2 * time.Second)
  725. // reload pr again
  726. pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
  727. assert.False(t, pr.HasMerged)
  728. assert.Empty(t, pr.MergedCommitID)
  729. // approve the PR from non-author
  730. approveSession := loginUser(t, "user2")
  731. req = NewRequest(t, "GET", fmt.Sprintf("/user2/repo1/pulls/%d", pr.Index))
  732. resp := approveSession.MakeRequest(t, req, http.StatusOK)
  733. htmlDoc := NewHTMLParser(t, resp.Body)
  734. testSubmitReview(t, approveSession, htmlDoc.GetCSRF(), "user2", "repo1", strconv.Itoa(int(pr.Index)), sha, "approve", http.StatusOK)
  735. time.Sleep(2 * time.Second)
  736. // reload pr again
  737. pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
  738. assert.True(t, pr.HasMerged)
  739. assert.NotEmpty(t, pr.MergedCommitID)
  740. unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
  741. })
  742. }
  743. func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing.T) {
  744. onGiteaRun(t, func(t *testing.T, u *url.URL) {
  745. // create a pull request
  746. baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
  747. dstPath := t.TempDir()
  748. u.Path = baseAPITestContext.GitPath()
  749. u.User = url.UserPassword("user2", userPassword)
  750. t.Run("Clone", doGitClone(dstPath, u))
  751. err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
  752. assert.NoError(t, err)
  753. err = git.AddChanges(t.Context(), dstPath, true)
  754. assert.NoError(t, err)
  755. err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
  756. Committer: &git.Signature{
  757. Email: "user2@example.com",
  758. Name: "user2",
  759. When: time.Now(),
  760. },
  761. Author: &git.Signature{
  762. Email: "user2@example.com",
  763. Name: "user2",
  764. When: time.Now(),
  765. },
  766. Message: "Testing commit 1",
  767. })
  768. assert.NoError(t, err)
  769. stderrBuf := &bytes.Buffer{}
  770. err = gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master", "-o").
  771. AddDynamicArguments(`topic=test/head2`).
  772. AddArguments("-o").
  773. AddDynamicArguments(`title="create a test pull request with agit"`).
  774. AddArguments("-o").
  775. AddDynamicArguments(`description="This PR is a test pull request which created with agit"`).
  776. Run(t.Context(), &gitcmd.RunOpts{Dir: dstPath, Stderr: stderrBuf})
  777. assert.NoError(t, err)
  778. assert.Contains(t, stderrBuf.String(), setting.AppURL+"user2/repo1/pulls/6")
  779. baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
  780. pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
  781. Flow: issues_model.PullRequestFlowAGit,
  782. BaseRepoID: baseRepo.ID,
  783. BaseBranch: "master",
  784. HeadRepoID: baseRepo.ID,
  785. HeadBranch: "user2/test/head2",
  786. })
  787. session := loginUser(t, "user1")
  788. // add protected branch for commit status
  789. csrf := GetUserCSRFToken(t, session)
  790. // Change master branch to protected
  791. req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
  792. "_csrf": csrf,
  793. "rule_name": "master",
  794. "enable_push": "true",
  795. "enable_status_check": "true",
  796. "status_check_contexts": "gitea/actions",
  797. "required_approvals": "1",
  798. })
  799. session.MakeRequest(t, req, http.StatusSeeOther)
  800. user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
  801. // first time insert automerge record, return true
  802. scheduled, err := automerge.ScheduleAutoMerge(t.Context(), user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
  803. assert.NoError(t, err)
  804. assert.True(t, scheduled)
  805. // second time insert automerge record, return false because it does exist
  806. scheduled, err = automerge.ScheduleAutoMerge(t.Context(), user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
  807. assert.Error(t, err)
  808. assert.False(t, scheduled)
  809. // reload pr again
  810. pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
  811. assert.False(t, pr.HasMerged)
  812. assert.Empty(t, pr.MergedCommitID)
  813. // update commit status to success, then it should be merged automatically
  814. baseGitRepo, err := gitrepo.OpenRepository(t.Context(), baseRepo)
  815. assert.NoError(t, err)
  816. sha, err := baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName())
  817. assert.NoError(t, err)
  818. masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
  819. assert.NoError(t, err)
  820. baseGitRepo.Close()
  821. defer func() {
  822. testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
  823. }()
  824. err = commitstatus_service.CreateCommitStatus(t.Context(), baseRepo, user1, sha, &git_model.CommitStatus{
  825. State: commitstatus.CommitStatusSuccess,
  826. TargetURL: "https://gitea.com",
  827. Context: "gitea/actions",
  828. })
  829. assert.NoError(t, err)
  830. // reload pr again
  831. pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
  832. assert.False(t, pr.HasMerged)
  833. assert.Empty(t, pr.MergedCommitID)
  834. // approve the PR from non-author
  835. approveSession := loginUser(t, "user1")
  836. req = NewRequest(t, "GET", fmt.Sprintf("/user2/repo1/pulls/%d", pr.Index))
  837. resp := approveSession.MakeRequest(t, req, http.StatusOK)
  838. htmlDoc := NewHTMLParser(t, resp.Body)
  839. testSubmitReview(t, approveSession, htmlDoc.GetCSRF(), "user2", "repo1", strconv.Itoa(int(pr.Index)), sha, "approve", http.StatusOK)
  840. // reload pr again
  841. pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
  842. assert.True(t, pr.HasMerged)
  843. assert.NotEmpty(t, pr.MergedCommitID)
  844. unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
  845. })
  846. }
  847. func TestPullNonMergeForAdminWithBranchProtection(t *testing.T) {
  848. onGiteaRun(t, func(t *testing.T, u *url.URL) {
  849. // create a pull request
  850. session := loginUser(t, "user1")
  851. forkedName := "repo1-1"
  852. testRepoFork(t, session, "user2", "repo1", "user1", forkedName, "")
  853. defer testDeleteRepository(t, session, "user1", forkedName)
  854. testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n")
  855. testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull")
  856. baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
  857. forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName})
  858. unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
  859. BaseRepoID: baseRepo.ID,
  860. BaseBranch: "master",
  861. HeadRepoID: forkedRepo.ID,
  862. HeadBranch: "master",
  863. })
  864. // add protected branch for commit status
  865. csrf := GetUserCSRFToken(t, session)
  866. // Change master branch to protected
  867. pbCreateReq := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
  868. "_csrf": csrf,
  869. "rule_name": "master",
  870. "enable_push": "true",
  871. "enable_status_check": "true",
  872. "status_check_contexts": "gitea/actions",
  873. "block_admin_merge_override": "true",
  874. })
  875. session.MakeRequest(t, pbCreateReq, http.StatusSeeOther)
  876. token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
  877. mergeReq := NewRequestWithValues(t, "POST", "/api/v1/repos/user2/repo1/pulls/6/merge", map[string]string{
  878. "_csrf": csrf,
  879. "head_commit_id": "",
  880. "merge_when_checks_succeed": "false",
  881. "force_merge": "true",
  882. "do": "rebase",
  883. }).AddTokenAuth(token)
  884. session.MakeRequest(t, mergeReq, http.StatusMethodNotAllowed)
  885. })
  886. }