mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
189 lines
8.5 KiB
Go
189 lines
8.5 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
auth_model "github.com/hanzoai/git/models/auth"
|
|
"github.com/hanzoai/git/models/db"
|
|
git_model "github.com/hanzoai/git/models/git"
|
|
repo_model "github.com/hanzoai/git/models/repo"
|
|
"github.com/hanzoai/git/models/unittest"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
api "github.com/hanzoai/git/modules/structs"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestRepoMergeUpstream(t *testing.T) {
|
|
onGitRun(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("/v1/repos/%s/%s/forks", baseUser.Name, baseRepo.Name), &api.CreateForkOption{
|
|
Name: new("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{
|
|
"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{
|
|
"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 = NewRequest(t, "POST", mergeUpstreamLink)
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
checkFileContent("fork-branch", "test-content-1")
|
|
|
|
// delete the "fork-branch" from the base repo
|
|
req = NewRequest(t, "POST", "/user2/repo1/branches/delete?name=fork-branch")
|
|
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(t.Context()).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("/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")
|
|
|
|
mergeResp := DecodeJSON(t, resp, &api.MergeUpstreamResponse{})
|
|
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{
|
|
"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("/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)
|
|
|
|
mergeResp := DecodeJSON(t, resp, &api.MergeUpstreamResponse{})
|
|
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("/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
|
|
Branch: "fork-branch",
|
|
FfOnly: true,
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusBadRequest)
|
|
})
|
|
|
|
t.Run("BasePrivateBlocksSync", func(t *testing.T) {
|
|
// add a new commit to the base repo, then make the base repo private
|
|
require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "secret.txt", "master", "private-content"))
|
|
baseRepo.IsPrivate = true
|
|
_, err := db.GetEngine(t.Context()).ID(baseRepo.ID).Cols("is_private").Update(baseRepo)
|
|
require.NoError(t, err)
|
|
// the fork owner can no longer read the base repo, so syncing must be refused
|
|
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
|
|
Branch: "fork-branch",
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusForbidden)
|
|
})
|
|
})
|
|
}
|