Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

pull_merge_test.go 16KB

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