2017-06-15 13:20:39 +02:00
|
|
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
2022-11-27 19:20:29 +01:00
|
|
|
// SPDX-License-Identifier: MIT
|
2017-06-15 13:20:39 +02:00
|
|
|
|
2022-09-02 21:18:23 +02:00
|
|
|
package integration
|
2017-06-15 13:20:39 +02:00
|
|
|
|
|
|
|
import (
|
2019-11-10 09:42:51 +01:00
|
|
|
"bytes"
|
2022-05-08 14:32:45 +02:00
|
|
|
"context"
|
2019-11-10 09:42:51 +01:00
|
|
|
"fmt"
|
2017-06-15 13:20:39 +02:00
|
|
|
"net/http"
|
2017-12-03 23:46:01 +01:00
|
|
|
"net/http/httptest"
|
2019-05-11 17:29:17 +02:00
|
|
|
"net/url"
|
2019-11-10 09:42:51 +01:00
|
|
|
"os"
|
2017-06-15 13:20:39 +02:00
|
|
|
"path"
|
2024-05-21 17:23:22 +02:00
|
|
|
"path/filepath"
|
|
|
|
"strconv"
|
2017-06-15 13:20:39 +02:00
|
|
|
"strings"
|
|
|
|
"testing"
|
2019-11-10 09:42:51 +01:00
|
|
|
"time"
|
2017-06-15 13:20:39 +02:00
|
|
|
|
2018-01-05 19:56:50 +01:00
|
|
|
"code.gitea.io/gitea/models"
|
2023-01-17 22:46:03 +01:00
|
|
|
auth_model "code.gitea.io/gitea/models/auth"
|
2022-05-20 16:08:52 +02:00
|
|
|
"code.gitea.io/gitea/models/db"
|
2024-05-21 17:23:22 +02:00
|
|
|
git_model "code.gitea.io/gitea/models/git"
|
2022-06-13 11:37:59 +02:00
|
|
|
issues_model "code.gitea.io/gitea/models/issues"
|
2024-05-21 17:23:22 +02:00
|
|
|
pull_model "code.gitea.io/gitea/models/pull"
|
2021-12-10 02:27:50 +01:00
|
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
2021-11-16 09:53:21 +01:00
|
|
|
"code.gitea.io/gitea/models/unittest"
|
2021-11-24 10:49:20 +01:00
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
2021-11-10 06:13:16 +01:00
|
|
|
"code.gitea.io/gitea/models/webhook"
|
2019-11-10 09:42:51 +01:00
|
|
|
"code.gitea.io/gitea/modules/git"
|
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-27 21:09:51 +01:00
|
|
|
"code.gitea.io/gitea/modules/gitrepo"
|
2024-03-31 10:34:37 +02:00
|
|
|
"code.gitea.io/gitea/modules/hostmatcher"
|
2024-05-08 16:45:15 +02:00
|
|
|
"code.gitea.io/gitea/modules/queue"
|
2024-03-31 10:34:37 +02:00
|
|
|
"code.gitea.io/gitea/modules/setting"
|
2019-11-10 09:42:51 +01:00
|
|
|
api "code.gitea.io/gitea/modules/structs"
|
2017-12-15 22:11:02 +01:00
|
|
|
"code.gitea.io/gitea/modules/test"
|
2022-06-26 16:19:22 +02:00
|
|
|
"code.gitea.io/gitea/modules/translation"
|
2024-05-21 17:23:22 +02:00
|
|
|
"code.gitea.io/gitea/services/automerge"
|
2024-06-01 10:58:46 +02:00
|
|
|
"code.gitea.io/gitea/services/forms"
|
2019-11-10 09:42:51 +01:00
|
|
|
"code.gitea.io/gitea/services/pull"
|
2024-05-21 17:23:22 +02:00
|
|
|
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
|
2022-04-21 23:55:45 +02:00
|
|
|
files_service "code.gitea.io/gitea/services/repository/files"
|
2024-03-31 10:34:37 +02:00
|
|
|
webhook_service "code.gitea.io/gitea/services/webhook"
|
2017-12-15 22:11:02 +01:00
|
|
|
|
2017-06-15 13:20:39 +02:00
|
|
|
"github.com/stretchr/testify/assert"
|
2024-07-30 21:41:10 +02:00
|
|
|
"github.com/stretchr/testify/require"
|
2017-06-15 13:20:39 +02:00
|
|
|
)
|
|
|
|
|
2024-06-01 22:18:17 +02:00
|
|
|
type optionsPullMerge map[string]string
|
|
|
|
|
2024-01-17 01:44:56 +01:00
|
|
|
func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle, deleteBranch bool) *httptest.ResponseRecorder {
|
2024-06-01 22:18:17 +02:00
|
|
|
options := optionsPullMerge{
|
|
|
|
"do": string(mergeStyle),
|
|
|
|
}
|
|
|
|
if deleteBranch {
|
|
|
|
options["delete_branch_after_merge"] = "on"
|
|
|
|
}
|
|
|
|
|
|
|
|
return testPullMergeForm(t, session, http.StatusOK, user, repo, pullnum, options)
|
|
|
|
}
|
|
|
|
|
|
|
|
func testPullMergeForm(t *testing.T, session *TestSession, expectedCode int, user, repo, pullnum string, addOptions optionsPullMerge) *httptest.ResponseRecorder {
|
2017-06-15 13:20:39 +02:00
|
|
|
req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
|
2017-07-07 21:36:47 +02:00
|
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
2017-06-15 13:20:39 +02:00
|
|
|
|
2017-06-17 18:29:59 +02:00
|
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
2022-05-12 15:39:02 +02:00
|
|
|
link := path.Join(user, repo, "pulls", pullnum, "merge")
|
2024-01-17 01:44:56 +01:00
|
|
|
|
|
|
|
options := map[string]string{
|
2017-06-17 06:49:45 +02:00
|
|
|
"_csrf": htmlDoc.GetCSRF(),
|
2024-01-17 01:44:56 +01:00
|
|
|
}
|
2024-06-01 22:18:17 +02:00
|
|
|
for k, v := range addOptions {
|
|
|
|
options[k] = v
|
2024-01-17 01:44:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
req = NewRequestWithValues(t, "POST", link, options)
|
2024-06-01 22:18:17 +02:00
|
|
|
resp = session.MakeRequest(t, req, expectedCode)
|
2024-01-14 23:00:47 +01:00
|
|
|
|
2024-06-01 22:18:17 +02:00
|
|
|
if expectedCode == http.StatusOK {
|
|
|
|
respJSON := struct {
|
|
|
|
Redirect string
|
|
|
|
}{}
|
|
|
|
DecodeJSON(t, resp, &respJSON)
|
2024-01-14 23:00:47 +01:00
|
|
|
|
2024-06-01 22:18:17 +02:00
|
|
|
assert.EqualValues(t, fmt.Sprintf("/%s/%s/pulls/%s", user, repo, pullnum), respJSON.Redirect)
|
|
|
|
}
|
2017-06-15 13:20:39 +02:00
|
|
|
|
|
|
|
return resp
|
|
|
|
}
|
|
|
|
|
2017-12-03 23:46:01 +01:00
|
|
|
func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder {
|
2017-06-21 03:00:03 +02:00
|
|
|
req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
|
2017-07-07 21:36:47 +02:00
|
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
2017-06-21 03:00:03 +02:00
|
|
|
|
2023-09-19 00:05:31 +02:00
|
|
|
// Click the little button to create a pull
|
2017-06-21 03:00:03 +02:00
|
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
2020-04-11 00:01:41 +02:00
|
|
|
link, exists := htmlDoc.doc.Find(".timeline-item .delete-button").Attr("data-url")
|
2022-05-12 15:39:02 +02:00
|
|
|
assert.True(t, exists, "The template has changed, can not find delete button url")
|
2017-06-21 03:00:03 +02:00
|
|
|
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
|
|
|
"_csrf": htmlDoc.GetCSRF(),
|
|
|
|
})
|
2017-07-07 21:36:47 +02:00
|
|
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
2017-06-21 03:00:03 +02:00
|
|
|
|
|
|
|
return resp
|
|
|
|
}
|
|
|
|
|
2024-04-05 11:24:32 +02:00
|
|
|
// returns the hook tasks, order by ID desc.
|
2024-03-19 11:59:48 +01:00
|
|
|
func retrieveHookTasks(t *testing.T, hookID int64, activateWebhook bool) []*webhook.HookTask {
|
|
|
|
t.Helper()
|
|
|
|
if activateWebhook {
|
2024-03-31 10:34:37 +02:00
|
|
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
}))
|
|
|
|
t.Cleanup(s.Close)
|
|
|
|
updated, err := db.GetEngine(db.DefaultContext).ID(hookID).Cols("is_active", "url").Update(webhook.Webhook{
|
|
|
|
IsActive: true,
|
|
|
|
URL: s.URL,
|
|
|
|
})
|
|
|
|
|
|
|
|
// allow webhook deliveries on localhost
|
|
|
|
t.Cleanup(test.MockVariableValue(&setting.Webhook.AllowedHostList, hostmatcher.MatchBuiltinLoopback))
|
|
|
|
webhook_service.Init()
|
|
|
|
|
2024-03-19 11:59:48 +01:00
|
|
|
assert.Equal(t, int64(1), updated)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-03-19 11:59:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
hookTasks, err := webhook.HookTasks(db.DefaultContext, hookID, 1)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-03-19 11:59:48 +01:00
|
|
|
return hookTasks
|
|
|
|
}
|
|
|
|
|
2017-06-15 13:20:39 +02:00
|
|
|
func TestPullMerge(t *testing.T) {
|
2019-05-11 17:29:17 +02:00
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
2024-03-19 11:59:48 +01:00
|
|
|
hookTasks := retrieveHookTasks(t, 1, true)
|
2019-08-11 22:31:18 +02:00
|
|
|
hookTasksLenBefore := len(hookTasks)
|
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
|
|
|
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
|
2017-06-15 13:20:39 +02:00
|
|
|
|
2024-01-17 01:44:56 +01:00
|
|
|
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
|
2017-06-15 13:20:39 +02:00
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
elem := strings.Split(test.RedirectURL(resp), "/")
|
|
|
|
assert.EqualValues(t, "pulls", elem[3])
|
2024-01-17 01:44:56 +01:00
|
|
|
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
|
2019-08-11 22:31:18 +02:00
|
|
|
|
2024-03-19 11:59:48 +01:00
|
|
|
hookTasks = retrieveHookTasks(t, 1, false)
|
2019-08-11 22:31:18 +02:00
|
|
|
assert.Len(t, hookTasks, hookTasksLenBefore+1)
|
2019-05-11 17:29:17 +02:00
|
|
|
})
|
2018-01-05 19:56:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestPullRebase(t *testing.T) {
|
2019-05-11 17:29:17 +02:00
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
2024-03-19 11:59:48 +01:00
|
|
|
hookTasks := retrieveHookTasks(t, 1, true)
|
2019-08-11 22:31:18 +02:00
|
|
|
hookTasksLenBefore := len(hookTasks)
|
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
|
|
|
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
|
2018-01-05 19:56:50 +01:00
|
|
|
|
2024-01-17 01:44:56 +01:00
|
|
|
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
|
2018-01-05 19:56:50 +01:00
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
elem := strings.Split(test.RedirectURL(resp), "/")
|
|
|
|
assert.EqualValues(t, "pulls", elem[3])
|
2024-01-17 01:44:56 +01:00
|
|
|
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebase, false)
|
2019-08-11 22:31:18 +02:00
|
|
|
|
2024-03-19 11:59:48 +01:00
|
|
|
hookTasks = retrieveHookTasks(t, 1, false)
|
2019-08-11 22:31:18 +02:00
|
|
|
assert.Len(t, hookTasks, hookTasksLenBefore+1)
|
2019-05-11 17:29:17 +02:00
|
|
|
})
|
2018-01-05 19:56:50 +01:00
|
|
|
}
|
|
|
|
|
2018-12-27 11:27:08 +01:00
|
|
|
func TestPullRebaseMerge(t *testing.T) {
|
2019-05-11 17:29:17 +02:00
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
2024-03-19 11:59:48 +01:00
|
|
|
hookTasks := retrieveHookTasks(t, 1, true)
|
2019-08-11 22:31:18 +02:00
|
|
|
hookTasksLenBefore := len(hookTasks)
|
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
|
|
|
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
|
2018-12-27 11:27:08 +01:00
|
|
|
|
2024-01-17 01:44:56 +01:00
|
|
|
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
|
2018-12-27 11:27:08 +01:00
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
elem := strings.Split(test.RedirectURL(resp), "/")
|
|
|
|
assert.EqualValues(t, "pulls", elem[3])
|
2024-01-17 01:44:56 +01:00
|
|
|
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleRebaseMerge, false)
|
2019-08-11 22:31:18 +02:00
|
|
|
|
2024-03-19 11:59:48 +01:00
|
|
|
hookTasks = retrieveHookTasks(t, 1, false)
|
2019-08-11 22:31:18 +02:00
|
|
|
assert.Len(t, hookTasks, hookTasksLenBefore+1)
|
2019-05-11 17:29:17 +02:00
|
|
|
})
|
2018-12-27 11:27:08 +01:00
|
|
|
}
|
|
|
|
|
2018-01-05 19:56:50 +01:00
|
|
|
func TestPullSquash(t *testing.T) {
|
2019-05-11 17:29:17 +02:00
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
2024-03-19 11:59:48 +01:00
|
|
|
hookTasks := retrieveHookTasks(t, 1, true)
|
2019-08-11 22:31:18 +02:00
|
|
|
hookTasksLenBefore := len(hookTasks)
|
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
|
|
|
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
|
|
|
|
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited!)\n")
|
|
|
|
|
2024-01-17 01:44:56 +01:00
|
|
|
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "This is a pull title")
|
2019-05-11 17:29:17 +02:00
|
|
|
|
|
|
|
elem := strings.Split(test.RedirectURL(resp), "/")
|
|
|
|
assert.EqualValues(t, "pulls", elem[3])
|
2024-01-17 01:44:56 +01:00
|
|
|
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleSquash, false)
|
2019-08-11 22:31:18 +02:00
|
|
|
|
2024-03-19 11:59:48 +01:00
|
|
|
hookTasks = retrieveHookTasks(t, 1, false)
|
2019-08-11 22:31:18 +02:00
|
|
|
assert.Len(t, hookTasks, hookTasksLenBefore+1)
|
2019-05-11 17:29:17 +02:00
|
|
|
})
|
2017-06-15 13:20:39 +02:00
|
|
|
}
|
2017-06-21 03:00:03 +02:00
|
|
|
|
|
|
|
func TestPullCleanUpAfterMerge(t *testing.T) {
|
2019-05-11 17:29:17 +02:00
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
2020-02-10 00:09:31 +01:00
|
|
|
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited - TestPullCleanUpAfterMerge)\n")
|
2017-06-21 03:00:03 +02:00
|
|
|
|
2024-01-17 01:44:56 +01:00
|
|
|
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "feature/test", "This is a pull title")
|
2017-06-21 03:00:03 +02:00
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
elem := strings.Split(test.RedirectURL(resp), "/")
|
|
|
|
assert.EqualValues(t, "pulls", elem[3])
|
2024-01-17 01:44:56 +01:00
|
|
|
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
|
2017-06-21 03:00:03 +02:00
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
// Check PR branch deletion
|
|
|
|
resp = testPullCleanUp(t, session, elem[1], elem[2], elem[4])
|
|
|
|
respJSON := struct {
|
|
|
|
Redirect string
|
|
|
|
}{}
|
|
|
|
DecodeJSON(t, resp, &respJSON)
|
2017-06-21 03:00:03 +02:00
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
assert.NotEmpty(t, respJSON.Redirect, "Redirected URL is not found")
|
2017-06-21 03:00:03 +02:00
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
elem = strings.Split(respJSON.Redirect, "/")
|
|
|
|
assert.EqualValues(t, "pulls", elem[3])
|
2017-06-21 03:00:03 +02:00
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
// Check branch deletion result
|
|
|
|
req := NewRequest(t, "GET", respJSON.Redirect)
|
|
|
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
2017-06-21 03:00:03 +02:00
|
|
|
|
2019-05-11 17:29:17 +02:00
|
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
|
|
resultMsg := htmlDoc.doc.Find(".ui.message>p").Text()
|
2017-06-21 03:00:03 +02:00
|
|
|
|
2023-04-18 00:04:26 +02:00
|
|
|
assert.EqualValues(t, "Branch \"user1/repo1:feature/test\" has been deleted.", resultMsg)
|
2019-05-11 17:29:17 +02:00
|
|
|
})
|
2017-06-21 03:00:03 +02:00
|
|
|
}
|
2018-08-13 21:04:39 +02:00
|
|
|
|
|
|
|
func TestCantMergeWorkInProgress(t *testing.T) {
|
2019-05-11 17:29:17 +02:00
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
|
|
|
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
|
|
|
|
|
2024-01-17 01:44:56 +01:00
|
|
|
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "[wip] This is a pull title")
|
2019-05-11 17:29:17 +02:00
|
|
|
|
2023-06-19 10:25:36 +02:00
|
|
|
req := NewRequest(t, "GET", test.RedirectURL(resp))
|
2019-05-11 17:29:17 +02:00
|
|
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
2020-11-15 21:58:16 +01:00
|
|
|
text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text())
|
2019-05-11 17:29:17 +02:00
|
|
|
assert.NotEmpty(t, text, "Can't find WIP text")
|
|
|
|
|
2024-02-14 22:48:45 +01:00
|
|
|
assert.Contains(t, text, translation.NewLocale("en-US").TrString("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
|
2021-05-27 22:02:04 +02:00
|
|
|
assert.Contains(t, text, "[wip]", "Unable to find WIP text")
|
2019-05-11 17:29:17 +02:00
|
|
|
})
|
2018-08-13 21:04:39 +02:00
|
|
|
}
|
2019-11-10 09:42:51 +01:00
|
|
|
|
|
|
|
func TestCantMergeConflict(t *testing.T) {
|
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
|
|
|
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
|
|
|
|
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
|
|
|
|
|
|
|
|
// Use API to create a conflicting pr
|
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>
2023-06-04 20:57:16 +02:00
|
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
2023-12-22 00:59:59 +01:00
|
|
|
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
|
2019-11-10 09:42:51 +01:00
|
|
|
Head: "conflict",
|
|
|
|
Base: "base",
|
|
|
|
Title: "create a conflicting pr",
|
2023-12-22 00:59:59 +01:00
|
|
|
}).AddTokenAuth(token)
|
2022-03-23 05:54:07 +01:00
|
|
|
session.MakeRequest(t, req, http.StatusCreated)
|
2019-11-10 09:42:51 +01:00
|
|
|
|
|
|
|
// Now this PR will be marked conflict - or at least a race will do - so drop down to pure code at this point...
|
2021-11-24 10:49:20 +01:00
|
|
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
|
2019-11-10 09:42:51 +01:00
|
|
|
Name: "user1",
|
2022-08-16 04:22:25 +02:00
|
|
|
})
|
2021-12-10 02:27:50 +01:00
|
|
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
|
2019-11-10 09:42:51 +01:00
|
|
|
OwnerID: user1.ID,
|
|
|
|
Name: "repo1",
|
2022-08-16 04:22:25 +02:00
|
|
|
})
|
2019-11-10 09:42:51 +01:00
|
|
|
|
2022-06-13 11:37:59 +02:00
|
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
|
2019-11-10 09:42:51 +01:00
|
|
|
HeadRepoID: repo1.ID,
|
|
|
|
BaseRepoID: repo1.ID,
|
|
|
|
HeadBranch: "conflict",
|
|
|
|
BaseBranch: "base",
|
2022-08-16 04:22:25 +02:00
|
|
|
})
|
2019-11-10 09:42:51 +01:00
|
|
|
|
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-27 21:09:51 +01:00
|
|
|
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo1)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2019-11-10 09:42:51 +01:00
|
|
|
|
2022-11-03 16:49:00 +01:00
|
|
|
err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "CONFLICT", false)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.Error(t, err, "Merge should return an error due to conflict")
|
2019-11-10 09:42:51 +01:00
|
|
|
assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error")
|
|
|
|
|
2022-11-03 16:49:00 +01:00
|
|
|
err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleRebase, "", "CONFLICT", false)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.Error(t, err, "Merge should return an error due to conflict")
|
2019-11-10 09:42:51 +01:00
|
|
|
assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
|
2021-01-06 20:23:57 +01:00
|
|
|
gitRepo.Close()
|
2019-11-10 09:42:51 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCantMergeUnrelated(t *testing.T) {
|
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
|
|
|
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
|
|
|
|
|
|
|
|
// Now we want to create a commit on a branch that is totally unrelated to our current head
|
|
|
|
// Drop down to pure code at this point
|
2021-11-24 10:49:20 +01:00
|
|
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
|
2019-11-10 09:42:51 +01:00
|
|
|
Name: "user1",
|
2022-08-16 04:22:25 +02:00
|
|
|
})
|
2021-12-10 02:27:50 +01:00
|
|
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
|
2019-11-10 09:42:51 +01:00
|
|
|
OwnerID: user1.ID,
|
|
|
|
Name: "repo1",
|
2022-08-16 04:22:25 +02:00
|
|
|
})
|
2021-12-10 02:27:50 +01:00
|
|
|
path := repo_model.RepoPath(user1.Name, repo1.Name)
|
2019-11-10 09:42:51 +01:00
|
|
|
|
2022-04-01 04:55:30 +02:00
|
|
|
err := git.NewCommand(git.DefaultContext, "read-tree", "--empty").Run(&git.RunOpts{Dir: path})
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2019-11-10 09:42:51 +01:00
|
|
|
|
|
|
|
stdin := bytes.NewBufferString("Unrelated File")
|
|
|
|
var stdout strings.Builder
|
2022-04-01 04:55:30 +02:00
|
|
|
err = git.NewCommand(git.DefaultContext, "hash-object", "-w", "--stdin").Run(&git.RunOpts{
|
|
|
|
Dir: path,
|
|
|
|
Stdin: stdin,
|
|
|
|
Stdout: &stdout,
|
2022-02-11 13:47:22 +01:00
|
|
|
})
|
|
|
|
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2019-11-10 09:42:51 +01:00
|
|
|
sha := strings.TrimSpace(stdout.String())
|
|
|
|
|
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>
2023-02-04 03:30:43 +01:00
|
|
|
_, _, err = git.NewCommand(git.DefaultContext, "update-index", "--add", "--replace", "--cacheinfo").AddDynamicArguments("100644", sha, "somewher-over-the-rainbow").RunStdString(&git.RunOpts{Dir: path})
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2019-11-10 09:42:51 +01:00
|
|
|
|
2022-04-01 04:55:30 +02:00
|
|
|
treeSha, _, err := git.NewCommand(git.DefaultContext, "write-tree").RunStdString(&git.RunOpts{Dir: path})
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2019-11-10 09:42:51 +01:00
|
|
|
treeSha = strings.TrimSpace(treeSha)
|
|
|
|
|
|
|
|
commitTimeStr := time.Now().Format(time.RFC3339)
|
|
|
|
doerSig := user1.NewGitSig()
|
|
|
|
env := append(os.Environ(),
|
|
|
|
"GIT_AUTHOR_NAME="+doerSig.Name,
|
|
|
|
"GIT_AUTHOR_EMAIL="+doerSig.Email,
|
|
|
|
"GIT_AUTHOR_DATE="+commitTimeStr,
|
|
|
|
"GIT_COMMITTER_NAME="+doerSig.Name,
|
|
|
|
"GIT_COMMITTER_EMAIL="+doerSig.Email,
|
|
|
|
"GIT_COMMITTER_DATE="+commitTimeStr,
|
|
|
|
)
|
|
|
|
|
|
|
|
messageBytes := new(bytes.Buffer)
|
|
|
|
_, _ = messageBytes.WriteString("Unrelated")
|
|
|
|
_, _ = messageBytes.WriteString("\n")
|
|
|
|
|
|
|
|
stdout.Reset()
|
2022-10-23 16:44:45 +02:00
|
|
|
err = git.NewCommand(git.DefaultContext, "commit-tree").AddDynamicArguments(treeSha).
|
2022-04-01 04:55:30 +02:00
|
|
|
Run(&git.RunOpts{
|
|
|
|
Env: env,
|
|
|
|
Dir: path,
|
|
|
|
Stdin: messageBytes,
|
|
|
|
Stdout: &stdout,
|
2022-02-11 13:47:22 +01:00
|
|
|
})
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2019-11-10 09:42:51 +01:00
|
|
|
commitSha := strings.TrimSpace(stdout.String())
|
|
|
|
|
2022-10-23 16:44:45 +02:00
|
|
|
_, _, err = git.NewCommand(git.DefaultContext, "branch", "unrelated").AddDynamicArguments(commitSha).RunStdString(&git.RunOpts{Dir: path})
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2019-11-10 09:42:51 +01:00
|
|
|
|
|
|
|
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
|
|
|
|
|
|
|
|
// Use API to create a conflicting pr
|
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>
2023-06-04 20:57:16 +02:00
|
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
2023-12-22 00:59:59 +01:00
|
|
|
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
|
2019-11-10 09:42:51 +01:00
|
|
|
Head: "unrelated",
|
|
|
|
Base: "base",
|
|
|
|
Title: "create an unrelated pr",
|
2023-12-22 00:59:59 +01:00
|
|
|
}).AddTokenAuth(token)
|
2022-03-23 05:54:07 +01:00
|
|
|
session.MakeRequest(t, req, http.StatusCreated)
|
2019-11-10 09:42:51 +01:00
|
|
|
|
|
|
|
// Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point...
|
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-27 21:09:51 +01:00
|
|
|
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo1)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2022-06-13 11:37:59 +02:00
|
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
|
2019-11-10 09:42:51 +01:00
|
|
|
HeadRepoID: repo1.ID,
|
|
|
|
BaseRepoID: repo1.ID,
|
|
|
|
HeadBranch: "unrelated",
|
|
|
|
BaseBranch: "base",
|
2022-08-16 04:22:25 +02:00
|
|
|
})
|
2019-11-10 09:42:51 +01:00
|
|
|
|
2022-11-03 16:49:00 +01:00
|
|
|
err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED", false)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.Error(t, err, "Merge should return an error due to unrelated")
|
2019-11-10 09:42:51 +01:00
|
|
|
assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
|
2021-01-06 20:23:57 +01:00
|
|
|
gitRepo.Close()
|
2019-11-10 09:42:51 +01:00
|
|
|
})
|
|
|
|
}
|
2022-04-21 23:55:45 +02:00
|
|
|
|
2024-02-12 23:37:23 +01:00
|
|
|
func TestFastForwardOnlyMerge(t *testing.T) {
|
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
|
|
|
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "update", "README.md", "Hello, World 2\n")
|
|
|
|
|
|
|
|
// Use API to create a pr from update to master
|
|
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
|
|
|
|
Head: "update",
|
|
|
|
Base: "master",
|
|
|
|
Title: "create a pr that can be fast-forward-only merged",
|
|
|
|
}).AddTokenAuth(token)
|
|
|
|
session.MakeRequest(t, req, http.StatusCreated)
|
|
|
|
|
|
|
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
|
|
|
|
Name: "user1",
|
|
|
|
})
|
|
|
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
|
|
|
|
OwnerID: user1.ID,
|
|
|
|
Name: "repo1",
|
|
|
|
})
|
|
|
|
|
|
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
|
|
|
|
HeadRepoID: repo1.ID,
|
|
|
|
BaseRepoID: repo1.ID,
|
|
|
|
HeadBranch: "update",
|
|
|
|
BaseBranch: "master",
|
|
|
|
})
|
|
|
|
|
|
|
|
gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-02-12 23:37:23 +01:00
|
|
|
|
|
|
|
err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "FAST-FORWARD-ONLY", false)
|
|
|
|
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-02-12 23:37:23 +01:00
|
|
|
|
|
|
|
gitRepo.Close()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCantFastForwardOnlyMergeDiverging(t *testing.T) {
|
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
|
|
|
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "diverging", "README.md", "Hello, World diverged\n")
|
|
|
|
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World 2\n")
|
|
|
|
|
|
|
|
// Use API to create a pr from diverging to update
|
|
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", "user1", "repo1"), &api.CreatePullRequestOption{
|
|
|
|
Head: "diverging",
|
|
|
|
Base: "master",
|
|
|
|
Title: "create a pr from a diverging branch",
|
|
|
|
}).AddTokenAuth(token)
|
|
|
|
session.MakeRequest(t, req, http.StatusCreated)
|
|
|
|
|
|
|
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{
|
|
|
|
Name: "user1",
|
|
|
|
})
|
|
|
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
|
|
|
|
OwnerID: user1.ID,
|
|
|
|
Name: "repo1",
|
|
|
|
})
|
|
|
|
|
|
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
|
|
|
|
HeadRepoID: repo1.ID,
|
|
|
|
BaseRepoID: repo1.ID,
|
|
|
|
HeadBranch: "diverging",
|
|
|
|
BaseBranch: "master",
|
|
|
|
})
|
|
|
|
|
|
|
|
gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-02-12 23:37:23 +01:00
|
|
|
|
|
|
|
err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleFastForwardOnly, "", "DIVERGING", false)
|
|
|
|
|
2024-07-30 21:41:10 +02:00
|
|
|
require.Error(t, err, "Merge should return an error due to being for a diverging branch")
|
2024-02-12 23:37:23 +01:00
|
|
|
assert.True(t, models.IsErrMergeDivergingFastForwardOnly(err), "Merge error is not a diverging fast-forward-only error")
|
|
|
|
|
|
|
|
gitRepo.Close()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-04-21 23:55:45 +02:00
|
|
|
func TestConflictChecking(t *testing.T) {
|
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
2022-08-16 04:22:25 +02:00
|
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
2022-04-21 23:55:45 +02:00
|
|
|
|
|
|
|
// Create new clean repo to test conflict checking.
|
2024-01-12 11:53:14 +01:00
|
|
|
baseRepo, _, f := CreateDeclarativeRepo(t, user, "conflict-checking", nil, nil, nil)
|
|
|
|
defer f()
|
2022-04-21 23:55:45 +02:00
|
|
|
|
|
|
|
// create a commit on new branch.
|
2024-01-12 11:53:14 +01:00
|
|
|
_, err := files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
|
2023-05-29 11:41:35 +02:00
|
|
|
Files: []*files_service.ChangeRepoFile{
|
|
|
|
{
|
2023-07-18 20:14:47 +02:00
|
|
|
Operation: "create",
|
|
|
|
TreePath: "important_file",
|
|
|
|
ContentReader: strings.NewReader("Just a non-important file"),
|
2023-05-29 11:41:35 +02:00
|
|
|
},
|
|
|
|
},
|
2022-04-21 23:55:45 +02:00
|
|
|
Message: "Add a important file",
|
|
|
|
OldBranch: "main",
|
|
|
|
NewBranch: "important-secrets",
|
|
|
|
})
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2022-04-21 23:55:45 +02:00
|
|
|
|
|
|
|
// create a commit on main branch.
|
2023-05-29 11:41:35 +02:00
|
|
|
_, err = files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user, &files_service.ChangeRepoFilesOptions{
|
|
|
|
Files: []*files_service.ChangeRepoFile{
|
|
|
|
{
|
2023-07-18 20:14:47 +02:00
|
|
|
Operation: "create",
|
|
|
|
TreePath: "important_file",
|
|
|
|
ContentReader: strings.NewReader("Not the same content :P"),
|
2023-05-29 11:41:35 +02:00
|
|
|
},
|
|
|
|
},
|
2022-04-21 23:55:45 +02:00
|
|
|
Message: "Add a important file",
|
|
|
|
OldBranch: "main",
|
|
|
|
NewBranch: "main",
|
|
|
|
})
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2022-04-21 23:55:45 +02:00
|
|
|
|
|
|
|
// create Pull to merge the important-secrets branch into main branch.
|
2022-06-13 11:37:59 +02:00
|
|
|
pullIssue := &issues_model.Issue{
|
2022-04-21 23:55:45 +02:00
|
|
|
RepoID: baseRepo.ID,
|
|
|
|
Title: "PR with conflict!",
|
|
|
|
PosterID: user.ID,
|
|
|
|
Poster: user,
|
|
|
|
IsPull: true,
|
|
|
|
}
|
|
|
|
|
2022-06-13 11:37:59 +02:00
|
|
|
pullRequest := &issues_model.PullRequest{
|
2022-04-21 23:55:45 +02:00
|
|
|
HeadRepoID: baseRepo.ID,
|
|
|
|
BaseRepoID: baseRepo.ID,
|
|
|
|
HeadBranch: "important-secrets",
|
|
|
|
BaseBranch: "main",
|
|
|
|
HeadRepo: baseRepo,
|
|
|
|
BaseRepo: baseRepo,
|
2022-06-13 11:37:59 +02:00
|
|
|
Type: issues_model.PullRequestGitea,
|
2022-04-21 23:55:45 +02:00
|
|
|
}
|
|
|
|
err = pull.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2022-04-21 23:55:45 +02:00
|
|
|
|
2022-08-16 04:22:25 +02:00
|
|
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, issue.LoadPullRequest(db.DefaultContext))
|
2024-03-21 14:13:08 +01:00
|
|
|
conflictingPR := issue.PullRequest
|
2022-04-21 23:55:45 +02:00
|
|
|
|
|
|
|
// Ensure conflictedFiles is populated.
|
2023-04-22 23:56:27 +02:00
|
|
|
assert.Len(t, conflictingPR.ConflictedFiles, 1)
|
2022-04-21 23:55:45 +02:00
|
|
|
// Check if status is correct.
|
2022-06-13 11:37:59 +02:00
|
|
|
assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
|
2022-04-21 23:55:45 +02:00
|
|
|
// Ensure that mergeable returns false
|
2023-10-11 06:24:07 +02:00
|
|
|
assert.False(t, conflictingPR.Mergeable(db.DefaultContext))
|
2022-04-21 23:55:45 +02:00
|
|
|
})
|
|
|
|
}
|
2024-01-17 01:44:56 +01:00
|
|
|
|
|
|
|
func TestPullRetargetChildOnBranchDelete(t *testing.T) {
|
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testEditFileToNewBranch(t, session, "user2", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
|
|
|
testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullRetargetOnCleanup - base PR)\n(Edited - TestPullRetargetOnCleanup - child PR)")
|
|
|
|
|
|
|
|
respBasePR := testPullCreate(t, session, "user2", "repo1", true, "master", "base-pr", "Base Pull Request")
|
|
|
|
elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/")
|
|
|
|
assert.EqualValues(t, "pulls", elemBasePR[3])
|
|
|
|
|
|
|
|
respChildPR := testPullCreate(t, session, "user1", "repo1", false, "base-pr", "child-pr", "Child Pull Request")
|
|
|
|
elemChildPR := strings.Split(test.RedirectURL(respChildPR), "/")
|
|
|
|
assert.EqualValues(t, "pulls", elemChildPR[3])
|
|
|
|
|
|
|
|
testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
|
|
|
|
|
|
|
|
// Check child PR
|
|
|
|
req := NewRequest(t, "GET", test.RedirectURL(respChildPR))
|
|
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
|
|
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
|
|
targetBranch := htmlDoc.doc.Find("#branch_target>a").Text()
|
|
|
|
prStatus := strings.TrimSpace(htmlDoc.doc.Find(".issue-title-meta>.issue-state-label").Text())
|
|
|
|
|
|
|
|
assert.EqualValues(t, "master", targetBranch)
|
|
|
|
assert.EqualValues(t, "Open", prStatus)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestPullDontRetargetChildOnWrongRepo(t *testing.T) {
|
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
|
|
|
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n")
|
|
|
|
testEditFileToNewBranch(t, session, "user1", "repo1", "base-pr", "child-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n(Edited - TestPullDontRetargetChildOnWrongRepo - child PR)")
|
|
|
|
|
|
|
|
respBasePR := testPullCreate(t, session, "user1", "repo1", false, "master", "base-pr", "Base Pull Request")
|
|
|
|
elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/")
|
|
|
|
assert.EqualValues(t, "pulls", elemBasePR[3])
|
|
|
|
|
|
|
|
respChildPR := testPullCreate(t, session, "user1", "repo1", true, "base-pr", "child-pr", "Child Pull Request")
|
|
|
|
elemChildPR := strings.Split(test.RedirectURL(respChildPR), "/")
|
|
|
|
assert.EqualValues(t, "pulls", elemChildPR[3])
|
|
|
|
|
|
|
|
testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
|
|
|
|
|
|
|
|
// Check child PR
|
|
|
|
req := NewRequest(t, "GET", test.RedirectURL(respChildPR))
|
|
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
|
|
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
|
|
targetBranch := htmlDoc.doc.Find("#branch_target>a").Text()
|
|
|
|
prStatus := strings.TrimSpace(htmlDoc.doc.Find(".issue-title-meta>.issue-state-label").Text())
|
|
|
|
|
|
|
|
assert.EqualValues(t, "base-pr", targetBranch)
|
|
|
|
assert.EqualValues(t, "Closed", prStatus)
|
|
|
|
})
|
|
|
|
}
|
2024-05-08 16:45:15 +02:00
|
|
|
|
|
|
|
func TestPullMergeIndexerNotifier(t *testing.T) {
|
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
|
|
|
// create a pull request
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
|
|
|
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
|
|
|
|
createPullResp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "Indexer notifier test pull")
|
|
|
|
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, queue.GetManager().FlushAll(context.Background(), 0))
|
2024-05-08 16:45:15 +02:00
|
|
|
time.Sleep(time.Second)
|
|
|
|
|
|
|
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
|
|
|
|
OwnerName: "user2",
|
|
|
|
Name: "repo1",
|
|
|
|
})
|
|
|
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
|
|
|
|
RepoID: repo1.ID,
|
|
|
|
Title: "Indexer notifier test pull",
|
|
|
|
IsPull: true,
|
|
|
|
IsClosed: false,
|
|
|
|
})
|
|
|
|
|
|
|
|
// build the request for searching issues
|
|
|
|
link, _ := url.Parse("/api/v1/repos/issues/search")
|
|
|
|
query := url.Values{}
|
|
|
|
query.Add("state", "closed")
|
|
|
|
query.Add("type", "pulls")
|
|
|
|
query.Add("q", "notifier")
|
|
|
|
link.RawQuery = query.Encode()
|
|
|
|
|
|
|
|
// search issues
|
|
|
|
searchIssuesResp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
|
|
|
var apiIssuesBefore []*api.Issue
|
|
|
|
DecodeJSON(t, searchIssuesResp, &apiIssuesBefore)
|
2024-07-30 21:41:10 +02:00
|
|
|
assert.Empty(t, apiIssuesBefore)
|
2024-05-08 16:45:15 +02:00
|
|
|
|
|
|
|
// merge the pull request
|
|
|
|
elem := strings.Split(test.RedirectURL(createPullResp), "/")
|
|
|
|
assert.EqualValues(t, "pulls", elem[3])
|
|
|
|
testPullMerge(t, session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
|
|
|
|
|
|
|
|
// check if the issue is closed
|
|
|
|
issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
|
|
|
|
ID: issue.ID,
|
|
|
|
})
|
|
|
|
assert.True(t, issue.IsClosed)
|
|
|
|
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, queue.GetManager().FlushAll(context.Background(), 0))
|
2024-05-08 16:45:15 +02:00
|
|
|
time.Sleep(time.Second)
|
|
|
|
|
|
|
|
// search issues again
|
|
|
|
searchIssuesResp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
|
|
|
var apiIssuesAfter []*api.Issue
|
|
|
|
DecodeJSON(t, searchIssuesResp, &apiIssuesAfter)
|
|
|
|
if assert.Len(t, apiIssuesAfter, 1) {
|
|
|
|
assert.Equal(t, issue.ID, apiIssuesAfter[0].ID)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2024-05-21 17:23:22 +02:00
|
|
|
|
|
|
|
func testResetRepo(t *testing.T, repoPath, branch, commitID string) {
|
|
|
|
f, err := os.OpenFile(filepath.Join(repoPath, "refs", "heads", branch), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
_, err = f.WriteString(commitID + "\n")
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
f.Close()
|
|
|
|
|
|
|
|
repo, err := git.OpenRepository(context.Background(), repoPath)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
defer repo.Close()
|
|
|
|
id, err := repo.GetBranchCommitID(branch)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
assert.EqualValues(t, commitID, id)
|
|
|
|
}
|
|
|
|
|
2024-06-01 10:58:46 +02:00
|
|
|
func TestPullMergeBranchProtect(t *testing.T) {
|
|
|
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
|
|
|
admin := "user1"
|
|
|
|
owner := "user5"
|
|
|
|
notOwner := "user4"
|
|
|
|
repo := "repo4"
|
|
|
|
|
|
|
|
dstPath := t.TempDir()
|
|
|
|
|
|
|
|
u.Path = fmt.Sprintf("%s/%s.git", owner, repo)
|
|
|
|
u.User = url.UserPassword(owner, userPassword)
|
|
|
|
|
|
|
|
t.Run("Clone", doGitClone(dstPath, u))
|
|
|
|
|
|
|
|
for _, testCase := range []struct {
|
|
|
|
name string
|
|
|
|
doer string
|
|
|
|
expectedCode map[string]int
|
|
|
|
filename string
|
|
|
|
protectBranch parameterProtectBranch
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "SuccessAdminNotEnoughMergeRequiredApprovals",
|
|
|
|
doer: admin,
|
|
|
|
expectedCode: map[string]int{"api": http.StatusOK, "web": http.StatusOK},
|
|
|
|
filename: "branch-data-file-",
|
|
|
|
protectBranch: parameterProtectBranch{
|
|
|
|
"required_approvals": "1",
|
|
|
|
"apply_to_admins": "true",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "FailOwnerProtectedFile",
|
|
|
|
doer: owner,
|
|
|
|
expectedCode: map[string]int{"api": http.StatusMethodNotAllowed, "web": http.StatusBadRequest},
|
|
|
|
filename: "protected-file-",
|
|
|
|
protectBranch: parameterProtectBranch{
|
|
|
|
"protected_file_patterns": "protected-file-*",
|
|
|
|
"apply_to_admins": "true",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "OwnerProtectedFile",
|
|
|
|
doer: owner,
|
|
|
|
expectedCode: map[string]int{"api": http.StatusOK, "web": http.StatusOK},
|
|
|
|
filename: "protected-file-",
|
|
|
|
protectBranch: parameterProtectBranch{
|
|
|
|
"protected_file_patterns": "protected-file-*",
|
|
|
|
"apply_to_admins": "false",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "FailNotOwnerProtectedFile",
|
|
|
|
doer: notOwner,
|
|
|
|
expectedCode: map[string]int{"api": http.StatusMethodNotAllowed, "web": http.StatusBadRequest},
|
|
|
|
filename: "protected-file-",
|
|
|
|
protectBranch: parameterProtectBranch{
|
|
|
|
"protected_file_patterns": "protected-file-*",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "FailOwnerNotEnoughMergeRequiredApprovals",
|
|
|
|
doer: owner,
|
|
|
|
expectedCode: map[string]int{"api": http.StatusMethodNotAllowed, "web": http.StatusBadRequest},
|
|
|
|
filename: "branch-data-file-",
|
|
|
|
protectBranch: parameterProtectBranch{
|
|
|
|
"required_approvals": "1",
|
|
|
|
"apply_to_admins": "true",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "SuccessOwnerNotEnoughMergeRequiredApprovals",
|
|
|
|
doer: owner,
|
|
|
|
expectedCode: map[string]int{"api": http.StatusOK, "web": http.StatusOK},
|
|
|
|
filename: "branch-data-file-",
|
|
|
|
protectBranch: parameterProtectBranch{
|
|
|
|
"required_approvals": "1",
|
|
|
|
"apply_to_admins": "false",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "FailNotOwnerNotEnoughMergeRequiredApprovals",
|
|
|
|
doer: notOwner,
|
|
|
|
expectedCode: map[string]int{"api": http.StatusMethodNotAllowed, "web": http.StatusBadRequest},
|
|
|
|
filename: "branch-data-file-",
|
|
|
|
protectBranch: parameterProtectBranch{
|
|
|
|
"required_approvals": "1",
|
|
|
|
"apply_to_admins": "false",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "SuccessNotOwner",
|
|
|
|
doer: notOwner,
|
|
|
|
expectedCode: map[string]int{"api": http.StatusOK, "web": http.StatusOK},
|
|
|
|
filename: "branch-data-file-",
|
|
|
|
protectBranch: parameterProtectBranch{
|
|
|
|
"required_approvals": "0",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
} {
|
|
|
|
mergeWith := func(t *testing.T, ctx APITestContext, apiOrWeb string, expectedCode int, pr int64) {
|
|
|
|
switch apiOrWeb {
|
|
|
|
case "api":
|
|
|
|
ctx.ExpectedCode = expectedCode
|
|
|
|
doAPIMergePullRequestForm(t, ctx, owner, repo, pr,
|
|
|
|
&forms.MergePullRequestForm{
|
|
|
|
MergeMessageField: "doAPIMergePullRequest Merge",
|
|
|
|
Do: string(repo_model.MergeStyleMerge),
|
|
|
|
ForceMerge: true,
|
|
|
|
})
|
|
|
|
ctx.ExpectedCode = 0
|
|
|
|
case "web":
|
|
|
|
testPullMergeForm(t, ctx.Session, expectedCode, owner, repo, fmt.Sprintf("%d", pr), optionsPullMerge{
|
|
|
|
"do": string(repo_model.MergeStyleMerge),
|
|
|
|
"force_merge": "true",
|
|
|
|
})
|
|
|
|
default:
|
|
|
|
panic(apiOrWeb)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, withAPIOrWeb := range []string{"api", "web"} {
|
|
|
|
t.Run(testCase.name+" "+withAPIOrWeb, func(t *testing.T) {
|
|
|
|
branch := testCase.name + "-" + withAPIOrWeb
|
|
|
|
unprotected := branch + "-unprotected"
|
|
|
|
doGitCheckoutBranch(dstPath, "master")(t)
|
|
|
|
doGitCreateBranch(dstPath, branch)(t)
|
|
|
|
doGitPushTestRepository(dstPath, "origin", branch)(t)
|
|
|
|
|
|
|
|
ctx := NewAPITestContext(t, owner, repo, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
|
|
doProtectBranch(ctx, branch, testCase.protectBranch)(t)
|
|
|
|
|
|
|
|
ctx = NewAPITestContext(t, testCase.doer, "not used", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
|
|
ctx.Username = owner
|
|
|
|
ctx.Reponame = repo
|
|
|
|
_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", testCase.filename)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-06-01 10:58:46 +02:00
|
|
|
doGitPushTestRepository(dstPath, "origin", branch+":"+unprotected)(t)
|
|
|
|
pr, err := doAPICreatePullRequest(ctx, owner, repo, branch, unprotected)(t)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-06-01 10:58:46 +02:00
|
|
|
mergeWith(t, ctx, withAPIOrWeb, testCase.expectedCode[withAPIOrWeb], pr.Index)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-05-21 17:23:22 +02:00
|
|
|
func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
|
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
|
|
|
// create a pull request
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
|
|
|
forkedName := "repo1-1"
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", forkedName)
|
|
|
|
defer func() {
|
|
|
|
testDeleteRepository(t, session, "user1", forkedName)
|
|
|
|
}()
|
|
|
|
testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n")
|
|
|
|
testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull")
|
|
|
|
|
|
|
|
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
|
|
|
|
forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName})
|
|
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
|
|
|
|
BaseRepoID: baseRepo.ID,
|
|
|
|
BaseBranch: "master",
|
|
|
|
HeadRepoID: forkedRepo.ID,
|
|
|
|
HeadBranch: "master",
|
|
|
|
})
|
|
|
|
|
|
|
|
// add protected branch for commit status
|
|
|
|
csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
|
|
|
|
// Change master branch to protected
|
|
|
|
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
|
|
|
|
"_csrf": csrf,
|
|
|
|
"rule_name": "master",
|
|
|
|
"enable_push": "true",
|
|
|
|
"enable_status_check": "true",
|
|
|
|
"status_check_contexts": "gitea/actions",
|
|
|
|
})
|
|
|
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
|
|
|
|
|
|
|
// first time insert automerge record, return true
|
|
|
|
scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
assert.True(t, scheduled)
|
|
|
|
|
|
|
|
// second time insert automerge record, return false because it does exist
|
|
|
|
scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
|
2024-07-30 21:41:10 +02:00
|
|
|
require.Error(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
assert.False(t, scheduled)
|
|
|
|
|
|
|
|
// reload pr again
|
|
|
|
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
|
|
|
assert.False(t, pr.HasMerged)
|
|
|
|
assert.Empty(t, pr.MergedCommitID)
|
|
|
|
|
|
|
|
// update commit status to success, then it should be merged automatically
|
|
|
|
baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
|
|
|
|
branches, _, err := baseGitRepo.GetBranchNames(0, 100)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
assert.ElementsMatch(t, []string{"sub-home-md-img-check", "home-md-img-check", "pr-to-update", "branch2", "DefaultBranch", "develop", "feature/1", "master"}, branches)
|
|
|
|
baseGitRepo.Close()
|
|
|
|
defer func() {
|
|
|
|
testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
|
|
|
|
}()
|
|
|
|
|
|
|
|
err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{
|
|
|
|
State: api.CommitStatusSuccess,
|
|
|
|
TargetURL: "https://gitea.com",
|
|
|
|
Context: "gitea/actions",
|
|
|
|
})
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
|
|
|
|
// realod pr again
|
|
|
|
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
|
|
|
assert.True(t, pr.HasMerged)
|
|
|
|
assert.NotEmpty(t, pr.MergedCommitID)
|
|
|
|
|
|
|
|
unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestPullAutoMergeAfterCommitStatusSucceedAndApproval(t *testing.T) {
|
|
|
|
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
|
|
|
// create a pull request
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
|
|
|
forkedName := "repo1-2"
|
|
|
|
testRepoFork(t, session, "user2", "repo1", "user1", forkedName)
|
|
|
|
defer func() {
|
|
|
|
testDeleteRepository(t, session, "user1", forkedName)
|
|
|
|
}()
|
|
|
|
testEditFile(t, session, "user1", forkedName, "master", "README.md", "Hello, World (Edited)\n")
|
|
|
|
testPullCreate(t, session, "user1", forkedName, false, "master", "master", "Indexer notifier test pull")
|
|
|
|
|
|
|
|
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
|
|
|
|
forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: forkedName})
|
|
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
|
|
|
|
BaseRepoID: baseRepo.ID,
|
|
|
|
BaseBranch: "master",
|
|
|
|
HeadRepoID: forkedRepo.ID,
|
|
|
|
HeadBranch: "master",
|
|
|
|
})
|
|
|
|
|
|
|
|
// add protected branch for commit status
|
|
|
|
csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
|
|
|
|
// Change master branch to protected
|
|
|
|
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
|
|
|
|
"_csrf": csrf,
|
|
|
|
"rule_name": "master",
|
|
|
|
"enable_push": "true",
|
|
|
|
"enable_status_check": "true",
|
|
|
|
"status_check_contexts": "gitea/actions",
|
|
|
|
"required_approvals": "1",
|
|
|
|
})
|
|
|
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
|
|
|
|
|
|
|
// first time insert automerge record, return true
|
|
|
|
scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
assert.True(t, scheduled)
|
|
|
|
|
|
|
|
// second time insert automerge record, return false because it does exist
|
|
|
|
scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
|
2024-07-30 21:41:10 +02:00
|
|
|
require.Error(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
assert.False(t, scheduled)
|
|
|
|
|
|
|
|
// reload pr again
|
|
|
|
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
|
|
|
assert.False(t, pr.HasMerged)
|
|
|
|
assert.Empty(t, pr.MergedCommitID)
|
|
|
|
|
|
|
|
// update commit status to success, then it should be merged automatically
|
|
|
|
baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
baseGitRepo.Close()
|
|
|
|
defer func() {
|
|
|
|
testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
|
|
|
|
}()
|
|
|
|
|
|
|
|
err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{
|
|
|
|
State: api.CommitStatusSuccess,
|
|
|
|
TargetURL: "https://gitea.com",
|
|
|
|
Context: "gitea/actions",
|
|
|
|
})
|
2024-07-30 21:41:10 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-21 17:23:22 +02:00
|
|
|
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
|
|
|
|
// reload pr again
|
|
|
|
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
|
|
|
assert.False(t, pr.HasMerged)
|
|
|
|
assert.Empty(t, pr.MergedCommitID)
|
|
|
|
|
|
|
|
// approve the PR from non-author
|
|
|
|
approveSession := loginUser(t, "user2")
|
|
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/user2/repo1/pulls/%d", pr.Index))
|
|
|
|
resp := approveSession.MakeRequest(t, req, http.StatusOK)
|
|
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
|
|
testSubmitReview(t, approveSession, htmlDoc.GetCSRF(), "user2", "repo1", strconv.Itoa(int(pr.Index)), sha, "approve", http.StatusOK)
|
|
|
|
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
|
|
|
|
// realod pr again
|
|
|
|
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
|
|
|
assert.True(t, pr.HasMerged)
|
|
|
|
assert.NotEmpty(t, pr.MergedCommitID)
|
|
|
|
|
|
|
|
unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
|
|
|
|
})
|
|
|
|
}
|
2024-08-20 08:17:21 +02:00
|
|
|
|
|
|
|
func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing.T) {
|
|
|
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
|
|
|
// create a pull request
|
|
|
|
baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
|
|
|
|
|
|
|
dstPath := t.TempDir()
|
|
|
|
|
|
|
|
u.Path = baseAPITestContext.GitPath()
|
|
|
|
u.User = url.UserPassword("user2", userPassword)
|
|
|
|
|
|
|
|
t.Run("Clone", doGitClone(dstPath, u))
|
|
|
|
|
|
|
|
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
err = git.AddChanges(dstPath, true)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
err = git.CommitChanges(dstPath, git.CommitChangesOptions{
|
|
|
|
Committer: &git.Signature{
|
|
|
|
Email: "user2@example.com",
|
|
|
|
Name: "user2",
|
|
|
|
When: time.Now(),
|
|
|
|
},
|
|
|
|
Author: &git.Signature{
|
|
|
|
Email: "user2@example.com",
|
|
|
|
Name: "user2",
|
|
|
|
When: time.Now(),
|
|
|
|
},
|
|
|
|
Message: "Testing commit 1",
|
|
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
stderrBuf := &bytes.Buffer{}
|
|
|
|
|
|
|
|
err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").
|
|
|
|
AddDynamicArguments(`topic=test/head2`).
|
|
|
|
AddArguments("-o").
|
|
|
|
AddDynamicArguments(`title="create a test pull request with agit"`).
|
|
|
|
AddArguments("-o").
|
|
|
|
AddDynamicArguments(`description="This PR is a test pull request which created with agit"`).
|
|
|
|
Run(&git.RunOpts{Dir: dstPath, Stderr: stderrBuf})
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
assert.Contains(t, stderrBuf.String(), setting.AppURL+"user2/repo1/pulls/6")
|
|
|
|
|
|
|
|
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
|
|
|
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
|
|
|
|
Flow: issues_model.PullRequestFlowAGit,
|
|
|
|
BaseRepoID: baseRepo.ID,
|
|
|
|
BaseBranch: "master",
|
|
|
|
HeadRepoID: baseRepo.ID,
|
|
|
|
HeadBranch: "user2/test/head2",
|
|
|
|
})
|
|
|
|
|
|
|
|
session := loginUser(t, "user1")
|
|
|
|
// add protected branch for commit status
|
|
|
|
csrf := GetCSRF(t, session, "/user2/repo1/settings/branches")
|
|
|
|
// Change master branch to protected
|
|
|
|
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
|
|
|
|
"_csrf": csrf,
|
|
|
|
"rule_name": "master",
|
|
|
|
"enable_push": "true",
|
|
|
|
"enable_status_check": "true",
|
|
|
|
"status_check_contexts": "gitea/actions",
|
|
|
|
"required_approvals": "1",
|
|
|
|
})
|
|
|
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
|
|
|
|
|
|
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
|
|
|
// first time insert automerge record, return true
|
|
|
|
scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.True(t, scheduled)
|
|
|
|
|
|
|
|
// second time insert automerge record, return false because it does exist
|
|
|
|
scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test")
|
|
|
|
assert.Error(t, err)
|
|
|
|
assert.False(t, scheduled)
|
|
|
|
|
|
|
|
// reload pr again
|
|
|
|
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
|
|
|
assert.False(t, pr.HasMerged)
|
|
|
|
assert.Empty(t, pr.MergedCommitID)
|
|
|
|
|
|
|
|
// update commit status to success, then it should be merged automatically
|
|
|
|
baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
|
|
|
|
assert.NoError(t, err)
|
|
|
|
masterCommitID, err := baseGitRepo.GetBranchCommitID("master")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
baseGitRepo.Close()
|
|
|
|
defer func() {
|
|
|
|
testResetRepo(t, baseRepo.RepoPath(), "master", masterCommitID)
|
|
|
|
}()
|
|
|
|
|
|
|
|
err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{
|
|
|
|
State: api.CommitStatusSuccess,
|
|
|
|
TargetURL: "https://gitea.com",
|
|
|
|
Context: "gitea/actions",
|
|
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
|
|
|
|
// reload pr again
|
|
|
|
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
|
|
|
assert.False(t, pr.HasMerged)
|
|
|
|
assert.Empty(t, pr.MergedCommitID)
|
|
|
|
|
|
|
|
// approve the PR from non-author
|
|
|
|
approveSession := loginUser(t, "user1")
|
|
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/user2/repo1/pulls/%d", pr.Index))
|
|
|
|
resp := approveSession.MakeRequest(t, req, http.StatusOK)
|
|
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
|
|
testSubmitReview(t, approveSession, htmlDoc.GetCSRF(), "user2", "repo1", strconv.Itoa(int(pr.Index)), sha, "approve", http.StatusOK)
|
|
|
|
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
|
|
|
|
// realod pr again
|
|
|
|
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
|
|
|
assert.True(t, pr.HasMerged)
|
|
|
|
assert.NotEmpty(t, pr.MergedCommitID)
|
|
|
|
|
|
|
|
unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
|
|
|
|
})
|
|
|
|
}
|