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

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