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

Only check for conflicts/merging if the PR has not been merged in the interim (#10132) * Only check for merging if the PR has not been merged in the interim * fixup! Only check for merging if the PR has not been merged in the interim * Try to fix test failure * Use PR2 not PR1 in tests as PR1 merges automatically * return already merged error * enforce locking * enforce locking - fix-test * enforce locking - fix-testx2 * enforce locking - fix-testx3 * move pullrequest checking to after merge This might improve the chance that the race does not affect us but does not prevent it. * Remove minor race with getting merge commit id * fixup * move check pr after merge * Remove unnecessary prepareTestEnv - onGiteaRun does this for us * Add information about when merging occuring * fix fmt * More logging * Attempt to fix mysql * Try MySQL fix again * try again * Try again?! * Try again?! * Sigh * remove the count - perhaps that will help * next remove the update id * next remove the update id - make it updated_unix instead * On failure to merge ensure that the pr is rechecked for conflict errors * On failure to merge ensure that the pr is rechecked for conflict errors * Update models/pull.go * Update models/pull.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Apply suggestions from code review Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
4 years ago
Redesign Scoped Access Tokens (#24767) ## Changes - Adds the following high level access scopes, each with `read` and `write` levels: - `activitypub` - `admin` (hidden if user is not a site admin) - `misc` - `notification` - `organization` - `package` - `issue` - `repository` - `user` - Adds new middleware function `tokenRequiresScopes()` in addition to `reqToken()` - `tokenRequiresScopes()` is used for each high-level api section - _if_ a scoped token is present, checks that the required scope is included based on the section and HTTP method - `reqToken()` is used for individual routes - checks that required authentication is present (but does not check scope levels as this will already have been handled by `tokenRequiresScopes()` - Adds migration to convert old scoped access tokens to the new set of scopes - Updates the user interface for scope selection ### User interface example <img width="903" alt="Screen Shot 2023-05-31 at 1 56 55 PM" src="https://github.com/go-gitea/gitea/assets/23248839/654766ec-2143-4f59-9037-3b51600e32f3"> <img width="917" alt="Screen Shot 2023-05-31 at 1 56 43 PM" src="https://github.com/go-gitea/gitea/assets/23248839/1ad64081-012c-4a73-b393-66b30352654c"> ## tokenRequiresScopes Design Decision - `tokenRequiresScopes()` was added to more reliably cover api routes. For an incoming request, this function uses the given scope category (say `AccessTokenScopeCategoryOrganization`) and the HTTP method (say `DELETE`) and verifies that any scoped tokens in use include `delete:organization`. - `reqToken()` is used to enforce auth for individual routes that require it. If a scoped token is not present for a request, `tokenRequiresScopes()` will not return an error ## TODO - [x] Alphabetize scope categories - [x] Change 'public repos only' to a radio button (private vs public). Also expand this to organizations - [X] Disable token creation if no scopes selected. Alternatively, show warning - [x] `reqToken()` is missing from many `POST/DELETE` routes in the api. `tokenRequiresScopes()` only checks that a given token has the correct scope, `reqToken()` must be used to check that a token (or some other auth) is present. - _This should be addressed in this PR_ - [x] The migration should be reviewed very carefully in order to minimize access changes to existing user tokens. - _This should be addressed in this PR_ - [x] Link to api to swagger documentation, clarify what read/write/delete levels correspond to - [x] Review cases where more than one scope is needed as this directly deviates from the api definition. - _This should be addressed in this PR_ - For example: ```go m.Group("/users/{username}/orgs", func() { m.Get("", reqToken(), org.ListUserOrgs) m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI()) ``` ## Future improvements - [ ] Add required scopes to swagger documentation - [ ] Redesign `reqToken()` to be opt-out rather than opt-in - [ ] Subdivide scopes like `repository` - [ ] Once a token is created, if it has no scopes, we should display text instead of an empty bullet point - [ ] If the 'public repos only' option is selected, should read categories be selected by default Closes #24501 Closes #24799 Co-authored-by: Jonathan Tran <jon@allspice.io> Co-authored-by: Kyle D <kdumontnu@gmail.com> Co-authored-by: silverwind <me@silverwind.io>
1 year ago
Refactor git command package to improve security and maintainability (#22678) This PR follows #21535 (and replace #22592) ## Review without space diff https://github.com/go-gitea/gitea/pull/22678/files?diff=split&w=1 ## Purpose of this PR 1. Make git module command completely safe (risky user inputs won't be passed as argument option anymore) 2. Avoid low-level mistakes like https://github.com/go-gitea/gitea/pull/22098#discussion_r1045234918 3. Remove deprecated and dirty `CmdArgCheck` function, hide the `CmdArg` type 4. Simplify code when using git command ## The main idea of this PR * Move the `git.CmdArg` to the `internal` package, then no other package except `git` could use it. Then developers could never do `AddArguments(git.CmdArg(userInput))` any more. * Introduce `git.ToTrustedCmdArgs`, it's for user-provided and already trusted arguments. It's only used in a few cases, for example: use git arguments from config file, help unit test with some arguments. * Introduce `AddOptionValues` and `AddOptionFormat`, they make code more clear and simple: * Before: `AddArguments("-m").AddDynamicArguments(message)` * After: `AddOptionValues("-m", message)` * - * Before: `AddArguments(git.CmdArg(fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email)))` * After: `AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)` ## FAQ ### Why these changes were not done in #21535 ? #21535 is mainly a search&replace, it did its best to not change too much logic. Making the framework better needs a lot of changes, so this separate PR is needed as the second step. ### The naming of `AddOptionXxx` According to git's manual, the `--xxx` part is called `option`. ### How can it guarantee that `internal.CmdArg` won't be not misused? Go's specification guarantees that. Trying to access other package's internal package causes compilation error. And, `golangci-lint` also denies the git/internal package. Only the `git/command.go` can use it carefully. ### There is still a `ToTrustedCmdArgs`, will it still allow developers to make mistakes and pass untrusted arguments? Generally speaking, no. Because when using `ToTrustedCmdArgs`, the code will be very complex (see the changes for examples). Then developers and reviewers can know that something might be unreasonable. ### Why there was a `CmdArgCheck` and why it's removed? At the moment of #21535, to reduce unnecessary changes, `CmdArgCheck` was introduced as a hacky patch. Now, almost all code could be written as `cmd := NewCommand(); cmd.AddXxx(...)`, then there is no need for `CmdArgCheck` anymore. ### Why many codes for `signArg == ""` is deleted? Because in the old code, `signArg` could never be empty string, it's either `-S[key-id]` or `--no-gpg-sign`. So the `signArg == ""` is just dead code. --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
1 year ago
Redesign Scoped Access Tokens (#24767) ## Changes - Adds the following high level access scopes, each with `read` and `write` levels: - `activitypub` - `admin` (hidden if user is not a site admin) - `misc` - `notification` - `organization` - `package` - `issue` - `repository` - `user` - Adds new middleware function `tokenRequiresScopes()` in addition to `reqToken()` - `tokenRequiresScopes()` is used for each high-level api section - _if_ a scoped token is present, checks that the required scope is included based on the section and HTTP method - `reqToken()` is used for individual routes - checks that required authentication is present (but does not check scope levels as this will already have been handled by `tokenRequiresScopes()` - Adds migration to convert old scoped access tokens to the new set of scopes - Updates the user interface for scope selection ### User interface example <img width="903" alt="Screen Shot 2023-05-31 at 1 56 55 PM" src="https://github.com/go-gitea/gitea/assets/23248839/654766ec-2143-4f59-9037-3b51600e32f3"> <img width="917" alt="Screen Shot 2023-05-31 at 1 56 43 PM" src="https://github.com/go-gitea/gitea/assets/23248839/1ad64081-012c-4a73-b393-66b30352654c"> ## tokenRequiresScopes Design Decision - `tokenRequiresScopes()` was added to more reliably cover api routes. For an incoming request, this function uses the given scope category (say `AccessTokenScopeCategoryOrganization`) and the HTTP method (say `DELETE`) and verifies that any scoped tokens in use include `delete:organization`. - `reqToken()` is used to enforce auth for individual routes that require it. If a scoped token is not present for a request, `tokenRequiresScopes()` will not return an error ## TODO - [x] Alphabetize scope categories - [x] Change 'public repos only' to a radio button (private vs public). Also expand this to organizations - [X] Disable token creation if no scopes selected. Alternatively, show warning - [x] `reqToken()` is missing from many `POST/DELETE` routes in the api. `tokenRequiresScopes()` only checks that a given token has the correct scope, `reqToken()` must be used to check that a token (or some other auth) is present. - _This should be addressed in this PR_ - [x] The migration should be reviewed very carefully in order to minimize access changes to existing user tokens. - _This should be addressed in this PR_ - [x] Link to api to swagger documentation, clarify what read/write/delete levels correspond to - [x] Review cases where more than one scope is needed as this directly deviates from the api definition. - _This should be addressed in this PR_ - For example: ```go m.Group("/users/{username}/orgs", func() { m.Get("", reqToken(), org.ListUserOrgs) m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI()) ``` ## Future improvements - [ ] Add required scopes to swagger documentation - [ ] Redesign `reqToken()` to be opt-out rather than opt-in - [ ] Subdivide scopes like `repository` - [ ] Once a token is created, if it has no scopes, we should display text instead of an empty bullet point - [ ] If the 'public repos only' option is selected, should read categories be selected by default Closes #24501 Closes #24799 Co-authored-by: Jonathan Tran <jon@allspice.io> Co-authored-by: Kyle D <kdumontnu@gmail.com> Co-authored-by: silverwind <me@silverwind.io>
1 year ago
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. }