mirror of
https://github.com/go-gitea/gitea.git
synced 2025-06-21 22:20:27 +02:00
The merge-upstream route was so far performing any kind of merge, even those that would create merge commits and thus make your branch diverge from upstream, requiring manual intervention via the git cli to undo the damage. With the new optional parameter ff_only, we can instruct gitea to error out, if a non-fast-forward merge would be performed.
184 lines
8.2 KiB
Go
184 lines
8.2 KiB
Go
// Copyright 2025 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
auth_model "code.gitea.io/gitea/models/auth"
|
|
"code.gitea.io/gitea/models/db"
|
|
git_model "code.gitea.io/gitea/models/git"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/models/unittest"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
api "code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/util"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestRepoMergeUpstream(t *testing.T) {
|
|
onGiteaRun(t, func(*testing.T, *url.URL) {
|
|
forkUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
|
|
|
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
baseUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: baseRepo.OwnerID})
|
|
|
|
checkFileContent := func(branch, exp string) {
|
|
req := NewRequest(t, "GET", fmt.Sprintf("/%s/test-repo-fork/raw/branch/%s/new-file.txt", forkUser.Name, branch))
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
require.Equal(t, exp, resp.Body.String())
|
|
}
|
|
|
|
session := loginUser(t, forkUser.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
// create a fork
|
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseUser.Name, baseRepo.Name), &api.CreateForkOption{
|
|
Name: util.ToPointer("test-repo-fork"),
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusAccepted)
|
|
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: forkUser.ID, Name: "test-repo-fork"})
|
|
|
|
// create fork-branch
|
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/test-repo-fork/branches/_new/branch/master", forkUser.Name), map[string]string{
|
|
"_csrf": GetUserCSRFToken(t, session),
|
|
"new_branch_name": "fork-branch",
|
|
})
|
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
|
|
|
queryMergeUpstreamButtonLink := func(htmlDoc *HTMLDoc) string {
|
|
return htmlDoc.Find(`button[data-url*="merge-upstream"]`).AttrOr("data-url", "")
|
|
}
|
|
|
|
t.Run("HeadBeforeBase", func(t *testing.T) {
|
|
// add a file in base repo
|
|
sessionBaseUser := loginUser(t, baseUser.Name)
|
|
require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-1"))
|
|
|
|
var mergeUpstreamLink string
|
|
t.Run("DetectDefaultBranch", func(t *testing.T) {
|
|
// the repo shows a prompt to "sync fork" (defaults to the default branch)
|
|
require.Eventually(t, func() bool {
|
|
resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
|
|
if mergeUpstreamLink == "" {
|
|
return false
|
|
}
|
|
respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
|
|
return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/master">user2/repo1:master</a>`)
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
})
|
|
|
|
t.Run("DetectSameBranch", func(t *testing.T) {
|
|
// if the fork-branch name also exists in the base repo, then use that branch instead
|
|
req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/master", map[string]string{
|
|
"_csrf": GetUserCSRFToken(t, sessionBaseUser),
|
|
"new_branch_name": "fork-branch",
|
|
})
|
|
sessionBaseUser.MakeRequest(t, req, http.StatusSeeOther)
|
|
|
|
require.Eventually(t, func() bool {
|
|
resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
|
|
if mergeUpstreamLink == "" {
|
|
return false
|
|
}
|
|
respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
|
|
return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/fork-branch">user2/repo1:fork-branch</a>`)
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
})
|
|
|
|
// click the "sync fork" button
|
|
req = NewRequestWithValues(t, "POST", mergeUpstreamLink, map[string]string{"_csrf": GetUserCSRFToken(t, session)})
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
checkFileContent("fork-branch", "test-content-1")
|
|
|
|
// delete the "fork-branch" from the base repo
|
|
req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/delete?name=fork-branch", map[string]string{
|
|
"_csrf": GetUserCSRFToken(t, sessionBaseUser),
|
|
})
|
|
sessionBaseUser.MakeRequest(t, req, http.StatusOK)
|
|
})
|
|
|
|
t.Run("BaseChangeAfterHeadChange", func(t *testing.T) {
|
|
// update the files: base first, head later, and check the prompt
|
|
require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-2"))
|
|
require.NoError(t, createOrReplaceFileInBranch(forkUser, forkRepo, "new-file-other.txt", "fork-branch", "test-content-other"))
|
|
|
|
// make sure the base branch's update time is before the fork, to make it test the complete logic
|
|
baseBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: baseRepo.ID, Name: "master"})
|
|
forkBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: forkRepo.ID, Name: "fork-branch"})
|
|
_, err := db.GetEngine(db.DefaultContext).ID(forkBranch.ID).Update(&git_model.Branch{UpdatedUnix: baseBranch.UpdatedUnix + 1})
|
|
require.NoError(t, err)
|
|
|
|
// the repo shows a prompt to "sync fork"
|
|
require.Eventually(t, func() bool {
|
|
resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
|
|
return strings.Contains(respMsg, `The base branch <a href="/user2/repo1/src/branch/master">user2/repo1:master</a> has new changes`)
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
|
|
// and do the merge-upstream by API
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
|
|
Branch: "fork-branch",
|
|
}).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
checkFileContent("fork-branch", "test-content-2")
|
|
|
|
var mergeResp api.MergeUpstreamResponse
|
|
DecodeJSON(t, resp, &mergeResp)
|
|
assert.Equal(t, "merge", mergeResp.MergeStyle)
|
|
|
|
// after merge, there should be no "sync fork" button anymore
|
|
require.Eventually(t, func() bool {
|
|
resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
|
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
|
return queryMergeUpstreamButtonLink(htmlDoc) == ""
|
|
}, 5*time.Second, 100*time.Millisecond)
|
|
})
|
|
|
|
t.Run("FastForwardOnly", func(t *testing.T) {
|
|
// Create a clean branch for fast-forward testing
|
|
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/test-repo-fork/branches/_new/branch/master", forkUser.Name), map[string]string{
|
|
"_csrf": GetUserCSRFToken(t, session),
|
|
"new_branch_name": "ff-test-branch",
|
|
})
|
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
|
|
|
// Add content to base repository that can be fast-forwarded
|
|
require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "ff-test.txt", "master", "ff-content-1"))
|
|
|
|
// ff_only=true with fast-forward possible (should succeed)
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
|
|
Branch: "ff-test-branch",
|
|
FfOnly: true,
|
|
}).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
|
|
var mergeResp api.MergeUpstreamResponse
|
|
DecodeJSON(t, resp, &mergeResp)
|
|
assert.Equal(t, "fast-forward", mergeResp.MergeStyle)
|
|
|
|
// ff_only=true when fast-forward is not possible (should fail)
|
|
require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "another-file.txt", "master", "more-content"))
|
|
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
|
|
Branch: "fork-branch",
|
|
FfOnly: true,
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusBadRequest)
|
|
})
|
|
})
|
|
}
|