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.

pull_merge_test.go 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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/models/unittest"
  18. user_model "code.gitea.io/gitea/models/user"
  19. "code.gitea.io/gitea/models/webhook"
  20. "code.gitea.io/gitea/modules/git"
  21. api "code.gitea.io/gitea/modules/structs"
  22. "code.gitea.io/gitea/modules/test"
  23. "code.gitea.io/gitea/services/pull"
  24. "github.com/stretchr/testify/assert"
  25. "github.com/unknwon/i18n"
  26. )
  27. func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle models.MergeStyle) *httptest.ResponseRecorder {
  28. req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
  29. resp := session.MakeRequest(t, req, http.StatusOK)
  30. // Click the little green button to create a pull
  31. htmlDoc := NewHTMLParser(t, resp.Body)
  32. link, exists := htmlDoc.doc.Find(".ui.form." + string(mergeStyle) + "-fields > form").Attr("action")
  33. assert.True(t, exists, "The template has changed")
  34. req = NewRequestWithValues(t, "POST", link, map[string]string{
  35. "_csrf": htmlDoc.GetCSRF(),
  36. "do": string(mergeStyle),
  37. })
  38. resp = session.MakeRequest(t, req, http.StatusFound)
  39. return resp
  40. }
  41. func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder {
  42. req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
  43. resp := session.MakeRequest(t, req, http.StatusOK)
  44. // Click the little green button to create a pull
  45. htmlDoc := NewHTMLParser(t, resp.Body)
  46. link, exists := htmlDoc.doc.Find(".timeline-item .delete-button").Attr("data-url")
  47. assert.True(t, exists, "The template has changed")
  48. req = NewRequestWithValues(t, "POST", link, map[string]string{
  49. "_csrf": htmlDoc.GetCSRF(),
  50. })
  51. resp = session.MakeRequest(t, req, http.StatusOK)
  52. return resp
  53. }
  54. func TestPullMerge(t *testing.T) {
  55. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  56. hookTasks, err := webhook.HookTasks(1, 1) //Retrieve previous hook number
  57. assert.NoError(t, err)
  58. hookTasksLenBefore := len(hookTasks)
  59. session := loginUser(t, "user1")
  60. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  61. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  62. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  63. elem := strings.Split(test.RedirectURL(resp), "/")
  64. assert.EqualValues(t, "pulls", elem[3])
  65. testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleMerge)
  66. hookTasks, err = webhook.HookTasks(1, 1)
  67. assert.NoError(t, err)
  68. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  69. })
  70. }
  71. func TestPullRebase(t *testing.T) {
  72. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  73. hookTasks, err := webhook.HookTasks(1, 1) //Retrieve previous hook number
  74. assert.NoError(t, err)
  75. hookTasksLenBefore := len(hookTasks)
  76. session := loginUser(t, "user1")
  77. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  78. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  79. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  80. elem := strings.Split(test.RedirectURL(resp), "/")
  81. assert.EqualValues(t, "pulls", elem[3])
  82. testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleRebase)
  83. hookTasks, err = webhook.HookTasks(1, 1)
  84. assert.NoError(t, err)
  85. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  86. })
  87. }
  88. func TestPullRebaseMerge(t *testing.T) {
  89. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  90. hookTasks, err := webhook.HookTasks(1, 1) //Retrieve previous hook number
  91. assert.NoError(t, err)
  92. hookTasksLenBefore := len(hookTasks)
  93. session := loginUser(t, "user1")
  94. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  95. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  96. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  97. elem := strings.Split(test.RedirectURL(resp), "/")
  98. assert.EqualValues(t, "pulls", elem[3])
  99. testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleRebaseMerge)
  100. hookTasks, err = webhook.HookTasks(1, 1)
  101. assert.NoError(t, err)
  102. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  103. })
  104. }
  105. func TestPullSquash(t *testing.T) {
  106. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  107. hookTasks, err := webhook.HookTasks(1, 1) //Retrieve previous hook number
  108. assert.NoError(t, err)
  109. hookTasksLenBefore := len(hookTasks)
  110. session := loginUser(t, "user1")
  111. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  112. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  113. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
  114. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  115. elem := strings.Split(test.RedirectURL(resp), "/")
  116. assert.EqualValues(t, "pulls", elem[3])
  117. testPullMerge(t, session, elem[1], elem[2], elem[4], models.MergeStyleSquash)
  118. hookTasks, err = webhook.HookTasks(1, 1)
  119. assert.NoError(t, err)
  120. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  121. })
  122. }
  123. func TestPullCleanUpAfterMerge(t *testing.T) {
  124. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  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 - TestPullCleanUpAfterMerge)\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. session := loginUser(t, "user1")
  152. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  153. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  154. resp := testPullCreate(t, session, "user1", "repo1", "master", "[wip] This is a pull title")
  155. req := NewRequest(t, "GET", resp.Header().Get("Location"))
  156. resp = session.MakeRequest(t, req, http.StatusOK)
  157. htmlDoc := NewHTMLParser(t, resp.Body)
  158. text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text())
  159. assert.NotEmpty(t, text, "Can't find WIP text")
  160. assert.Contains(t, text, i18n.Tr("en", "repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
  161. assert.Contains(t, text, "[wip]", "Unable to find WIP text")
  162. })
  163. }
  164. func TestCantMergeConflict(t *testing.T) {
  165. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  166. session := loginUser(t, "user1")
  167. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  168. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
  169. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
  170. // Use API to create a conflicting pr
  171. token := getTokenForLoggedInUser(t, session)
  172. req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
  173. Head: "conflict",
  174. Base: "base",
  175. Title: "create a conflicting pr",
  176. })
  177. session.MakeRequest(t, req, 201)
  178. // Now this PR will be marked conflict - or at least a race will do - so drop down to pure code at this point...
  179. user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  180. Name: "user1",
  181. }).(*user_model.User)
  182. repo1 := unittest.AssertExistsAndLoadBean(t, &models.Repository{
  183. OwnerID: user1.ID,
  184. Name: "repo1",
  185. }).(*models.Repository)
  186. pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{
  187. HeadRepoID: repo1.ID,
  188. BaseRepoID: repo1.ID,
  189. HeadBranch: "conflict",
  190. BaseBranch: "base",
  191. }).(*models.PullRequest)
  192. gitRepo, err := git.OpenRepository(models.RepoPath(user1.Name, repo1.Name))
  193. assert.NoError(t, err)
  194. err = pull.Merge(pr, user1, gitRepo, models.MergeStyleMerge, "CONFLICT")
  195. assert.Error(t, err, "Merge should return an error due to conflict")
  196. assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error")
  197. err = pull.Merge(pr, user1, gitRepo, models.MergeStyleRebase, "CONFLICT")
  198. assert.Error(t, err, "Merge should return an error due to conflict")
  199. assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
  200. gitRepo.Close()
  201. })
  202. }
  203. func TestCantMergeUnrelated(t *testing.T) {
  204. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  205. session := loginUser(t, "user1")
  206. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  207. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
  208. // Now we want to create a commit on a branch that is totally unrelated to our current head
  209. // Drop down to pure code at this point
  210. user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  211. Name: "user1",
  212. }).(*user_model.User)
  213. repo1 := unittest.AssertExistsAndLoadBean(t, &models.Repository{
  214. OwnerID: user1.ID,
  215. Name: "repo1",
  216. }).(*models.Repository)
  217. path := models.RepoPath(user1.Name, repo1.Name)
  218. _, err := git.NewCommand("read-tree", "--empty").RunInDir(path)
  219. assert.NoError(t, err)
  220. stdin := bytes.NewBufferString("Unrelated File")
  221. var stdout strings.Builder
  222. err = git.NewCommand("hash-object", "-w", "--stdin").RunInDirFullPipeline(path, &stdout, nil, stdin)
  223. assert.NoError(t, err)
  224. sha := strings.TrimSpace(stdout.String())
  225. _, err = git.NewCommand("update-index", "--add", "--replace", "--cacheinfo", "100644", sha, "somewher-over-the-rainbow").RunInDir(path)
  226. assert.NoError(t, err)
  227. treeSha, err := git.NewCommand("write-tree").RunInDir(path)
  228. assert.NoError(t, err)
  229. treeSha = strings.TrimSpace(treeSha)
  230. commitTimeStr := time.Now().Format(time.RFC3339)
  231. doerSig := user1.NewGitSig()
  232. env := append(os.Environ(),
  233. "GIT_AUTHOR_NAME="+doerSig.Name,
  234. "GIT_AUTHOR_EMAIL="+doerSig.Email,
  235. "GIT_AUTHOR_DATE="+commitTimeStr,
  236. "GIT_COMMITTER_NAME="+doerSig.Name,
  237. "GIT_COMMITTER_EMAIL="+doerSig.Email,
  238. "GIT_COMMITTER_DATE="+commitTimeStr,
  239. )
  240. messageBytes := new(bytes.Buffer)
  241. _, _ = messageBytes.WriteString("Unrelated")
  242. _, _ = messageBytes.WriteString("\n")
  243. stdout.Reset()
  244. err = git.NewCommand("commit-tree", treeSha).RunInDirTimeoutEnvFullPipeline(env, -1, path, &stdout, nil, messageBytes)
  245. assert.NoError(t, err)
  246. commitSha := strings.TrimSpace(stdout.String())
  247. _, err = git.NewCommand("branch", "unrelated", commitSha).RunInDir(path)
  248. assert.NoError(t, err)
  249. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
  250. // Use API to create a conflicting pr
  251. token := getTokenForLoggedInUser(t, session)
  252. req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
  253. Head: "unrelated",
  254. Base: "base",
  255. Title: "create an unrelated pr",
  256. })
  257. session.MakeRequest(t, req, 201)
  258. // Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point...
  259. gitRepo, err := git.OpenRepository(path)
  260. assert.NoError(t, err)
  261. pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{
  262. HeadRepoID: repo1.ID,
  263. BaseRepoID: repo1.ID,
  264. HeadBranch: "unrelated",
  265. BaseBranch: "base",
  266. }).(*models.PullRequest)
  267. err = pull.Merge(pr, user1, gitRepo, models.MergeStyleMerge, "UNRELATED")
  268. assert.Error(t, err, "Merge should return an error due to unrelated")
  269. assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
  270. gitRepo.Close()
  271. })
  272. }