You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

333 lines
12 KiB

Only check for conflicts/merging if the PR has not been merged in the interim (#10132) * Only check for merging if the PR has not been merged in the interim * fixup! Only check for merging if the PR has not been merged in the interim * Try to fix test failure * Use PR2 not PR1 in tests as PR1 merges automatically * return already merged error * enforce locking * enforce locking - fix-test * enforce locking - fix-testx2 * enforce locking - fix-testx3 * move pullrequest checking to after merge This might improve the chance that the race does not affect us but does not prevent it. * Remove minor race with getting merge commit id * fixup * move check pr after merge * Remove unnecessary prepareTestEnv - onGiteaRun does this for us * Add information about when merging occuring * fix fmt * More logging * Attempt to fix mysql * Try MySQL fix again * try again * Try again?! * Try again?! * Sigh * remove the count - perhaps that will help * next remove the update id * next remove the update id - make it updated_unix instead * On failure to merge ensure that the pr is rechecked for conflict errors * On failure to merge ensure that the pr is rechecked for conflict errors * Update models/pull.go * Update models/pull.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Apply suggestions from code review Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
4 years ago
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package integrations
  5. import (
  6. "bytes"
  7. "fmt"
  8. "net/http"
  9. "net/http/httptest"
  10. "net/url"
  11. "os"
  12. "path"
  13. "strings"
  14. "testing"
  15. "time"
  16. "code.gitea.io/gitea/models"
  17. "code.gitea.io/gitea/modules/git"
  18. api "code.gitea.io/gitea/modules/structs"
  19. "code.gitea.io/gitea/modules/test"
  20. "code.gitea.io/gitea/services/pull"
  21. "github.com/stretchr/testify/assert"
  22. "github.com/unknwon/i18n"
  23. )
  24. func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle models.MergeStyle) *httptest.ResponseRecorder {
  25. req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
  26. resp := session.MakeRequest(t, req, http.StatusOK)
  27. // Click the little green button to create a pull
  28. htmlDoc := NewHTMLParser(t, resp.Body)
  29. link, exists := htmlDoc.doc.Find(".ui.form." + string(mergeStyle) + "-fields > form").Attr("action")
  30. assert.True(t, exists, "The template has changed")
  31. req = NewRequestWithValues(t, "POST", link, map[string]string{
  32. "_csrf": htmlDoc.GetCSRF(),
  33. "do": string(mergeStyle),
  34. })
  35. resp = session.MakeRequest(t, req, http.StatusFound)
  36. return resp
  37. }
  38. func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder {
  39. req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
  40. resp := session.MakeRequest(t, req, http.StatusOK)
  41. // Click the little green button to create a pull
  42. htmlDoc := NewHTMLParser(t, resp.Body)
  43. link, exists := htmlDoc.doc.Find(".timeline-item .delete-button").Attr("data-url")
  44. assert.True(t, exists, "The template has changed")
  45. req = NewRequestWithValues(t, "POST", link, map[string]string{
  46. "_csrf": htmlDoc.GetCSRF(),
  47. })
  48. resp = session.MakeRequest(t, req, http.StatusOK)
  49. return resp
  50. }
  51. func TestPullMerge(t *testing.T) {
  52. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  53. hookTasks, err := models.HookTasks(1, 1) //Retrieve previous hook number
  54. assert.NoError(t, err)
  55. hookTasksLenBefore := len(hookTasks)
  56. session := loginUser(t, "user1")
  57. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  58. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  59. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  60. elem := strings.Split(test.RedirectURL(resp), "/")
  61. assert.EqualValues(t, "pulls", elem[3])
  62. testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleMerge)
  63. hookTasks, err = models.HookTasks(1, 1)
  64. assert.NoError(t, err)
  65. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  66. })
  67. }
  68. func TestPullRebase(t *testing.T) {
  69. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  70. hookTasks, err := models.HookTasks(1, 1) //Retrieve previous hook number
  71. assert.NoError(t, err)
  72. hookTasksLenBefore := len(hookTasks)
  73. session := loginUser(t, "user1")
  74. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  75. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  76. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  77. elem := strings.Split(test.RedirectURL(resp), "/")
  78. assert.EqualValues(t, "pulls", elem[3])
  79. testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleRebase)
  80. hookTasks, err = models.HookTasks(1, 1)
  81. assert.NoError(t, err)
  82. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  83. })
  84. }
  85. func TestPullRebaseMerge(t *testing.T) {
  86. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  87. hookTasks, err := models.HookTasks(1, 1) //Retrieve previous hook number
  88. assert.NoError(t, err)
  89. hookTasksLenBefore := len(hookTasks)
  90. session := loginUser(t, "user1")
  91. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  92. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  93. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  94. elem := strings.Split(test.RedirectURL(resp), "/")
  95. assert.EqualValues(t, "pulls", elem[3])
  96. testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleRebaseMerge)
  97. hookTasks, err = models.HookTasks(1, 1)
  98. assert.NoError(t, err)
  99. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  100. })
  101. }
  102. func TestPullSquash(t *testing.T) {
  103. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  104. hookTasks, err := models.HookTasks(1, 1) //Retrieve previous hook number
  105. assert.NoError(t, err)
  106. hookTasksLenBefore := len(hookTasks)
  107. session := loginUser(t, "user1")
  108. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  109. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  110. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
  111. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  112. elem := strings.Split(test.RedirectURL(resp), "/")
  113. assert.EqualValues(t, "pulls", elem[3])
  114. testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleSquash)
  115. hookTasks, err = models.HookTasks(1, 1)
  116. assert.NoError(t, err)
  117. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  118. })
  119. }
  120. func TestPullCleanUpAfterMerge(t *testing.T) {
  121. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  122. session := loginUser(t, "user1")
  123. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  124. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n")
  125. resp := testPullCreate(t, session, "user1", "repo1", "feature/test", "This is a pull title")
  126. elem := strings.Split(test.RedirectURL(resp), "/")
  127. assert.EqualValues(t, "pulls", elem[3])
  128. testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleMerge)
  129. // Check PR branch deletion
  130. resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4])
  131. respJSON := struct {
  132. Redirect string
  133. }{}
  134. DecodeJSON(t, resp, &respJSON)
  135. assert.NotEmpty(t, respJSON.Redirect, "Redirected URL is not found")
  136. elem = strings.Split(respJSON.Redirect, "/")
  137. assert.EqualValues(t, "pulls", elem[3])
  138. // Check branch deletion result
  139. req := NewRequest(t, "GET", respJSON.Redirect)
  140. resp = session.MakeRequest(t, req, http.StatusOK)
  141. htmlDoc := NewHTMLParser(t, resp.Body)
  142. resultMsg := htmlDoc.doc.Find(".ui.message>p").Text()
  143. assert.EqualValues(t, "Branch 'user1/feature/test' has been deleted.", resultMsg)
  144. })
  145. }
  146. func TestCantMergeWorkInProgress(t *testing.T) {
  147. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  148. session := loginUser(t, "user1")
  149. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  150. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  151. resp := testPullCreate(t, session, "user1", "repo1", "master", "[wip] This is a pull title")
  152. req := NewRequest(t, "GET", resp.Header().Get("Location"))
  153. resp = session.MakeRequest(t, req, http.StatusOK)
  154. htmlDoc := NewHTMLParser(t, resp.Body)
  155. text := strings.TrimSpace(htmlDoc.doc.Find(".attached.merge-section.no-header > .text.grey").Last().Text())
  156. assert.NotEmpty(t, text, "Can't find WIP text")
  157. // remove <strong /> from lang
  158. expected := i18n.Tr("en", "repo.pulls.cannot_merge_work_in_progress", "[wip]")
  159. replacer := strings.NewReplacer("<strong>", "", "</strong>", "")
  160. assert.Equal(t, replacer.Replace(expected), text, "Unable to find WIP text")
  161. })
  162. }
  163. func TestCantMergeConflict(t *testing.T) {
  164. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  165. session := loginUser(t, "user1")
  166. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  167. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
  168. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
  169. // Use API to create a conflicting pr
  170. token := getTokenForLoggedInUser(t, session)
  171. req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
  172. Head: "conflict",
  173. Base: "base",
  174. Title: "create a conflicting pr",
  175. })
  176. session.MakeRequest(t, req, 201)
  177. // Now this PR will be marked conflict - or at least a race will do - so drop down to pure code at this point...
  178. user1 := models.AssertExistsAndLoadBean(t, &models.User{
  179. Name: "user1",
  180. }).(*models.User)
  181. repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{
  182. OwnerID: user1.ID,
  183. Name: "repo1",
  184. }).(*models.Repository)
  185. pr := models.AssertExistsAndLoadBean(t, &models.PullRequest{
  186. HeadRepoID: repo1.ID,
  187. BaseRepoID: repo1.ID,
  188. HeadBranch: "conflict",
  189. BaseBranch: "base",
  190. }).(*models.PullRequest)
  191. gitRepo, err := git.OpenRepository(models.RepoPath(user1.Name, repo1.Name))
  192. assert.NoError(t, err)
  193. err = pull.Merge(pr, user1, gitRepo, models.MergeStyleMerge, "CONFLICT")
  194. assert.Error(t, err, "Merge should return an error due to conflict")
  195. assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error")
  196. err = pull.Merge(pr, user1, gitRepo, models.MergeStyleRebase, "CONFLICT")
  197. assert.Error(t, err, "Merge should return an error due to conflict")
  198. assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
  199. })
  200. }
  201. func TestCantMergeUnrelated(t *testing.T) {
  202. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  203. session := loginUser(t, "user1")
  204. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  205. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
  206. // Now we want to create a commit on a branch that is totally unrelated to our current head
  207. // Drop down to pure code at this point
  208. user1 := models.AssertExistsAndLoadBean(t, &models.User{
  209. Name: "user1",
  210. }).(*models.User)
  211. repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{
  212. OwnerID: user1.ID,
  213. Name: "repo1",
  214. }).(*models.Repository)
  215. path := models.RepoPath(user1.Name, repo1.Name)
  216. _, err := git.NewCommand("read-tree", "--empty").RunInDir(path)
  217. assert.NoError(t, err)
  218. stdin := bytes.NewBufferString("Unrelated File")
  219. var stdout strings.Builder
  220. err = git.NewCommand("hash-object", "-w", "--stdin").RunInDirFullPipeline(path, &stdout, nil, stdin)
  221. assert.NoError(t, err)
  222. sha := strings.TrimSpace(stdout.String())
  223. _, err = git.NewCommand("update-index", "--add", "--replace", "--cacheinfo", "100644", sha, "somewher-over-the-rainbow").RunInDir(path)
  224. assert.NoError(t, err)
  225. treeSha, err := git.NewCommand("write-tree").RunInDir(path)
  226. assert.NoError(t, err)
  227. treeSha = strings.TrimSpace(treeSha)
  228. commitTimeStr := time.Now().Format(time.RFC3339)
  229. doerSig := user1.NewGitSig()
  230. env := append(os.Environ(),
  231. "GIT_AUTHOR_NAME="+doerSig.Name,
  232. "GIT_AUTHOR_EMAIL="+doerSig.Email,
  233. "GIT_AUTHOR_DATE="+commitTimeStr,
  234. "GIT_COMMITTER_NAME="+doerSig.Name,
  235. "GIT_COMMITTER_EMAIL="+doerSig.Email,
  236. "GIT_COMMITTER_DATE="+commitTimeStr,
  237. )
  238. messageBytes := new(bytes.Buffer)
  239. _, _ = messageBytes.WriteString("Unrelated")
  240. _, _ = messageBytes.WriteString("\n")
  241. stdout.Reset()
  242. err = git.NewCommand("commit-tree", treeSha).RunInDirTimeoutEnvFullPipeline(env, -1, path, &stdout, nil, messageBytes)
  243. assert.NoError(t, err)
  244. commitSha := strings.TrimSpace(stdout.String())
  245. _, err = git.NewCommand("branch", "unrelated", commitSha).RunInDir(path)
  246. assert.NoError(t, err)
  247. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
  248. // Use API to create a conflicting pr
  249. token := getTokenForLoggedInUser(t, session)
  250. req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
  251. Head: "unrelated",
  252. Base: "base",
  253. Title: "create an unrelated pr",
  254. })
  255. session.MakeRequest(t, req, 201)
  256. // Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point...
  257. gitRepo, err := git.OpenRepository(path)
  258. assert.NoError(t, err)
  259. pr := models.AssertExistsAndLoadBean(t, &models.PullRequest{
  260. HeadRepoID: repo1.ID,
  261. BaseRepoID: repo1.ID,
  262. HeadBranch: "unrelated",
  263. BaseBranch: "base",
  264. }).(*models.PullRequest)
  265. err = pull.Merge(pr, user1, gitRepo, models.MergeStyleMerge, "UNRELATED")
  266. assert.Error(t, err, "Merge should return an error due to unrelated")
  267. assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
  268. })
  269. }