gitea源码

repo_merge_upstream_test.go 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. // Copyright 2025 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package integration
  4. import (
  5. "fmt"
  6. "net/http"
  7. "net/url"
  8. "strings"
  9. "testing"
  10. "time"
  11. auth_model "code.gitea.io/gitea/models/auth"
  12. "code.gitea.io/gitea/models/db"
  13. git_model "code.gitea.io/gitea/models/git"
  14. repo_model "code.gitea.io/gitea/models/repo"
  15. "code.gitea.io/gitea/models/unittest"
  16. user_model "code.gitea.io/gitea/models/user"
  17. api "code.gitea.io/gitea/modules/structs"
  18. "code.gitea.io/gitea/modules/util"
  19. "github.com/stretchr/testify/assert"
  20. "github.com/stretchr/testify/require"
  21. )
  22. func TestRepoMergeUpstream(t *testing.T) {
  23. onGiteaRun(t, func(*testing.T, *url.URL) {
  24. forkUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
  25. baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
  26. baseUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: baseRepo.OwnerID})
  27. checkFileContent := func(branch, exp string) {
  28. req := NewRequest(t, "GET", fmt.Sprintf("/%s/test-repo-fork/raw/branch/%s/new-file.txt", forkUser.Name, branch))
  29. resp := MakeRequest(t, req, http.StatusOK)
  30. require.Equal(t, exp, resp.Body.String())
  31. }
  32. session := loginUser(t, forkUser.Name)
  33. token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
  34. // create a fork
  35. req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseUser.Name, baseRepo.Name), &api.CreateForkOption{
  36. Name: util.ToPointer("test-repo-fork"),
  37. }).AddTokenAuth(token)
  38. MakeRequest(t, req, http.StatusAccepted)
  39. forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: forkUser.ID, Name: "test-repo-fork"})
  40. // create fork-branch
  41. req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/test-repo-fork/branches/_new/branch/master", forkUser.Name), map[string]string{
  42. "_csrf": GetUserCSRFToken(t, session),
  43. "new_branch_name": "fork-branch",
  44. })
  45. session.MakeRequest(t, req, http.StatusSeeOther)
  46. queryMergeUpstreamButtonLink := func(htmlDoc *HTMLDoc) string {
  47. return htmlDoc.Find(`button[data-url*="merge-upstream"]`).AttrOr("data-url", "")
  48. }
  49. t.Run("HeadBeforeBase", func(t *testing.T) {
  50. // add a file in base repo
  51. sessionBaseUser := loginUser(t, baseUser.Name)
  52. require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-1"))
  53. var mergeUpstreamLink string
  54. t.Run("DetectDefaultBranch", func(t *testing.T) {
  55. // the repo shows a prompt to "sync fork" (defaults to the default branch)
  56. require.Eventually(t, func() bool {
  57. resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
  58. htmlDoc := NewHTMLParser(t, resp.Body)
  59. mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
  60. if mergeUpstreamLink == "" {
  61. return false
  62. }
  63. respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
  64. return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/master">user2/repo1:master</a>`)
  65. }, 5*time.Second, 100*time.Millisecond)
  66. })
  67. t.Run("DetectSameBranch", func(t *testing.T) {
  68. // if the fork-branch name also exists in the base repo, then use that branch instead
  69. req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/master", map[string]string{
  70. "_csrf": GetUserCSRFToken(t, sessionBaseUser),
  71. "new_branch_name": "fork-branch",
  72. })
  73. sessionBaseUser.MakeRequest(t, req, http.StatusSeeOther)
  74. require.Eventually(t, func() bool {
  75. resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
  76. htmlDoc := NewHTMLParser(t, resp.Body)
  77. mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
  78. if mergeUpstreamLink == "" {
  79. return false
  80. }
  81. respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
  82. return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/fork-branch">user2/repo1:fork-branch</a>`)
  83. }, 5*time.Second, 100*time.Millisecond)
  84. })
  85. // click the "sync fork" button
  86. req = NewRequestWithValues(t, "POST", mergeUpstreamLink, map[string]string{"_csrf": GetUserCSRFToken(t, session)})
  87. session.MakeRequest(t, req, http.StatusOK)
  88. checkFileContent("fork-branch", "test-content-1")
  89. // delete the "fork-branch" from the base repo
  90. req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/delete?name=fork-branch", map[string]string{
  91. "_csrf": GetUserCSRFToken(t, sessionBaseUser),
  92. })
  93. sessionBaseUser.MakeRequest(t, req, http.StatusOK)
  94. })
  95. t.Run("BaseChangeAfterHeadChange", func(t *testing.T) {
  96. // update the files: base first, head later, and check the prompt
  97. require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-2"))
  98. require.NoError(t, createOrReplaceFileInBranch(forkUser, forkRepo, "new-file-other.txt", "fork-branch", "test-content-other"))
  99. // make sure the base branch's update time is before the fork, to make it test the complete logic
  100. baseBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: baseRepo.ID, Name: "master"})
  101. forkBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: forkRepo.ID, Name: "fork-branch"})
  102. _, err := db.GetEngine(t.Context()).ID(forkBranch.ID).Update(&git_model.Branch{UpdatedUnix: baseBranch.UpdatedUnix + 1})
  103. require.NoError(t, err)
  104. // the repo shows a prompt to "sync fork"
  105. require.Eventually(t, func() bool {
  106. resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
  107. htmlDoc := NewHTMLParser(t, resp.Body)
  108. respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
  109. return strings.Contains(respMsg, `The base branch <a href="/user2/repo1/src/branch/master">user2/repo1:master</a> has new changes`)
  110. }, 5*time.Second, 100*time.Millisecond)
  111. // and do the merge-upstream by API
  112. req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
  113. Branch: "fork-branch",
  114. }).AddTokenAuth(token)
  115. resp := MakeRequest(t, req, http.StatusOK)
  116. checkFileContent("fork-branch", "test-content-2")
  117. var mergeResp api.MergeUpstreamResponse
  118. DecodeJSON(t, resp, &mergeResp)
  119. assert.Equal(t, "merge", mergeResp.MergeStyle)
  120. // after merge, there should be no "sync fork" button anymore
  121. require.Eventually(t, func() bool {
  122. resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
  123. htmlDoc := NewHTMLParser(t, resp.Body)
  124. return queryMergeUpstreamButtonLink(htmlDoc) == ""
  125. }, 5*time.Second, 100*time.Millisecond)
  126. })
  127. t.Run("FastForwardOnly", func(t *testing.T) {
  128. // Create a clean branch for fast-forward testing
  129. req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/test-repo-fork/branches/_new/branch/master", forkUser.Name), map[string]string{
  130. "_csrf": GetUserCSRFToken(t, session),
  131. "new_branch_name": "ff-test-branch",
  132. })
  133. session.MakeRequest(t, req, http.StatusSeeOther)
  134. // Add content to base repository that can be fast-forwarded
  135. require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "ff-test.txt", "master", "ff-content-1"))
  136. // ff_only=true with fast-forward possible (should succeed)
  137. req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
  138. Branch: "ff-test-branch",
  139. FfOnly: true,
  140. }).AddTokenAuth(token)
  141. resp := MakeRequest(t, req, http.StatusOK)
  142. var mergeResp api.MergeUpstreamResponse
  143. DecodeJSON(t, resp, &mergeResp)
  144. assert.Equal(t, "fast-forward", mergeResp.MergeStyle)
  145. // ff_only=true when fast-forward is not possible (should fail)
  146. require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "another-file.txt", "master", "more-content"))
  147. req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
  148. Branch: "fork-branch",
  149. FfOnly: true,
  150. }).AddTokenAuth(token)
  151. MakeRequest(t, req, http.StatusBadRequest)
  152. })
  153. })
  154. }