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

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