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 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  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. "context"
  8. "fmt"
  9. "net/http"
  10. "net/http/httptest"
  11. "net/url"
  12. "os"
  13. "path"
  14. "strings"
  15. "testing"
  16. "time"
  17. "code.gitea.io/gitea/models"
  18. "code.gitea.io/gitea/models/db"
  19. repo_model "code.gitea.io/gitea/models/repo"
  20. "code.gitea.io/gitea/models/unittest"
  21. user_model "code.gitea.io/gitea/models/user"
  22. "code.gitea.io/gitea/models/webhook"
  23. "code.gitea.io/gitea/modules/git"
  24. api "code.gitea.io/gitea/modules/structs"
  25. "code.gitea.io/gitea/modules/test"
  26. "code.gitea.io/gitea/modules/translation/i18n"
  27. "code.gitea.io/gitea/services/pull"
  28. repo_service "code.gitea.io/gitea/services/repository"
  29. files_service "code.gitea.io/gitea/services/repository/files"
  30. "github.com/stretchr/testify/assert"
  31. )
  32. func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle) *httptest.ResponseRecorder {
  33. req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
  34. resp := session.MakeRequest(t, req, http.StatusOK)
  35. htmlDoc := NewHTMLParser(t, resp.Body)
  36. link := path.Join(user, repo, "pulls", pullnum, "merge")
  37. req = NewRequestWithValues(t, "POST", link, map[string]string{
  38. "_csrf": htmlDoc.GetCSRF(),
  39. "do": string(mergeStyle),
  40. })
  41. resp = session.MakeRequest(t, req, http.StatusSeeOther)
  42. return resp
  43. }
  44. func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder {
  45. req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
  46. resp := session.MakeRequest(t, req, http.StatusOK)
  47. // Click the little green button to create a pull
  48. htmlDoc := NewHTMLParser(t, resp.Body)
  49. link, exists := htmlDoc.doc.Find(".timeline-item .delete-button").Attr("data-url")
  50. assert.True(t, exists, "The template has changed, can not find delete button url")
  51. req = NewRequestWithValues(t, "POST", link, map[string]string{
  52. "_csrf": htmlDoc.GetCSRF(),
  53. })
  54. resp = session.MakeRequest(t, req, http.StatusOK)
  55. return resp
  56. }
  57. func TestPullMerge(t *testing.T) {
  58. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  59. hookTasks, err := webhook.HookTasks(1, 1) // Retrieve previous hook number
  60. assert.NoError(t, err)
  61. hookTasksLenBefore := len(hookTasks)
  62. session := loginUser(t, "user1")
  63. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  64. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  65. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  66. elem := strings.Split(test.RedirectURL(resp), "/")
  67. assert.EqualValues(t, "pulls", elem[3])
  68. testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
  69. hookTasks, err = webhook.HookTasks(1, 1)
  70. assert.NoError(t, err)
  71. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  72. })
  73. }
  74. func TestPullRebase(t *testing.T) {
  75. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  76. hookTasks, err := webhook.HookTasks(1, 1) // Retrieve previous hook number
  77. assert.NoError(t, err)
  78. hookTasksLenBefore := len(hookTasks)
  79. session := loginUser(t, "user1")
  80. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  81. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  82. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  83. elem := strings.Split(test.RedirectURL(resp), "/")
  84. assert.EqualValues(t, "pulls", elem[3])
  85. testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebase)
  86. hookTasks, err = webhook.HookTasks(1, 1)
  87. assert.NoError(t, err)
  88. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  89. })
  90. }
  91. func TestPullRebaseMerge(t *testing.T) {
  92. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  93. hookTasks, err := webhook.HookTasks(1, 1) // Retrieve previous hook number
  94. assert.NoError(t, err)
  95. hookTasksLenBefore := len(hookTasks)
  96. session := loginUser(t, "user1")
  97. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  98. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  99. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  100. elem := strings.Split(test.RedirectURL(resp), "/")
  101. assert.EqualValues(t, "pulls", elem[3])
  102. testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebaseMerge)
  103. hookTasks, err = webhook.HookTasks(1, 1)
  104. assert.NoError(t, err)
  105. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  106. })
  107. }
  108. func TestPullSquash(t *testing.T) {
  109. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  110. hookTasks, err := webhook.HookTasks(1, 1) // Retrieve previous hook number
  111. assert.NoError(t, err)
  112. hookTasksLenBefore := len(hookTasks)
  113. session := loginUser(t, "user1")
  114. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  115. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  116. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
  117. resp := testPullCreate(t, session, "user1", "repo1", "master", "This is a pull title")
  118. elem := strings.Split(test.RedirectURL(resp), "/")
  119. assert.EqualValues(t, "pulls", elem[3])
  120. testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleSquash)
  121. hookTasks, err = webhook.HookTasks(1, 1)
  122. assert.NoError(t, err)
  123. assert.Len(t, hookTasks, hookTasksLenBefore+1)
  124. })
  125. }
  126. func TestPullCleanUpAfterMerge(t *testing.T) {
  127. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  128. session := loginUser(t, "user1")
  129. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  130. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n")
  131. resp := testPullCreate(t, session, "user1", "repo1", "feature/test", "This is a pull title")
  132. elem := strings.Split(test.RedirectURL(resp), "/")
  133. assert.EqualValues(t, "pulls", elem[3])
  134. testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge)
  135. // Check PR branch deletion
  136. resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4])
  137. respJSON := struct {
  138. Redirect string
  139. }{}
  140. DecodeJSON(t, resp, &respJSON)
  141. assert.NotEmpty(t, respJSON.Redirect, "Redirected URL is not found")
  142. elem = strings.Split(respJSON.Redirect, "/")
  143. assert.EqualValues(t, "pulls", elem[3])
  144. // Check branch deletion result
  145. req := NewRequest(t, "GET", respJSON.Redirect)
  146. resp = session.MakeRequest(t, req, http.StatusOK)
  147. htmlDoc := NewHTMLParser(t, resp.Body)
  148. resultMsg := htmlDoc.doc.Find(".ui.message>p").Text()
  149. assert.EqualValues(t, "Branch 'user1/feature/test' has been deleted.", resultMsg)
  150. })
  151. }
  152. func TestCantMergeWorkInProgress(t *testing.T) {
  153. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  154. session := loginUser(t, "user1")
  155. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  156. testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
  157. resp := testPullCreate(t, session, "user1", "repo1", "master", "[wip] This is a pull title")
  158. req := NewRequest(t, "GET", resp.Header().Get("Location"))
  159. resp = session.MakeRequest(t, req, http.StatusOK)
  160. htmlDoc := NewHTMLParser(t, resp.Body)
  161. text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text())
  162. assert.NotEmpty(t, text, "Can't find WIP text")
  163. assert.Contains(t, text, i18n.Tr("en", "repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
  164. assert.Contains(t, text, "[wip]", "Unable to find WIP text")
  165. })
  166. }
  167. func TestCantMergeConflict(t *testing.T) {
  168. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  169. session := loginUser(t, "user1")
  170. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  171. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
  172. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
  173. // Use API to create a conflicting pr
  174. token := getTokenForLoggedInUser(t, session)
  175. req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
  176. Head: "conflict",
  177. Base: "base",
  178. Title: "create a conflicting pr",
  179. })
  180. session.MakeRequest(t, req, http.StatusCreated)
  181. // Now this PR will be marked conflict - or at least a race will do - so drop down to pure code at this point...
  182. user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  183. Name: "user1",
  184. }).(*user_model.User)
  185. repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
  186. OwnerID: user1.ID,
  187. Name: "repo1",
  188. }).(*repo_model.Repository)
  189. pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{
  190. HeadRepoID: repo1.ID,
  191. BaseRepoID: repo1.ID,
  192. HeadBranch: "conflict",
  193. BaseBranch: "base",
  194. }).(*models.PullRequest)
  195. gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
  196. assert.NoError(t, err)
  197. err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "CONFLICT")
  198. assert.Error(t, err, "Merge should return an error due to conflict")
  199. assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error")
  200. err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleRebase, "", "CONFLICT")
  201. assert.Error(t, err, "Merge should return an error due to conflict")
  202. assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
  203. gitRepo.Close()
  204. })
  205. }
  206. func TestCantMergeUnrelated(t *testing.T) {
  207. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  208. session := loginUser(t, "user1")
  209. testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
  210. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
  211. // Now we want to create a commit on a branch that is totally unrelated to our current head
  212. // Drop down to pure code at this point
  213. user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
  214. Name: "user1",
  215. }).(*user_model.User)
  216. repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
  217. OwnerID: user1.ID,
  218. Name: "repo1",
  219. }).(*repo_model.Repository)
  220. path := repo_model.RepoPath(user1.Name, repo1.Name)
  221. err := git.NewCommand(git.DefaultContext, "read-tree", "--empty").Run(&git.RunOpts{Dir: path})
  222. assert.NoError(t, err)
  223. stdin := bytes.NewBufferString("Unrelated File")
  224. var stdout strings.Builder
  225. err = git.NewCommand(git.DefaultContext, "hash-object", "-w", "--stdin").Run(&git.RunOpts{
  226. Dir: path,
  227. Stdin: stdin,
  228. Stdout: &stdout,
  229. })
  230. assert.NoError(t, err)
  231. sha := strings.TrimSpace(stdout.String())
  232. _, _, err = git.NewCommand(git.DefaultContext, "update-index", "--add", "--replace", "--cacheinfo", "100644", sha, "somewher-over-the-rainbow").RunStdString(&git.RunOpts{Dir: path})
  233. assert.NoError(t, err)
  234. treeSha, _, err := git.NewCommand(git.DefaultContext, "write-tree").RunStdString(&git.RunOpts{Dir: path})
  235. assert.NoError(t, err)
  236. treeSha = strings.TrimSpace(treeSha)
  237. commitTimeStr := time.Now().Format(time.RFC3339)
  238. doerSig := user1.NewGitSig()
  239. env := append(os.Environ(),
  240. "GIT_AUTHOR_NAME="+doerSig.Name,
  241. "GIT_AUTHOR_EMAIL="+doerSig.Email,
  242. "GIT_AUTHOR_DATE="+commitTimeStr,
  243. "GIT_COMMITTER_NAME="+doerSig.Name,
  244. "GIT_COMMITTER_EMAIL="+doerSig.Email,
  245. "GIT_COMMITTER_DATE="+commitTimeStr,
  246. )
  247. messageBytes := new(bytes.Buffer)
  248. _, _ = messageBytes.WriteString("Unrelated")
  249. _, _ = messageBytes.WriteString("\n")
  250. stdout.Reset()
  251. err = git.NewCommand(git.DefaultContext, "commit-tree", treeSha).
  252. Run(&git.RunOpts{
  253. Env: env,
  254. Dir: path,
  255. Stdin: messageBytes,
  256. Stdout: &stdout,
  257. })
  258. assert.NoError(t, err)
  259. commitSha := strings.TrimSpace(stdout.String())
  260. _, _, err = git.NewCommand(git.DefaultContext, "branch", "unrelated", commitSha).RunStdString(&git.RunOpts{Dir: path})
  261. assert.NoError(t, err)
  262. testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
  263. // Use API to create a conflicting pr
  264. token := getTokenForLoggedInUser(t, session)
  265. req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
  266. Head: "unrelated",
  267. Base: "base",
  268. Title: "create an unrelated pr",
  269. })
  270. session.MakeRequest(t, req, http.StatusCreated)
  271. // Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point...
  272. gitRepo, err := git.OpenRepository(git.DefaultContext, path)
  273. assert.NoError(t, err)
  274. pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{
  275. HeadRepoID: repo1.ID,
  276. BaseRepoID: repo1.ID,
  277. HeadBranch: "unrelated",
  278. BaseBranch: "base",
  279. }).(*models.PullRequest)
  280. err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED")
  281. assert.Error(t, err, "Merge should return an error due to unrelated")
  282. assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
  283. gitRepo.Close()
  284. })
  285. }
  286. func TestConflictChecking(t *testing.T) {
  287. onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
  288. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
  289. // Create new clean repo to test conflict checking.
  290. baseRepo, err := repo_service.CreateRepository(user, user, models.CreateRepoOptions{
  291. Name: "conflict-checking",
  292. Description: "Tempo repo",
  293. AutoInit: true,
  294. Readme: "Default",
  295. DefaultBranch: "main",
  296. })
  297. assert.NoError(t, err)
  298. assert.NotEmpty(t, baseRepo)
  299. // create a commit on new branch.
  300. _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{
  301. TreePath: "important_file",
  302. Message: "Add a important file",
  303. Content: "Just a non-important file",
  304. IsNewFile: true,
  305. OldBranch: "main",
  306. NewBranch: "important-secrets",
  307. })
  308. assert.NoError(t, err)
  309. // create a commit on main branch.
  310. _, err = files_service.CreateOrUpdateRepoFile(git.DefaultContext, baseRepo, user, &files_service.UpdateRepoFileOptions{
  311. TreePath: "important_file",
  312. Message: "Add a important file",
  313. Content: "Not the same content :P",
  314. IsNewFile: true,
  315. OldBranch: "main",
  316. NewBranch: "main",
  317. })
  318. assert.NoError(t, err)
  319. // create Pull to merge the important-secrets branch into main branch.
  320. pullIssue := &models.Issue{
  321. RepoID: baseRepo.ID,
  322. Title: "PR with conflict!",
  323. PosterID: user.ID,
  324. Poster: user,
  325. IsPull: true,
  326. }
  327. pullRequest := &models.PullRequest{
  328. HeadRepoID: baseRepo.ID,
  329. BaseRepoID: baseRepo.ID,
  330. HeadBranch: "important-secrets",
  331. BaseBranch: "main",
  332. HeadRepo: baseRepo,
  333. BaseRepo: baseRepo,
  334. Type: models.PullRequestGitea,
  335. }
  336. err = pull.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
  337. assert.NoError(t, err)
  338. issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{Title: "PR with conflict!"}).(*models.Issue)
  339. conflictingPR, err := models.GetPullRequestByIssueID(db.DefaultContext, issue.ID)
  340. assert.NoError(t, err)
  341. // Ensure conflictedFiles is populated.
  342. assert.Equal(t, 1, len(conflictingPR.ConflictedFiles))
  343. // Check if status is correct.
  344. assert.Equal(t, models.PullRequestStatusConflict, conflictingPR.Status)
  345. // Ensure that mergeable returns false
  346. assert.False(t, conflictingPR.Mergeable())
  347. })
  348. }