From 580e21dd2e9dfb3a3f86f51c4eb188c1bbfa8b11 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 12 Nov 2024 10:38:22 +0800 Subject: Refactor LFS SSH and internal routers (#32473) Gitea instance keeps reporting a lot of errors like "LFS SSH transfer connection denied, pure SSH protocol is disabled". When starting debugging the problem, there are more problems found. Try to address most of them: * avoid unnecessary server side error logs (change `fail()` to not log them) * figure out the broken tests/user2/lfs.git (added comments) * avoid `migratePushMirrors` failure when a repository doesn't exist (ignore them) * avoid "Authorization" (internal&lfs) header conflicts, remove the tricky "swapAuth" and use "X-Gitea-Internal-Auth" * make internal token comparing constant time (it wasn't a serous problem because in a real world it's nearly impossible to timing-attack the token, but good to fix and backport) * avoid duplicate routers (introduce AddOwnerRepoGitLFSRoutes) * avoid "internal (private)" routes using session/web context (they should use private context) * fix incorrect "path" usages (use "filepath") * fix incorrect mocked route point handling (need to check func nil correctly) * split some tests from "git general tests" to "git misc tests" (to keep "git_general_test.go" simple) Still no correct result for Git LFS SSH tests. So the code is kept there (`tests/integration/git_lfs_ssh_test.go`) and a FIXME explains the details. --- tests/integration/api_repo_file_get_test.go | 2 +- tests/integration/git_general_test.go | 891 ++++++++++++++++++++++++ tests/integration/git_lfs_ssh_test.go | 61 ++ tests/integration/git_misc_test.go | 138 ++++ tests/integration/git_test.go | 1002 --------------------------- tests/test_utils.go | 11 +- 6 files changed, 1098 insertions(+), 1007 deletions(-) create mode 100644 tests/integration/git_general_test.go create mode 100644 tests/integration/git_lfs_ssh_test.go create mode 100644 tests/integration/git_misc_test.go delete mode 100644 tests/integration/git_test.go (limited to 'tests') diff --git a/tests/integration/api_repo_file_get_test.go b/tests/integration/api_repo_file_get_test.go index 4649babad1..27bc9e25bf 100644 --- a/tests/integration/api_repo_file_get_test.go +++ b/tests/integration/api_repo_file_get_test.go @@ -39,7 +39,7 @@ func TestAPIGetRawFileOrLFS(t *testing.T) { t.Run("Partial Clone", doPartialGitClone(dstPath2, u)) - lfs, _ := lfsCommitAndPushTest(t, dstPath) + lfs := lfsCommitAndPushTest(t, dstPath, littleSize)[0] reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs) respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK) diff --git a/tests/integration/git_general_test.go b/tests/integration/git_general_test.go new file mode 100644 index 0000000000..7fd19e7edd --- /dev/null +++ b/tests/integration/git_general_test.go @@ -0,0 +1,891 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strconv" + "testing" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/lfs" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + gitea_context "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +const ( + littleSize = 1024 // 1K + bigSize = 128 * 1024 * 1024 // 128M +) + +func TestGitGeneral(t *testing.T) { + onGiteaRun(t, testGitGeneral) +} + +func testGitGeneral(t *testing.T, u *url.URL) { + username := "user2" + baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + u.Path = baseAPITestContext.GitPath() + + forkedUserCtx := NewAPITestContext(t, "user4", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + t.Run("HTTP", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + ensureAnonymousClone(t, u) + httpContext := baseAPITestContext + httpContext.Reponame = "repo-tmp-17" + forkedUserCtx.Reponame = httpContext.Reponame + + dstPath := t.TempDir() + + t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false)) + t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, httpContext.Username, perm.AccessModeRead)) + + t.Run("ForkFromDifferentUser", doAPIForkRepository(httpContext, forkedUserCtx.Username)) + + u.Path = httpContext.GitPath() + u.User = url.UserPassword(username, userPassword) + + t.Run("Clone", doGitClone(dstPath, u)) + + dstPath2 := t.TempDir() + + t.Run("Partial Clone", doPartialGitClone(dstPath2, u)) + + pushedFilesStandard := standardCommitAndPushTest(t, dstPath, littleSize, bigSize) + pushedFilesLFS := lfsCommitAndPushTest(t, dstPath, littleSize, bigSize) + rawTest(t, &httpContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1]) + mediaTest(t, &httpContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1]) + + t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "test/head")) + t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath)) + t.Run("AutoMerge", doAutoPRMerge(&httpContext, dstPath)) + t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge")) + t.Run("MergeFork", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + t.Run("CreatePRAndMerge", doMergeFork(httpContext, forkedUserCtx, "master", httpContext.Username+":master")) + rawTest(t, &forkedUserCtx, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1]) + mediaTest(t, &forkedUserCtx, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1]) + }) + + t.Run("PushCreate", doPushCreate(httpContext, u)) + }) + t.Run("SSH", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + sshContext := baseAPITestContext + sshContext.Reponame = "repo-tmp-18" + keyname := "my-testing-key" + forkedUserCtx.Reponame = sshContext.Reponame + t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false)) + t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, sshContext.Username, perm.AccessModeRead)) + t.Run("ForkFromDifferentUser", doAPIForkRepository(sshContext, forkedUserCtx.Username)) + + // Setup key the user ssh key + withKeyFile(t, keyname, func(keyFile string) { + t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile)) + + // Setup remote link + // TODO: get url from api + sshURL := createSSHUrl(sshContext.GitPath(), u) + + // Setup clone folder + dstPath := t.TempDir() + + t.Run("Clone", doGitClone(dstPath, sshURL)) + + pushedFilesStandard := standardCommitAndPushTest(t, dstPath, littleSize, bigSize) + pushedFilesLFS := lfsCommitAndPushTest(t, dstPath, littleSize, bigSize) + rawTest(t, &sshContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1]) + mediaTest(t, &sshContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1]) + + t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "test/head2")) + t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath)) + t.Run("MergeFork", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + t.Run("CreatePRAndMerge", doMergeFork(sshContext, forkedUserCtx, "master", sshContext.Username+":master")) + rawTest(t, &forkedUserCtx, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1]) + mediaTest(t, &forkedUserCtx, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1]) + }) + + t.Run("PushCreate", doPushCreate(sshContext, sshURL)) + }) + }) +} + +func ensureAnonymousClone(t *testing.T, u *url.URL) { + dstLocalPath := t.TempDir() + t.Run("CloneAnonymous", doGitClone(dstLocalPath, u)) +} + +func standardCommitAndPushTest(t *testing.T, dstPath string, sizes ...int) (pushedFiles []string) { + t.Run("CommitAndPushStandard", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + pushedFiles = commitAndPushTest(t, dstPath, "data-file-", sizes...) + }) + return pushedFiles +} + +func lfsCommitAndPushTest(t *testing.T, dstPath string, sizes ...int) (pushedFiles []string) { + t.Run("CommitAndPushLFS", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + prefix := "lfs-data-file-" + err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("install").Run(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("track").AddDynamicArguments(prefix + "*").RunStdString(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + err = git.AddChanges(dstPath, false, ".gitattributes") + assert.NoError(t, err) + + err = git.CommitChangesWithArgs(dstPath, git.AllowLFSFiltersArgs(), git.CommitChangesOptions{ + Committer: &git.Signature{ + Email: "user2@example.com", + Name: "User Two", + When: time.Now(), + }, + Author: &git.Signature{ + Email: "user2@example.com", + Name: "User Two", + When: time.Now(), + }, + Message: fmt.Sprintf("Testing commit @ %v", time.Now()), + }) + assert.NoError(t, err) + + pushedFiles = commitAndPushTest(t, dstPath, prefix, sizes...) + t.Run("Locks", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + lockTest(t, dstPath) + }) + }) + return pushedFiles +} + +func commitAndPushTest(t *testing.T, dstPath, prefix string, sizes ...int) (pushedFiles []string) { + for _, size := range sizes { + t.Run("PushCommit Size-"+strconv.Itoa(size), func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + pushedFiles = append(pushedFiles, doCommitAndPush(t, size, dstPath, prefix)) + }) + } + return pushedFiles +} + +func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) { + t.Run("Raw", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + username := ctx.Username + reponame := ctx.Reponame + + session := loginUser(t, username) + + // Request raw paths + req := NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", little)) + resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, littleSize, resp.Length) + + if setting.LFS.StartServer { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS)) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.NotEqual(t, littleSize, resp.Body.Len()) + assert.LessOrEqual(t, resp.Body.Len(), 1024) + if resp.Body.Len() != littleSize && resp.Body.Len() <= 1024 { + assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier) + } + } + + if !testing.Short() { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big)) + resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Length) + + if setting.LFS.StartServer { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS)) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.NotEqual(t, bigSize, resp.Body.Len()) + if resp.Body.Len() != bigSize && resp.Body.Len() <= 1024 { + assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier) + } + } + } + }) +} + +func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) { + t.Run("Media", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + username := ctx.Username + reponame := ctx.Reponame + + session := loginUser(t, username) + + // Request media paths + req := NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", little)) + resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, littleSize, resp.Length) + + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, littleSize, resp.Length) + + if !testing.Short() { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Length) + + if setting.LFS.StartServer { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Length) + } + } + }) +} + +func lockTest(t *testing.T, repoPath string) { + lockFileTest(t, "README.md", repoPath) +} + +func lockFileTest(t *testing.T, filename, repoPath string) { + _, _, err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("locks").RunStdString(&git.RunOpts{Dir: repoPath}) + assert.NoError(t, err) + _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("lock").AddDynamicArguments(filename).RunStdString(&git.RunOpts{Dir: repoPath}) + assert.NoError(t, err) + _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("locks").RunStdString(&git.RunOpts{Dir: repoPath}) + assert.NoError(t, err) + _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("unlock").AddDynamicArguments(filename).RunStdString(&git.RunOpts{Dir: repoPath}) + assert.NoError(t, err) +} + +func doCommitAndPush(t *testing.T, size int, repoPath, prefix string) string { + name, err := generateCommitWithNewData(size, repoPath, "user2@example.com", "User Two", prefix) + assert.NoError(t, err) + _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "master").RunStdString(&git.RunOpts{Dir: repoPath}) // Push + assert.NoError(t, err) + return name +} + +func generateCommitWithNewData(size int, repoPath, email, fullName, prefix string) (string, error) { + // Generate random file + bufSize := 4 * 1024 + if bufSize > size { + bufSize = size + } + + buffer := make([]byte, bufSize) + + tmpFile, err := os.CreateTemp(repoPath, prefix) + if err != nil { + return "", err + } + defer tmpFile.Close() + written := 0 + for written < size { + n := size - written + if n > bufSize { + n = bufSize + } + _, err := rand.Read(buffer[:n]) + if err != nil { + return "", err + } + n, err = tmpFile.Write(buffer[:n]) + if err != nil { + return "", err + } + written += n + } + + // Commit + // Now here we should explicitly allow lfs filters to run + globalArgs := git.AllowLFSFiltersArgs() + err = git.AddChangesWithArgs(repoPath, globalArgs, false, filepath.Base(tmpFile.Name())) + if err != nil { + return "", err + } + err = git.CommitChangesWithArgs(repoPath, globalArgs, git.CommitChangesOptions{ + Committer: &git.Signature{ + Email: email, + Name: fullName, + When: time.Now(), + }, + Author: &git.Signature{ + Email: email, + Name: fullName, + When: time.Now(), + }, + Message: fmt.Sprintf("Testing commit @ %v", time.Now()), + }) + return filepath.Base(tmpFile.Name()), err +} + +func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { + return func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + t.Run("CreateBranchProtected", doGitCreateBranch(dstPath, "protected")) + t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected")) + + ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository) + + // Protect branch without any whitelisting + t.Run("ProtectBranchNoWhitelist", func(t *testing.T) { + doProtectBranch(ctx, "protected", "", "", "") + }) + + // Try to push without permissions, which should fail + t.Run("TryPushWithoutPermissions", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + doGitPushTestRepositoryFail(dstPath, "origin", "protected") + }) + + // Set up permissions for normal push but not force push + t.Run("SetupNormalPushPermissions", func(t *testing.T) { + doProtectBranch(ctx, "protected", baseCtx.Username, "", "") + }) + + // Normal push should work + t.Run("NormalPushWithPermissions", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + doGitPushTestRepository(dstPath, "origin", "protected") + }) + + // Try to force push without force push permissions, which should fail + t.Run("ForcePushWithoutForcePermissions", func(t *testing.T) { + t.Run("CreateDivergentHistory", func(t *testing.T) { + git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath}) + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-new") + assert.NoError(t, err) + }) + doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected") + }) + + // Set up permissions for force push but not normal push + t.Run("SetupForcePushPermissions", func(t *testing.T) { + doProtectBranch(ctx, "protected", "", baseCtx.Username, "") + }) + + // Try to force push without normal push permissions, which should fail + t.Run("ForcePushWithoutNormalPermissions", func(t *testing.T) { + doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected") + }) + + // Set up permissions for normal and force push (both are required to force push) + t.Run("SetupNormalAndForcePushPermissions", func(t *testing.T) { + doProtectBranch(ctx, "protected", baseCtx.Username, baseCtx.Username, "") + }) + + // Force push should now work + t.Run("ForcePushWithPermissions", func(t *testing.T) { + doGitPushTestRepository(dstPath, "-f", "origin", "protected") + }) + + t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", "", "")) + t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected")) + var pr api.PullRequest + var err error + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected")(t) + assert.NoError(t, err) + }) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + }) + t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected-2")) + var pr2 api.PullRequest + t.Run("CreatePullRequest", func(t *testing.T) { + pr2, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "unprotected", "unprotected-2")(t) + assert.NoError(t, err) + }) + t.Run("MergePR2", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr2.Index)) + t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + t.Run("PullProtected", doGitPull(dstPath, "origin", "protected")) + + t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "", "unprotected-file-*")) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "unprotected-file-") + assert.NoError(t, err) + }) + t.Run("PushUnprotectedFilesToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected")) + + t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, "", "")) + + t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master")) + t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce")) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + }) + t.Run("FailToForcePushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "toforce:protected")) + t.Run("MergeProtectedToToforce", doGitMerge(dstPath, "protected")) + t.Run("PushToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "toforce:protected")) + t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master")) + } +} + +func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns string) func(t *testing.T) { + // We are going to just use the owner to set the protection. + return func(t *testing.T) { + csrf := GetUserCSRFToken(t, ctx.Session) + + formData := map[string]string{ + "_csrf": csrf, + "rule_name": branch, + "unprotected_file_patterns": unprotectedFilePatterns, + } + + if userToWhitelistPush != "" { + user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistPush) + assert.NoError(t, err) + formData["whitelist_users"] = strconv.FormatInt(user.ID, 10) + formData["enable_push"] = "whitelist" + formData["enable_whitelist"] = "on" + } + + if userToWhitelistForcePush != "" { + user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistForcePush) + assert.NoError(t, err) + formData["force_push_allowlist_users"] = strconv.FormatInt(user.ID, 10) + formData["enable_force_push"] = "whitelist" + formData["enable_force_push_allowlist"] = "on" + } + + // Send the request to update branch protection settings + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), formData) + ctx.Session.MakeRequest(t, req, http.StatusSeeOther) + + // Check if master branch has been locked successfully + flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash) + assert.NotNil(t, flashCookie) + assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2522"+url.QueryEscape(branch)+"%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) + } +} + +func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) func(t *testing.T) { + return func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + var pr api.PullRequest + var err error + + // Create a test pullrequest + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t) + assert.NoError(t, err) + }) + + // Ensure the PR page works + t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) + + // Then get the diff string + var diffHash string + var diffLength int + t.Run("GetDiff", func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(baseCtx.Username), url.PathEscape(baseCtx.Reponame), pr.Index)) + resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK) + diffHash = string(resp.Hash.Sum(nil)) + diffLength = resp.Length + }) + + // Now: Merge the PR & make sure that doesn't break the PR page or change its diff + t.Run("MergePR", doAPIMergePullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) + t.Run("CheckPR", func(t *testing.T) { + oldMergeBase := pr.MergeBase + pr2, err := doAPIGetPullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) + assert.NoError(t, err) + assert.Equal(t, oldMergeBase, pr2.MergeBase) + }) + t.Run("EnsurDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength)) + + // Then: Delete the head branch & make sure that doesn't break the PR page or change its diff + t.Run("DeleteHeadBranch", doBranchDelete(baseCtx, baseCtx.Username, baseCtx.Reponame, headBranch)) + t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) + t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength)) + + // Delete the head repository & make sure that doesn't break the PR page or change its diff + t.Run("DeleteHeadRepository", doAPIDeleteRepository(ctx)) + t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) + t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength)) + } +} + +func doCreatePRAndSetManuallyMerged(ctx, baseCtx APITestContext, dstPath, baseBranch, headBranch string) func(t *testing.T) { + return func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + var ( + pr api.PullRequest + err error + lastCommitID string + ) + + trueBool := true + falseBool := false + + t.Run("AllowSetManuallyMergedAndSwitchOffAutodetectManualMerge", doAPIEditRepository(baseCtx, &api.EditRepoOption{ + HasPullRequests: &trueBool, + AllowManualMerge: &trueBool, + AutodetectManualMerge: &falseBool, + })) + + t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch)) + t.Run("PushToHeadBranch", doGitPushTestRepository(dstPath, "origin", headBranch)) + t.Run("CreateEmptyPullRequest", func(t *testing.T) { + pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t) + assert.NoError(t, err) + }) + lastCommitID = pr.Base.Sha + t.Run("ManuallyMergePR", doAPIManuallyMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, lastCommitID, pr.Index)) + } +} + +func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest) func(t *testing.T) { + return func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) + ctx.Session.MakeRequest(t, req, http.StatusOK) + req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/files", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) + ctx.Session.MakeRequest(t, req, http.StatusOK) + req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) + ctx.Session.MakeRequest(t, req, http.StatusOK) + } +} + +func doEnsureDiffNoChange(ctx APITestContext, pr api.PullRequest, diffHash string, diffLength int) func(t *testing.T) { + return func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) + resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK) + actual := string(resp.Hash.Sum(nil)) + actualLength := resp.Length + + equal := diffHash == actual + assert.True(t, equal, "Unexpected change in the diff string: expected hash: %s size: %d but was actually: %s size: %d", hex.EncodeToString([]byte(diffHash)), diffLength, hex.EncodeToString([]byte(actual)), actualLength) + } +} + +func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) { + return func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // create a context for a currently non-existent repository + ctx.Reponame = fmt.Sprintf("repo-tmp-push-create-%s", u.Scheme) + u.Path = ctx.GitPath() + + // Create a temporary directory + tmpDir := t.TempDir() + + // Now create local repository to push as our test and set its origin + t.Run("InitTestRepository", doGitInitTestRepository(tmpDir)) + t.Run("AddRemote", doGitAddRemote(tmpDir, "origin", u)) + + // Disable "Push To Create" and attempt to push + setting.Repository.EnablePushCreateUser = false + t.Run("FailToPushAndCreateTestRepository", doGitPushTestRepositoryFail(tmpDir, "origin", "master")) + + // Enable "Push To Create" + setting.Repository.EnablePushCreateUser = true + + // Assert that cloning from a non-existent repository does not create it and that it definitely wasn't create above + t.Run("FailToCloneFromNonExistentRepository", doGitCloneFail(u)) + + // Then "Push To Create"x + t.Run("SuccessfullyPushAndCreateTestRepository", doGitPushTestRepository(tmpDir, "origin", "master")) + + // Finally, fetch repo from database and ensure the correct repository has been created + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ctx.Username, ctx.Reponame) + assert.NoError(t, err) + assert.False(t, repo.IsEmpty) + assert.True(t, repo.IsPrivate) + + // Now add a remote that is invalid to "Push To Create" + invalidCtx := ctx + invalidCtx.Reponame = fmt.Sprintf("invalid/repo-tmp-push-create-%s", u.Scheme) + u.Path = invalidCtx.GitPath() + t.Run("AddInvalidRemote", doGitAddRemote(tmpDir, "invalid", u)) + + // Fail to "Push To Create" the invalid + t.Run("FailToPushAndCreateInvalidTestRepository", doGitPushTestRepositoryFail(tmpDir, "invalid", "master")) + } +} + +func doBranchDelete(ctx APITestContext, owner, repo, branch string) func(*testing.T) { + return func(t *testing.T) { + csrf := GetUserCSRFToken(t, ctx.Session) + + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/branches/delete?name=%s", url.PathEscape(owner), url.PathEscape(repo), url.QueryEscape(branch)), map[string]string{ + "_csrf": csrf, + }) + ctx.Session.MakeRequest(t, req, http.StatusOK) + } +} + +func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { + return func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository) + + t.Run("CheckoutProtected", doGitCheckoutBranch(dstPath, "protected")) + t.Run("PullProtected", doGitPull(dstPath, "origin", "protected")) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + }) + t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected3")) + var pr api.PullRequest + var err error + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected3")(t) + assert.NoError(t, err) + }) + + // Request repository commits page + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", baseCtx.Username, baseCtx.Reponame, pr.Index)) + resp := ctx.Session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + // Get first commit URL + commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href") + assert.True(t, exists) + assert.NotEmpty(t, commitURL) + + commitID := path.Base(commitURL) + + addCommitStatus := func(status api.CommitStatusState) func(*testing.T) { + return doAPICreateCommitStatus(ctx, commitID, api.CreateStatusOption{ + State: status, + TargetURL: "http://test.ci/", + Description: "", + Context: "testci", + }) + } + + // Call API to add Pending status for commit + t.Run("CreateStatus", addCommitStatus(api.CommitStatusPending)) + + // Cancel not existing auto merge + ctx.ExpectedCode = http.StatusNotFound + t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + + // Add auto merge request + ctx.ExpectedCode = http.StatusCreated + t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + + // Can not create schedule twice + ctx.ExpectedCode = http.StatusConflict + t.Run("AutoMergePRTwice", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + + // Cancel auto merge request + ctx.ExpectedCode = http.StatusNoContent + t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + + // Add auto merge request + ctx.ExpectedCode = http.StatusCreated + t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + + // Check pr status + ctx.ExpectedCode = 0 + pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) + assert.NoError(t, err) + assert.False(t, pr.HasMerged) + + // Call API to add Failure status for commit + t.Run("CreateStatus", addCommitStatus(api.CommitStatusFailure)) + + // Check pr status + pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) + assert.NoError(t, err) + assert.False(t, pr.HasMerged) + + // Call API to add Success status for commit + t.Run("CreateStatus", addCommitStatus(api.CommitStatusSuccess)) + + // wait to let gitea merge stuff + time.Sleep(time.Second) + + // test pr status + pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) + assert.NoError(t, err) + assert.True(t, pr.HasMerged) + } +} + +func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string) func(t *testing.T) { + return func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // skip this test if git version is low + if !git.DefaultFeatures().SupportProcReceive { + return + } + + gitRepo, err := git.OpenRepository(git.DefaultContext, dstPath) + if !assert.NoError(t, err) { + return + } + defer gitRepo.Close() + + var ( + pr1, pr2 *issues_model.PullRequest + commit string + ) + repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ctx.Username, ctx.Reponame) + if !assert.NoError(t, err) { + return + } + + pullNum := unittest.GetCount(t, &issues_model.PullRequest{}) + + t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch)) + + t.Run("AddCommit", func(t *testing.T) { + err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666) + if !assert.NoError(t, err) { + return + } + + 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) + commit, err = gitRepo.GetRefCommitID("HEAD") + assert.NoError(t, err) + }) + + t.Run("Push", func(t *testing.T) { + err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + headBranch).Run(&git.RunOpts{Dir: dstPath}) + if !assert.NoError(t, err) { + return + } + unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+1) + pr1 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ + HeadRepoID: repo.ID, + Flow: issues_model.PullRequestFlowAGit, + }) + if !assert.NotEmpty(t, pr1) { + return + } + prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, "user2/"+headBranch, pr1.HeadBranch) + assert.False(t, prMsg.HasMerged) + assert.Contains(t, "Testing commit 1", prMsg.Body) + assert.Equal(t, commit, prMsg.Head.Sha) + + _, _, err = git.NewCommand(git.DefaultContext, "push", "origin").AddDynamicArguments("HEAD:refs/for/master/test/" + headBranch).RunStdString(&git.RunOpts{Dir: dstPath}) + if !assert.NoError(t, err) { + return + } + unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2) + pr2 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ + HeadRepoID: repo.ID, + Index: pr1.Index + 1, + Flow: issues_model.PullRequestFlowAGit, + }) + if !assert.NotEmpty(t, pr2) { + return + } + prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, "user2/test/"+headBranch, pr2.HeadBranch) + assert.False(t, prMsg.HasMerged) + }) + + if pr1 == nil || pr2 == nil { + return + } + + t.Run("AddCommit2", func(t *testing.T) { + err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content \n ## test content 2"), 0o666) + if !assert.NoError(t, err) { + return + } + + 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 2", + }) + assert.NoError(t, err) + commit, err = gitRepo.GetRefCommitID("HEAD") + assert.NoError(t, err) + }) + + t.Run("Push2", func(t *testing.T) { + err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + headBranch).Run(&git.RunOpts{Dir: dstPath}) + if !assert.NoError(t, err) { + return + } + unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2) + prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t) + if !assert.NoError(t, err) { + return + } + assert.False(t, prMsg.HasMerged) + assert.Equal(t, commit, prMsg.Head.Sha) + + _, _, err = git.NewCommand(git.DefaultContext, "push", "origin").AddDynamicArguments("HEAD:refs/for/master/test/" + headBranch).RunStdString(&git.RunOpts{Dir: dstPath}) + if !assert.NoError(t, err) { + return + } + unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2) + prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t) + if !assert.NoError(t, err) { + return + } + assert.False(t, prMsg.HasMerged) + assert.Equal(t, commit, prMsg.Head.Sha) + }) + t.Run("Merge", doAPIMergePullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)) + t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master")) + } +} diff --git a/tests/integration/git_lfs_ssh_test.go b/tests/integration/git_lfs_ssh_test.go new file mode 100644 index 0000000000..33c2fba620 --- /dev/null +++ b/tests/integration/git_lfs_ssh_test.go @@ -0,0 +1,61 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/url" + "sync" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/private" + "code.gitea.io/gitea/services/context" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGitLFSSSH(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + dstPath := t.TempDir() + apiTestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + var mu sync.Mutex + var routerCalls []string + web.RouteMock(private.RouterMockPointInternalLFS, func(ctx *context.PrivateContext) { + mu.Lock() + routerCalls = append(routerCalls, ctx.Req.Method+" "+ctx.Req.URL.Path) + mu.Unlock() + }) + + withKeyFile(t, "my-testing-key", func(keyFile string) { + t.Run("CreateUserKey", doAPICreateUserKey(apiTestContext, "test-key", keyFile)) + cloneURL := createSSHUrl(apiTestContext.GitPath(), u) + t.Run("Clone", doGitClone(dstPath, cloneURL)) + + cfg, err := setting.CfgProvider.PrepareSaving() + require.NoError(t, err) + cfg.Section("server").Key("LFS_ALLOW_PURE_SSH").SetValue("true") + setting.LFS.AllowPureSSH = true + require.NoError(t, cfg.Save()) + + // do LFS SSH transfer? + lfsCommitAndPushTest(t, dstPath, 10) + }) + + // FIXME: Here we only see the following calls, but actually there should be calls to "PUT"? + // 0 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks" + // 1 = {string} "POST /api/internal/repo/user2/repo1.git/info/lfs/objects/batch" + // 2 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks" + // 3 = {string} "POST /api/internal/repo/user2/repo1.git/info/lfs/locks" + // 4 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks" + // 5 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks" + // 6 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks" + // 7 = {string} "POST /api/internal/repo/user2/repo1.git/info/lfs/locks/24/unlock" + assert.NotEmpty(t, routerCalls) + // assert.Contains(t, routerCalls, "PUT /api/internal/repo/user2/repo1.git/info/lfs/objects/....") + }) +} diff --git a/tests/integration/git_misc_test.go b/tests/integration/git_misc_test.go new file mode 100644 index 0000000000..82ab184bb0 --- /dev/null +++ b/tests/integration/git_misc_test.go @@ -0,0 +1,138 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "bytes" + "context" + "io" + "net/url" + "sync" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + files_service "code.gitea.io/gitea/services/repository/files" + + "github.com/stretchr/testify/assert" +) + +func TestDataAsyncDoubleRead_Issue29101(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + testContent := bytes.Repeat([]byte{'a'}, 10000) + resp, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: "test.txt", + ContentReader: bytes.NewReader(testContent), + }, + }, + OldBranch: repo.DefaultBranch, + NewBranch: repo.DefaultBranch, + }) + assert.NoError(t, err) + + sha := resp.Commit.SHA + + gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo) + assert.NoError(t, err) + + commit, err := gitRepo.GetCommit(sha) + assert.NoError(t, err) + + entry, err := commit.GetTreeEntryByPath("test.txt") + assert.NoError(t, err) + + b := entry.Blob() + r1, err := b.DataAsync() + assert.NoError(t, err) + defer r1.Close() + r2, err := b.DataAsync() + assert.NoError(t, err) + defer r2.Close() + + var data1, data2 []byte + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + data1, _ = io.ReadAll(r1) + assert.NoError(t, err) + wg.Done() + }() + go func() { + data2, _ = io.ReadAll(r2) + assert.NoError(t, err) + wg.Done() + }() + wg.Wait() + assert.Equal(t, testContent, data1) + assert.Equal(t, testContent, data2) + }) +} + +func TestAgitPullPush(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + u.Path = baseAPITestContext.GitPath() + u.User = url.UserPassword("user2", userPassword) + + dstPath := t.TempDir() + doGitClone(dstPath, u)(t) + + gitRepo, err := git.OpenRepository(context.Background(), dstPath) + assert.NoError(t, err) + defer gitRepo.Close() + + doGitCreateBranch(dstPath, "test-agit-push") + + // commit 1 + _, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + + // push to create an agit pull request + err = git.NewCommand(git.DefaultContext, "push", "origin", + "-o", "title=test-title", "-o", "description=test-description", + "HEAD:refs/for/master/test-agit-push", + ).Run(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + + // check pull request exist + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: 1, Flow: issues_model.PullRequestFlowAGit, HeadBranch: "user2/test-agit-push"}) + assert.NoError(t, pr.LoadIssue(db.DefaultContext)) + assert.Equal(t, "test-title", pr.Issue.Title) + assert.Equal(t, "test-description", pr.Issue.Content) + + // commit 2 + _, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-2-") + assert.NoError(t, err) + + // push 2 + err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").Run(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + + // reset to first commit + err = git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + + // test force push without confirm + _, stderr, err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").RunStdString(&git.RunOpts{Dir: dstPath}) + assert.Error(t, err) + assert.Contains(t, stderr, "[remote rejected] HEAD -> refs/for/master/test-agit-push (request `force-push` push option)") + + // test force push with confirm + err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push", "-o", "force-push").Run(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + }) +} diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go deleted file mode 100644 index 76db3c6932..0000000000 --- a/tests/integration/git_test.go +++ /dev/null @@ -1,1002 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package integration - -import ( - "bytes" - "context" - "crypto/rand" - "encoding/hex" - "fmt" - "net/http" - "net/url" - "os" - "path" - "path/filepath" - "strconv" - "testing" - "time" - - auth_model "code.gitea.io/gitea/models/auth" - "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/perm" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/modules/lfs" - "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" - gitea_context "code.gitea.io/gitea/services/context" - files_service "code.gitea.io/gitea/services/repository/files" - "code.gitea.io/gitea/tests" - - "github.com/stretchr/testify/assert" -) - -const ( - littleSize = 1024 // 1ko - bigSize = 128 * 1024 * 1024 // 128Mo -) - -func TestGit(t *testing.T) { - onGiteaRun(t, testGit) -} - -func testGit(t *testing.T, u *url.URL) { - username := "user2" - baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - - u.Path = baseAPITestContext.GitPath() - - forkedUserCtx := NewAPITestContext(t, "user4", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - - t.Run("HTTP", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - ensureAnonymousClone(t, u) - httpContext := baseAPITestContext - httpContext.Reponame = "repo-tmp-17" - forkedUserCtx.Reponame = httpContext.Reponame - - dstPath := t.TempDir() - - t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false)) - t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, httpContext.Username, perm.AccessModeRead)) - - t.Run("ForkFromDifferentUser", doAPIForkRepository(httpContext, forkedUserCtx.Username)) - - u.Path = httpContext.GitPath() - u.User = url.UserPassword(username, userPassword) - - t.Run("Clone", doGitClone(dstPath, u)) - - dstPath2 := t.TempDir() - - t.Run("Partial Clone", doPartialGitClone(dstPath2, u)) - - little, big := standardCommitAndPushTest(t, dstPath) - littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath) - rawTest(t, &httpContext, little, big, littleLFS, bigLFS) - mediaTest(t, &httpContext, little, big, littleLFS, bigLFS) - - t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "test/head")) - t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath)) - t.Run("AutoMerge", doAutoPRMerge(&httpContext, dstPath)) - t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge")) - t.Run("MergeFork", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - t.Run("CreatePRAndMerge", doMergeFork(httpContext, forkedUserCtx, "master", httpContext.Username+":master")) - rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS) - mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS) - }) - - t.Run("PushCreate", doPushCreate(httpContext, u)) - }) - t.Run("SSH", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - sshContext := baseAPITestContext - sshContext.Reponame = "repo-tmp-18" - keyname := "my-testing-key" - forkedUserCtx.Reponame = sshContext.Reponame - t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false)) - t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, sshContext.Username, perm.AccessModeRead)) - t.Run("ForkFromDifferentUser", doAPIForkRepository(sshContext, forkedUserCtx.Username)) - - // Setup key the user ssh key - withKeyFile(t, keyname, func(keyFile string) { - t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile)) - - // Setup remote link - // TODO: get url from api - sshURL := createSSHUrl(sshContext.GitPath(), u) - - // Setup clone folder - dstPath := t.TempDir() - - t.Run("Clone", doGitClone(dstPath, sshURL)) - - little, big := standardCommitAndPushTest(t, dstPath) - littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath) - rawTest(t, &sshContext, little, big, littleLFS, bigLFS) - mediaTest(t, &sshContext, little, big, littleLFS, bigLFS) - - t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "test/head2")) - t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath)) - t.Run("MergeFork", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - t.Run("CreatePRAndMerge", doMergeFork(sshContext, forkedUserCtx, "master", sshContext.Username+":master")) - rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS) - mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS) - }) - - t.Run("PushCreate", doPushCreate(sshContext, sshURL)) - }) - }) -} - -func ensureAnonymousClone(t *testing.T, u *url.URL) { - dstLocalPath := t.TempDir() - t.Run("CloneAnonymous", doGitClone(dstLocalPath, u)) -} - -func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string) { - t.Run("Standard", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - little, big = commitAndPushTest(t, dstPath, "data-file-") - }) - return little, big -} - -func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) { - t.Run("LFS", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - prefix := "lfs-data-file-" - err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("install").Run(&git.RunOpts{Dir: dstPath}) - assert.NoError(t, err) - _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("track").AddDynamicArguments(prefix + "*").RunStdString(&git.RunOpts{Dir: dstPath}) - assert.NoError(t, err) - err = git.AddChanges(dstPath, false, ".gitattributes") - assert.NoError(t, err) - - err = git.CommitChangesWithArgs(dstPath, git.AllowLFSFiltersArgs(), git.CommitChangesOptions{ - Committer: &git.Signature{ - Email: "user2@example.com", - Name: "User Two", - When: time.Now(), - }, - Author: &git.Signature{ - Email: "user2@example.com", - Name: "User Two", - When: time.Now(), - }, - Message: fmt.Sprintf("Testing commit @ %v", time.Now()), - }) - assert.NoError(t, err) - - littleLFS, bigLFS = commitAndPushTest(t, dstPath, prefix) - - t.Run("Locks", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - lockTest(t, dstPath) - }) - }) - return littleLFS, bigLFS -} - -func commitAndPushTest(t *testing.T, dstPath, prefix string) (little, big string) { - t.Run("PushCommit", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - t.Run("Little", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - little = doCommitAndPush(t, littleSize, dstPath, prefix) - }) - t.Run("Big", func(t *testing.T) { - if testing.Short() { - t.Skip("Skipping test in short mode.") - return - } - defer tests.PrintCurrentTest(t)() - big = doCommitAndPush(t, bigSize, dstPath, prefix) - }) - }) - return little, big -} - -func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) { - t.Run("Raw", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - username := ctx.Username - reponame := ctx.Reponame - - session := loginUser(t, username) - - // Request raw paths - req := NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", little)) - resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Length) - - if setting.LFS.StartServer { - req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS)) - resp := session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, littleSize, resp.Body.Len()) - assert.LessOrEqual(t, resp.Body.Len(), 1024) - if resp.Body.Len() != littleSize && resp.Body.Len() <= 1024 { - assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier) - } - } - - if !testing.Short() { - req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big)) - resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Length) - - if setting.LFS.StartServer { - req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS)) - resp := session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, bigSize, resp.Body.Len()) - if resp.Body.Len() != bigSize && resp.Body.Len() <= 1024 { - assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier) - } - } - } - }) -} - -func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) { - t.Run("Media", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - - username := ctx.Username - reponame := ctx.Reponame - - session := loginUser(t, username) - - // Request media paths - req := NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", little)) - resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Length) - - req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Length) - - if !testing.Short() { - req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Length) - - if setting.LFS.StartServer { - req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Length) - } - } - }) -} - -func lockTest(t *testing.T, repoPath string) { - lockFileTest(t, "README.md", repoPath) -} - -func lockFileTest(t *testing.T, filename, repoPath string) { - _, _, err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("locks").RunStdString(&git.RunOpts{Dir: repoPath}) - assert.NoError(t, err) - _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("lock").AddDynamicArguments(filename).RunStdString(&git.RunOpts{Dir: repoPath}) - assert.NoError(t, err) - _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("locks").RunStdString(&git.RunOpts{Dir: repoPath}) - assert.NoError(t, err) - _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("unlock").AddDynamicArguments(filename).RunStdString(&git.RunOpts{Dir: repoPath}) - assert.NoError(t, err) -} - -func doCommitAndPush(t *testing.T, size int, repoPath, prefix string) string { - name, err := generateCommitWithNewData(size, repoPath, "user2@example.com", "User Two", prefix) - assert.NoError(t, err) - _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "master").RunStdString(&git.RunOpts{Dir: repoPath}) // Push - assert.NoError(t, err) - return name -} - -func generateCommitWithNewData(size int, repoPath, email, fullName, prefix string) (string, error) { - // Generate random file - bufSize := 4 * 1024 - if bufSize > size { - bufSize = size - } - - buffer := make([]byte, bufSize) - - tmpFile, err := os.CreateTemp(repoPath, prefix) - if err != nil { - return "", err - } - defer tmpFile.Close() - written := 0 - for written < size { - n := size - written - if n > bufSize { - n = bufSize - } - _, err := rand.Read(buffer[:n]) - if err != nil { - return "", err - } - n, err = tmpFile.Write(buffer[:n]) - if err != nil { - return "", err - } - written += n - } - - // Commit - // Now here we should explicitly allow lfs filters to run - globalArgs := git.AllowLFSFiltersArgs() - err = git.AddChangesWithArgs(repoPath, globalArgs, false, filepath.Base(tmpFile.Name())) - if err != nil { - return "", err - } - err = git.CommitChangesWithArgs(repoPath, globalArgs, git.CommitChangesOptions{ - Committer: &git.Signature{ - Email: email, - Name: fullName, - When: time.Now(), - }, - Author: &git.Signature{ - Email: email, - Name: fullName, - When: time.Now(), - }, - Message: fmt.Sprintf("Testing commit @ %v", time.Now()), - }) - return filepath.Base(tmpFile.Name()), err -} - -func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { - return func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - t.Run("CreateBranchProtected", doGitCreateBranch(dstPath, "protected")) - t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected")) - - ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository) - - // Protect branch without any whitelisting - t.Run("ProtectBranchNoWhitelist", func(t *testing.T) { - doProtectBranch(ctx, "protected", "", "", "") - }) - - // Try to push without permissions, which should fail - t.Run("TryPushWithoutPermissions", func(t *testing.T) { - _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") - assert.NoError(t, err) - doGitPushTestRepositoryFail(dstPath, "origin", "protected") - }) - - // Set up permissions for normal push but not force push - t.Run("SetupNormalPushPermissions", func(t *testing.T) { - doProtectBranch(ctx, "protected", baseCtx.Username, "", "") - }) - - // Normal push should work - t.Run("NormalPushWithPermissions", func(t *testing.T) { - _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") - assert.NoError(t, err) - doGitPushTestRepository(dstPath, "origin", "protected") - }) - - // Try to force push without force push permissions, which should fail - t.Run("ForcePushWithoutForcePermissions", func(t *testing.T) { - t.Run("CreateDivergentHistory", func(t *testing.T) { - git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath}) - _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-new") - assert.NoError(t, err) - }) - doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected") - }) - - // Set up permissions for force push but not normal push - t.Run("SetupForcePushPermissions", func(t *testing.T) { - doProtectBranch(ctx, "protected", "", baseCtx.Username, "") - }) - - // Try to force push without normal push permissions, which should fail - t.Run("ForcePushWithoutNormalPermissions", func(t *testing.T) { - doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected") - }) - - // Set up permissions for normal and force push (both are required to force push) - t.Run("SetupNormalAndForcePushPermissions", func(t *testing.T) { - doProtectBranch(ctx, "protected", baseCtx.Username, baseCtx.Username, "") - }) - - // Force push should now work - t.Run("ForcePushWithPermissions", func(t *testing.T) { - doGitPushTestRepository(dstPath, "-f", "origin", "protected") - }) - - t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", "", "")) - t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected")) - var pr api.PullRequest - var err error - t.Run("CreatePullRequest", func(t *testing.T) { - pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected")(t) - assert.NoError(t, err) - }) - t.Run("GenerateCommit", func(t *testing.T) { - _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") - assert.NoError(t, err) - }) - t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected-2")) - var pr2 api.PullRequest - t.Run("CreatePullRequest", func(t *testing.T) { - pr2, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "unprotected", "unprotected-2")(t) - assert.NoError(t, err) - }) - t.Run("MergePR2", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr2.Index)) - t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) - t.Run("PullProtected", doGitPull(dstPath, "origin", "protected")) - - t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "", "unprotected-file-*")) - t.Run("GenerateCommit", func(t *testing.T) { - _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "unprotected-file-") - assert.NoError(t, err) - }) - t.Run("PushUnprotectedFilesToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected")) - - t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, "", "")) - - t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master")) - t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce")) - t.Run("GenerateCommit", func(t *testing.T) { - _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") - assert.NoError(t, err) - }) - t.Run("FailToForcePushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "toforce:protected")) - t.Run("MergeProtectedToToforce", doGitMerge(dstPath, "protected")) - t.Run("PushToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "toforce:protected")) - t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master")) - } -} - -func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns string) func(t *testing.T) { - // We are going to just use the owner to set the protection. - return func(t *testing.T) { - csrf := GetUserCSRFToken(t, ctx.Session) - - formData := map[string]string{ - "_csrf": csrf, - "rule_name": branch, - "unprotected_file_patterns": unprotectedFilePatterns, - } - - if userToWhitelistPush != "" { - user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistPush) - assert.NoError(t, err) - formData["whitelist_users"] = strconv.FormatInt(user.ID, 10) - formData["enable_push"] = "whitelist" - formData["enable_whitelist"] = "on" - } - - if userToWhitelistForcePush != "" { - user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistForcePush) - assert.NoError(t, err) - formData["force_push_allowlist_users"] = strconv.FormatInt(user.ID, 10) - formData["enable_force_push"] = "whitelist" - formData["enable_force_push_allowlist"] = "on" - } - - // Send the request to update branch protection settings - req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), formData) - ctx.Session.MakeRequest(t, req, http.StatusSeeOther) - - // Check if master branch has been locked successfully - flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash) - assert.NotNil(t, flashCookie) - assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2522"+url.QueryEscape(branch)+"%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) - } -} - -func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) func(t *testing.T) { - return func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - var pr api.PullRequest - var err error - - // Create a test pullrequest - t.Run("CreatePullRequest", func(t *testing.T) { - pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t) - assert.NoError(t, err) - }) - - // Ensure the PR page works - t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) - - // Then get the diff string - var diffHash string - var diffLength int - t.Run("GetDiff", func(t *testing.T) { - req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(baseCtx.Username), url.PathEscape(baseCtx.Reponame), pr.Index)) - resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK) - diffHash = string(resp.Hash.Sum(nil)) - diffLength = resp.Length - }) - - // Now: Merge the PR & make sure that doesn't break the PR page or change its diff - t.Run("MergePR", doAPIMergePullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)) - t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) - t.Run("CheckPR", func(t *testing.T) { - oldMergeBase := pr.MergeBase - pr2, err := doAPIGetPullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) - assert.NoError(t, err) - assert.Equal(t, oldMergeBase, pr2.MergeBase) - }) - t.Run("EnsurDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength)) - - // Then: Delete the head branch & make sure that doesn't break the PR page or change its diff - t.Run("DeleteHeadBranch", doBranchDelete(baseCtx, baseCtx.Username, baseCtx.Reponame, headBranch)) - t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) - t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength)) - - // Delete the head repository & make sure that doesn't break the PR page or change its diff - t.Run("DeleteHeadRepository", doAPIDeleteRepository(ctx)) - t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr)) - t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength)) - } -} - -func doCreatePRAndSetManuallyMerged(ctx, baseCtx APITestContext, dstPath, baseBranch, headBranch string) func(t *testing.T) { - return func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - var ( - pr api.PullRequest - err error - lastCommitID string - ) - - trueBool := true - falseBool := false - - t.Run("AllowSetManuallyMergedAndSwitchOffAutodetectManualMerge", doAPIEditRepository(baseCtx, &api.EditRepoOption{ - HasPullRequests: &trueBool, - AllowManualMerge: &trueBool, - AutodetectManualMerge: &falseBool, - })) - - t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch)) - t.Run("PushToHeadBranch", doGitPushTestRepository(dstPath, "origin", headBranch)) - t.Run("CreateEmptyPullRequest", func(t *testing.T) { - pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t) - assert.NoError(t, err) - }) - lastCommitID = pr.Base.Sha - t.Run("ManuallyMergePR", doAPIManuallyMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, lastCommitID, pr.Index)) - } -} - -func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest) func(t *testing.T) { - return func(t *testing.T) { - req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) - ctx.Session.MakeRequest(t, req, http.StatusOK) - req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/files", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) - ctx.Session.MakeRequest(t, req, http.StatusOK) - req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) - ctx.Session.MakeRequest(t, req, http.StatusOK) - } -} - -func doEnsureDiffNoChange(ctx APITestContext, pr api.PullRequest, diffHash string, diffLength int) func(t *testing.T) { - return func(t *testing.T) { - req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index)) - resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK) - actual := string(resp.Hash.Sum(nil)) - actualLength := resp.Length - - equal := diffHash == actual - assert.True(t, equal, "Unexpected change in the diff string: expected hash: %s size: %d but was actually: %s size: %d", hex.EncodeToString([]byte(diffHash)), diffLength, hex.EncodeToString([]byte(actual)), actualLength) - } -} - -func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) { - return func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - - // create a context for a currently non-existent repository - ctx.Reponame = fmt.Sprintf("repo-tmp-push-create-%s", u.Scheme) - u.Path = ctx.GitPath() - - // Create a temporary directory - tmpDir := t.TempDir() - - // Now create local repository to push as our test and set its origin - t.Run("InitTestRepository", doGitInitTestRepository(tmpDir)) - t.Run("AddRemote", doGitAddRemote(tmpDir, "origin", u)) - - // Disable "Push To Create" and attempt to push - setting.Repository.EnablePushCreateUser = false - t.Run("FailToPushAndCreateTestRepository", doGitPushTestRepositoryFail(tmpDir, "origin", "master")) - - // Enable "Push To Create" - setting.Repository.EnablePushCreateUser = true - - // Assert that cloning from a non-existent repository does not create it and that it definitely wasn't create above - t.Run("FailToCloneFromNonExistentRepository", doGitCloneFail(u)) - - // Then "Push To Create"x - t.Run("SuccessfullyPushAndCreateTestRepository", doGitPushTestRepository(tmpDir, "origin", "master")) - - // Finally, fetch repo from database and ensure the correct repository has been created - repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ctx.Username, ctx.Reponame) - assert.NoError(t, err) - assert.False(t, repo.IsEmpty) - assert.True(t, repo.IsPrivate) - - // Now add a remote that is invalid to "Push To Create" - invalidCtx := ctx - invalidCtx.Reponame = fmt.Sprintf("invalid/repo-tmp-push-create-%s", u.Scheme) - u.Path = invalidCtx.GitPath() - t.Run("AddInvalidRemote", doGitAddRemote(tmpDir, "invalid", u)) - - // Fail to "Push To Create" the invalid - t.Run("FailToPushAndCreateInvalidTestRepository", doGitPushTestRepositoryFail(tmpDir, "invalid", "master")) - } -} - -func doBranchDelete(ctx APITestContext, owner, repo, branch string) func(*testing.T) { - return func(t *testing.T) { - csrf := GetUserCSRFToken(t, ctx.Session) - - req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/branches/delete?name=%s", url.PathEscape(owner), url.PathEscape(repo), url.QueryEscape(branch)), map[string]string{ - "_csrf": csrf, - }) - ctx.Session.MakeRequest(t, req, http.StatusOK) - } -} - -func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { - return func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - - ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository) - - t.Run("CheckoutProtected", doGitCheckoutBranch(dstPath, "protected")) - t.Run("PullProtected", doGitPull(dstPath, "origin", "protected")) - t.Run("GenerateCommit", func(t *testing.T) { - _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") - assert.NoError(t, err) - }) - t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected3")) - var pr api.PullRequest - var err error - t.Run("CreatePullRequest", func(t *testing.T) { - pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected3")(t) - assert.NoError(t, err) - }) - - // Request repository commits page - req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", baseCtx.Username, baseCtx.Reponame, pr.Index)) - resp := ctx.Session.MakeRequest(t, req, http.StatusOK) - doc := NewHTMLParser(t, resp.Body) - - // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href") - assert.True(t, exists) - assert.NotEmpty(t, commitURL) - - commitID := path.Base(commitURL) - - addCommitStatus := func(status api.CommitStatusState) func(*testing.T) { - return doAPICreateCommitStatus(ctx, commitID, api.CreateStatusOption{ - State: status, - TargetURL: "http://test.ci/", - Description: "", - Context: "testci", - }) - } - - // Call API to add Pending status for commit - t.Run("CreateStatus", addCommitStatus(api.CommitStatusPending)) - - // Cancel not existing auto merge - ctx.ExpectedCode = http.StatusNotFound - t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) - - // Add auto merge request - ctx.ExpectedCode = http.StatusCreated - t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) - - // Can not create schedule twice - ctx.ExpectedCode = http.StatusConflict - t.Run("AutoMergePRTwice", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) - - // Cancel auto merge request - ctx.ExpectedCode = http.StatusNoContent - t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) - - // Add auto merge request - ctx.ExpectedCode = http.StatusCreated - t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) - - // Check pr status - ctx.ExpectedCode = 0 - pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) - assert.NoError(t, err) - assert.False(t, pr.HasMerged) - - // Call API to add Failure status for commit - t.Run("CreateStatus", addCommitStatus(api.CommitStatusFailure)) - - // Check pr status - pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) - assert.NoError(t, err) - assert.False(t, pr.HasMerged) - - // Call API to add Success status for commit - t.Run("CreateStatus", addCommitStatus(api.CommitStatusSuccess)) - - // wait to let gitea merge stuff - time.Sleep(time.Second) - - // test pr status - pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) - assert.NoError(t, err) - assert.True(t, pr.HasMerged) - } -} - -func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string) func(t *testing.T) { - return func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - - // skip this test if git version is low - if !git.DefaultFeatures().SupportProcReceive { - return - } - - gitRepo, err := git.OpenRepository(git.DefaultContext, dstPath) - if !assert.NoError(t, err) { - return - } - defer gitRepo.Close() - - var ( - pr1, pr2 *issues_model.PullRequest - commit string - ) - repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ctx.Username, ctx.Reponame) - if !assert.NoError(t, err) { - return - } - - pullNum := unittest.GetCount(t, &issues_model.PullRequest{}) - - t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch)) - - t.Run("AddCommit", func(t *testing.T) { - err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666) - if !assert.NoError(t, err) { - return - } - - 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) - commit, err = gitRepo.GetRefCommitID("HEAD") - assert.NoError(t, err) - }) - - t.Run("Push", func(t *testing.T) { - err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + headBranch).Run(&git.RunOpts{Dir: dstPath}) - if !assert.NoError(t, err) { - return - } - unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+1) - pr1 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ - HeadRepoID: repo.ID, - Flow: issues_model.PullRequestFlowAGit, - }) - if !assert.NotEmpty(t, pr1) { - return - } - prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, "user2/"+headBranch, pr1.HeadBranch) - assert.False(t, prMsg.HasMerged) - assert.Contains(t, "Testing commit 1", prMsg.Body) - assert.Equal(t, commit, prMsg.Head.Sha) - - _, _, err = git.NewCommand(git.DefaultContext, "push", "origin").AddDynamicArguments("HEAD:refs/for/master/test/" + headBranch).RunStdString(&git.RunOpts{Dir: dstPath}) - if !assert.NoError(t, err) { - return - } - unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2) - pr2 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ - HeadRepoID: repo.ID, - Index: pr1.Index + 1, - Flow: issues_model.PullRequestFlowAGit, - }) - if !assert.NotEmpty(t, pr2) { - return - } - prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, "user2/test/"+headBranch, pr2.HeadBranch) - assert.False(t, prMsg.HasMerged) - }) - - if pr1 == nil || pr2 == nil { - return - } - - t.Run("AddCommit2", func(t *testing.T) { - err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content \n ## test content 2"), 0o666) - if !assert.NoError(t, err) { - return - } - - 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 2", - }) - assert.NoError(t, err) - commit, err = gitRepo.GetRefCommitID("HEAD") - assert.NoError(t, err) - }) - - t.Run("Push2", func(t *testing.T) { - err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + headBranch).Run(&git.RunOpts{Dir: dstPath}) - if !assert.NoError(t, err) { - return - } - unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2) - prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t) - if !assert.NoError(t, err) { - return - } - assert.False(t, prMsg.HasMerged) - assert.Equal(t, commit, prMsg.Head.Sha) - - _, _, err = git.NewCommand(git.DefaultContext, "push", "origin").AddDynamicArguments("HEAD:refs/for/master/test/" + headBranch).RunStdString(&git.RunOpts{Dir: dstPath}) - if !assert.NoError(t, err) { - return - } - unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2) - prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t) - if !assert.NoError(t, err) { - return - } - assert.False(t, prMsg.HasMerged) - assert.Equal(t, commit, prMsg.Head.Sha) - }) - t.Run("Merge", doAPIMergePullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)) - t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master")) - } -} - -func TestDataAsync_Issue29101(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - - resp, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "create", - TreePath: "test.txt", - ContentReader: bytes.NewReader(make([]byte, 10000)), - }, - }, - OldBranch: repo.DefaultBranch, - NewBranch: repo.DefaultBranch, - }) - assert.NoError(t, err) - - sha := resp.Commit.SHA - - gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo) - assert.NoError(t, err) - - commit, err := gitRepo.GetCommit(sha) - assert.NoError(t, err) - - entry, err := commit.GetTreeEntryByPath("test.txt") - assert.NoError(t, err) - - b := entry.Blob() - - r, err := b.DataAsync() - assert.NoError(t, err) - defer r.Close() - - r2, err := b.DataAsync() - assert.NoError(t, err) - defer r2.Close() - }) -} - -func TestAgitPullPush(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - - u.Path = baseAPITestContext.GitPath() - u.User = url.UserPassword("user2", userPassword) - - dstPath := t.TempDir() - doGitClone(dstPath, u)(t) - - gitRepo, err := git.OpenRepository(context.Background(), dstPath) - assert.NoError(t, err) - defer gitRepo.Close() - - doGitCreateBranch(dstPath, "test-agit-push") - - // commit 1 - _, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") - assert.NoError(t, err) - - // push to create an agit pull request - err = git.NewCommand(git.DefaultContext, "push", "origin", - "-o", "title=test-title", "-o", "description=test-description", - "HEAD:refs/for/master/test-agit-push", - ).Run(&git.RunOpts{Dir: dstPath}) - assert.NoError(t, err) - - // check pull request exist - pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: 1, Flow: issues_model.PullRequestFlowAGit, HeadBranch: "user2/test-agit-push"}) - assert.NoError(t, pr.LoadIssue(db.DefaultContext)) - assert.Equal(t, "test-title", pr.Issue.Title) - assert.Equal(t, "test-description", pr.Issue.Content) - - // commit 2 - _, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-2-") - assert.NoError(t, err) - - // push 2 - err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").Run(&git.RunOpts{Dir: dstPath}) - assert.NoError(t, err) - - // reset to first commit - err = git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath}) - assert.NoError(t, err) - - // test force push without confirm - _, stderr, err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").RunStdString(&git.RunOpts{Dir: dstPath}) - assert.Error(t, err) - assert.Contains(t, stderr, "[remote rejected] HEAD -> refs/for/master/test-agit-push (request `force-push` push option)") - - // test force push with confirm - err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push", "-o", "force-push").Run(&git.RunOpts{Dir: dstPath}) - assert.NoError(t, err) - }) -} diff --git a/tests/test_utils.go b/tests/test_utils.go index e6ce3cce0e..3503ca1975 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -9,7 +9,6 @@ import ( "database/sql" "fmt" "os" - "path" "path/filepath" "testing" @@ -53,7 +52,7 @@ func InitTest(requireGitea bool) { if setting.IsWindows { giteaBinary += ".exe" } - setting.AppPath = path.Join(giteaRoot, giteaBinary) + setting.AppPath = filepath.Join(giteaRoot, giteaBinary) if _, err := os.Stat(setting.AppPath); err != nil { exitf("Could not find gitea binary at %s", setting.AppPath) } @@ -70,7 +69,7 @@ func InitTest(requireGitea bool) { exitf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify`) } } - if !path.IsAbs(giteaConf) { + if !filepath.IsAbs(giteaConf) { setting.CustomConf = filepath.Join(giteaRoot, giteaConf) } else { setting.CustomConf = giteaConf @@ -193,8 +192,12 @@ func PrepareAttachmentsStorage(t testing.TB) { } func PrepareGitRepoDirectory(t testing.TB) { + if !assert.NotEmpty(t, setting.RepoRootPath) { + return + } + assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) - assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath)) + assert.NoError(t, unittest.CopyDir(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath)) ownerDirs, err := os.ReadDir(setting.RepoRootPath) if err != nil { -- cgit v1.2.3