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.

341 lines
12 KiB

  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(".comments .merge .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. prepareTestEnv(t)
  88. hookTasks, err := models.HookTasks(1, 1) //Retrieve previous hook number
  89. assert.NoError(t, err)
  90. hookTasksLenBefore := len(hookTasks)
  91. session := loginUser(t, "user1")
  92. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  93. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  94. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  95. elem := strings.Split(test.RedirectURL(resp), "/")
  96. assert.EqualValues(t, "pulls", elem[3])
  97. testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleRebaseMerge)
  98. hookTasks, err = models.HookTasks(1, 1)
  99. assert.NoError(t, err)
  100. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  101. })
  102. }
  103. func TestPullSquash(t *testing.T) {
  104. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  105. prepareTestEnv(t)
  106. hookTasks, err := models.HookTasks(1, 1) //Retrieve previous hook number
  107. assert.NoError(t, err)
  108. hookTasksLenBefore := len(hookTasks)
  109. session := loginUser(t, "user1")
  110. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  111. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  112. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
  113. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  114. elem := strings.Split(test.RedirectURL(resp), "/")
  115. assert.EqualValues(t, "pulls", elem[3])
  116. testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleSquash)
  117. hookTasks, err = models.HookTasks(1, 1)
  118. assert.NoError(t, err)
  119. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  120. })
  121. }
  122. func TestPullCleanUpAfterMerge(t *testing.T) {
  123. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  124. prepareTestEnv(t)
  125. session := loginUser(t, "user1")
  126. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  127. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n")
  128. resp := testPullCreate(t, session, "user1", "repo1", "feature/test", "This is a pull title")
  129. elem := strings.Split(test.RedirectURL(resp), "/")
  130. assert.EqualValues(t, "pulls", elem[3])
  131. testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleMerge)
  132. // Check PR branch deletion
  133. resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4])
  134. respJSON := struct {
  135. Redirect string
  136. }{}
  137. DecodeJSON(t, resp, &respJSON)
  138. assert.NotEmpty(t, respJSON.Redirect, "Redirected URL is not found")
  139. elem = strings.Split(respJSON.Redirect, "/")
  140. assert.EqualValues(t, "pulls", elem[3])
  141. // Check branch deletion result
  142. req := NewRequest(t, "GET", respJSON.Redirect)
  143. resp = session.MakeRequest(t, req, http.StatusOK)
  144. htmlDoc := NewHTMLParser(t, resp.Body)
  145. resultMsg := htmlDoc.doc.Find(".ui.message>p").Text()
  146. assert.EqualValues(t, "Branch 'user1/feature/test' has been deleted.", resultMsg)
  147. })
  148. }
  149. func TestCantMergeWorkInProgress(t *testing.T) {
  150. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  151. prepareTestEnv(t)
  152. session := loginUser(t, "user1")
  153. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  154. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  155. resp := testPullCreate(t, session, "user1", "repo1", "master", "[wip] This is a pull title")
  156. req := NewRequest(t, "GET", resp.Header().Get("Location"))
  157. resp = session.MakeRequest(t, req, http.StatusOK)
  158. htmlDoc := NewHTMLParser(t, resp.Body)
  159. text := strings.TrimSpace(htmlDoc.doc.Find(".attached.header > .text.grey").Last().Text())
  160. assert.NotEmpty(t, text, "Can't find WIP text")
  161. // remove <strong /> from lang
  162. expected := i18n.Tr("en", "repo.pulls.cannot_merge_work_in_progress", "[wip]")
  163. replacer := strings.NewReplacer("<strong>", "", "</strong>", "")
  164. assert.Equal(t, replacer.Replace(expected), text, "Unable to find WIP text")
  165. })
  166. }
  167. func TestCantMergeConflict(t *testing.T) {
  168. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  169. prepareTestEnv(t)
  170. session := loginUser(t, "user1")
  171. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  172. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
  173. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
  174. // Use API to create a conflicting pr
  175. token := getTokenForLoggedInUser(t, session)
  176. req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
  177. Head: "conflict",
  178. Base: "base",
  179. Title: "create a conflicting pr",
  180. })
  181. session.MakeRequest(t, req, 201)
  182. // Now this PR will be marked conflict - or at least a race will do - so drop down to pure code at this point...
  183. user1 := models.AssertExistsAndLoadBean(t, &models.User{
  184. Name: "user1",
  185. }).(*models.User)
  186. repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{
  187. OwnerID: user1.ID,
  188. Name: "repo1",
  189. }).(*models.Repository)
  190. pr := models.AssertExistsAndLoadBean(t, &models.PullRequest{
  191. HeadRepoID: repo1.ID,
  192. BaseRepoID: repo1.ID,
  193. HeadBranch: "conflict",
  194. BaseBranch: "base",
  195. }).(*models.PullRequest)
  196. gitRepo, err := git.OpenRepository(models.RepoPath(user1.Name, repo1.Name))
  197. assert.NoError(t, err)
  198. err = pull.Merge(pr, user1, gitRepo, models.MergeStyleMerge, "CONFLICT")
  199. assert.Error(t, err, "Merge should return an error due to conflict")
  200. assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error")
  201. err = pull.Merge(pr, user1, gitRepo, models.MergeStyleRebase, "CONFLICT")
  202. assert.Error(t, err, "Merge should return an error due to conflict")
  203. assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
  204. })
  205. }
  206. func TestCantMergeUnrelated(t *testing.T) {
  207. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  208. prepareTestEnv(t)
  209. session := loginUser(t, "user1")
  210. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  211. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
  212. // Now we want to create a commit on a branch that is totally unrelated to our current head
  213. // Drop down to pure code at this point
  214. user1 := models.AssertExistsAndLoadBean(t, &models.User{
  215. Name: "user1",
  216. }).(*models.User)
  217. repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{
  218. OwnerID: user1.ID,
  219. Name: "repo1",
  220. }).(*models.Repository)
  221. path := models.RepoPath(user1.Name, repo1.Name)
  222. _, err := git.NewCommand("read-tree", "--empty").RunInDir(path)
  223. assert.NoError(t, err)
  224. stdin := bytes.NewBufferString("Unrelated File")
  225. var stdout strings.Builder
  226. err = git.NewCommand("hash-object", "-w", "--stdin").RunInDirFullPipeline(path, &stdout, nil, stdin)
  227. assert.NoError(t, err)
  228. sha := strings.TrimSpace(stdout.String())
  229. _, err = git.NewCommand("update-index", "--add", "--replace", "--cacheinfo", "100644", sha, "somewher-over-the-rainbow").RunInDir(path)
  230. assert.NoError(t, err)
  231. treeSha, err := git.NewCommand("write-tree").RunInDir(path)
  232. assert.NoError(t, err)
  233. treeSha = strings.TrimSpace(treeSha)
  234. commitTimeStr := time.Now().Format(time.RFC3339)
  235. doerSig := user1.NewGitSig()
  236. env := append(os.Environ(),
  237. "GIT_AUTHOR_NAME="+doerSig.Name,
  238. "GIT_AUTHOR_EMAIL="+doerSig.Email,
  239. "GIT_AUTHOR_DATE="+commitTimeStr,
  240. "GIT_COMMITTER_NAME="+doerSig.Name,
  241. "GIT_COMMITTER_EMAIL="+doerSig.Email,
  242. "GIT_COMMITTER_DATE="+commitTimeStr,
  243. )
  244. messageBytes := new(bytes.Buffer)
  245. _, _ = messageBytes.WriteString("Unrelated")
  246. _, _ = messageBytes.WriteString("\n")
  247. stdout.Reset()
  248. err = git.NewCommand("commit-tree", treeSha).RunInDirTimeoutEnvFullPipeline(env, -1, path, &stdout, nil, messageBytes)
  249. assert.NoError(t, err)
  250. commitSha := strings.TrimSpace(stdout.String())
  251. _, err = git.NewCommand("branch", "unrelated", commitSha).RunInDir(path)
  252. assert.NoError(t, err)
  253. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
  254. // Use API to create a conflicting pr
  255. token := getTokenForLoggedInUser(t, session)
  256. req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
  257. Head: "unrelated",
  258. Base: "base",
  259. Title: "create an unrelated pr",
  260. })
  261. session.MakeRequest(t, req, 201)
  262. // Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point...
  263. gitRepo, err := git.OpenRepository(path)
  264. assert.NoError(t, err)
  265. pr := models.AssertExistsAndLoadBean(t, &models.PullRequest{
  266. HeadRepoID: repo1.ID,
  267. BaseRepoID: repo1.ID,
  268. HeadBranch: "unrelated",
  269. BaseBranch: "base",
  270. }).(*models.PullRequest)
  271. err = pull.Merge(pr, user1, gitRepo, models.MergeStyleMerge, "UNRELATED")
  272. assert.Error(t, err, "Merge should return an error due to unrelated")
  273. assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
  274. })
  275. }