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.
if !setting.IsProd {
_, _ = fmt.Fprintln(os.Stderr, "Gitea:", logMsg)
}
- if userMessage != "" {
- if unicode.IsPunct(rune(userMessage[len(userMessage)-1])) {
- logMsg = userMessage + " " + logMsg
- } else {
- logMsg = userMessage + ". " + logMsg
- }
+ if unicode.IsPunct(rune(userMessage[len(userMessage)-1])) {
+ logMsg = userMessage + " " + logMsg
+ } else {
+ logMsg = userMessage + ". " + logMsg
}
_ = private.SSHLog(ctx, true, logMsg)
}
if allowedCommands.Contains(verb) {
if allowedCommandsLfs.Contains(verb) {
if !setting.LFS.StartServer {
- return fail(ctx, "Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled")
+ return fail(ctx, "LFS Server is not enabled", "")
}
if verb == verbLfsTransfer && !setting.LFS.AllowPureSSH {
- return fail(ctx, "Unknown git command", "LFS SSH transfer connection denied, pure SSH protocol is disabled")
+ return fail(ctx, "LFS SSH transfer is not enabled", "")
}
if len(words) > 2 {
lfsVerb = words[2]
# These are the LFS objects in user2/lfs.git
+# user2/lfs is an INVALID repository
+#
+# commit e9c32647bab825977942598c0efa415de300304b (HEAD -> master)
+# Author: Rowan Bohde <rowan.bohde@gmail.com>
+# Date: Thu Aug 1 14:38:23 2024 -0500
+#
+# add invalid lfs file
-
id: 1
id: 2
oid: 2eccdb43825d2a49d99d542daa20075cff1d97d9d2349a8977efe9c03661737c
- size: 107
+ size: 107 # real size is 2048
repository_id: 54
created_unix: 1671607299
size: 25
repository_id: 54
created_unix: 1671607299
+
+# this file is missing
+# -
+#
+# id: 5
+# oid: 9d178b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351
+# size: 25
+# repository_id: 54
+# created_unix: 1671607299
"code.gitea.io/gitea/modules/git"
giturl "code.gitea.io/gitea/modules/git/url"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
"xorm.io/xorm"
)
func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git")
-
+ if exist, _ := util.IsExist(repoPath); !exist {
+ return "", nil
+ }
remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName)
if err != nil {
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)
}
// ReadBatchLine reads the header line from cat-file --batch
-// We expect:
-// <sha> SP <type> SP <size> LF
-// sha is a hex encoded here
+// We expect: <oid> SP <type> SP <size> LF
+// then leaving the rest of the stream "<contents> LF" to be read
func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) {
typ, err = rd.ReadString('\n')
if err != nil {
// GiteaBackend is an adapter between git-lfs-transfer library and Gitea's internal LFS API
type GiteaBackend struct {
- ctx context.Context
- server *url.URL
- op string
- token string
- itoken string
- logger transfer.Logger
+ ctx context.Context
+ server *url.URL
+ op string
+ authToken string
+ internalAuth string
+ logger transfer.Logger
}
func New(ctx context.Context, repo, op, token string, logger transfer.Logger) (transfer.Backend, error) {
return nil, err
}
server = server.JoinPath("api/internal/repo", repo, "info/lfs")
- return &GiteaBackend{ctx: ctx, server: server, op: op, token: token, itoken: fmt.Sprintf("Bearer %s", setting.InternalToken), logger: logger}, nil
+ return &GiteaBackend{ctx: ctx, server: server, op: op, authToken: token, internalAuth: fmt.Sprintf("Bearer %s", setting.InternalToken), logger: logger}, nil
}
// Batch implements transfer.Backend
}
url := g.server.JoinPath("objects/batch").String()
headers := map[string]string{
- headerAuthorisation: g.itoken,
- headerAuthX: g.token,
- headerAccept: mimeGitLFS,
- headerContentType: mimeGitLFS,
+ headerAuthorization: g.authToken,
+ headerGiteaInternalAuth: g.internalAuth,
+ headerAccept: mimeGitLFS,
+ headerContentType: mimeGitLFS,
}
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
resp, err := req.Response()
}
idMapStr := base64.StdEncoding.EncodeToString(idMapBytes)
item.Args[argID] = idMapStr
- if authHeader, ok := action.Header[headerAuthorisation]; ok {
+ if authHeader, ok := action.Header[headerAuthorization]; ok {
authHeaderB64 := base64.StdEncoding.EncodeToString([]byte(authHeader))
item.Args[argToken] = authHeaderB64
}
}
idMapStr := base64.StdEncoding.EncodeToString(idMapBytes)
item.Args[argID] = idMapStr
- if authHeader, ok := action.Header[headerAuthorisation]; ok {
+ if authHeader, ok := action.Header[headerAuthorization]; ok {
authHeaderB64 := base64.StdEncoding.EncodeToString([]byte(authHeader))
item.Args[argToken] = authHeaderB64
}
}
url := action.Href
headers := map[string]string{
- headerAuthorisation: g.itoken,
- headerAuthX: g.token,
- headerAccept: mimeOctetStream,
+ headerAuthorization: g.authToken,
+ headerGiteaInternalAuth: g.internalAuth,
+ headerAccept: mimeOctetStream,
}
req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
resp, err := req.Response()
}
url := action.Href
headers := map[string]string{
- headerAuthorisation: g.itoken,
- headerAuthX: g.token,
- headerContentType: mimeOctetStream,
- headerContentLength: strconv.FormatInt(size, 10),
+ headerAuthorization: g.authToken,
+ headerGiteaInternalAuth: g.internalAuth,
+ headerContentType: mimeOctetStream,
+ headerContentLength: strconv.FormatInt(size, 10),
}
reqBytes, err := io.ReadAll(r)
if err != nil {
}
url := action.Href
headers := map[string]string{
- headerAuthorisation: g.itoken,
- headerAuthX: g.token,
- headerAccept: mimeGitLFS,
- headerContentType: mimeGitLFS,
+ headerAuthorization: g.authToken,
+ headerGiteaInternalAuth: g.internalAuth,
+ headerAccept: mimeGitLFS,
+ headerContentType: mimeGitLFS,
}
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
resp, err := req.Response()
var _ transfer.LockBackend = &giteaLockBackend{}
type giteaLockBackend struct {
- ctx context.Context
- g *GiteaBackend
- server *url.URL
- token string
- itoken string
- logger transfer.Logger
+ ctx context.Context
+ g *GiteaBackend
+ server *url.URL
+ authToken string
+ internalAuth string
+ logger transfer.Logger
}
func newGiteaLockBackend(g *GiteaBackend) transfer.LockBackend {
server := g.server.JoinPath("locks")
- return &giteaLockBackend{ctx: g.ctx, g: g, server: server, token: g.token, itoken: g.itoken, logger: g.logger}
+ return &giteaLockBackend{ctx: g.ctx, g: g, server: server, authToken: g.authToken, internalAuth: g.internalAuth, logger: g.logger}
}
// Create implements transfer.LockBackend
}
url := g.server.String()
headers := map[string]string{
- headerAuthorisation: g.itoken,
- headerAuthX: g.token,
- headerAccept: mimeGitLFS,
- headerContentType: mimeGitLFS,
+ headerAuthorization: g.authToken,
+ headerGiteaInternalAuth: g.internalAuth,
+ headerAccept: mimeGitLFS,
+ headerContentType: mimeGitLFS,
}
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
resp, err := req.Response()
}
url := g.server.JoinPath(lock.ID(), "unlock").String()
headers := map[string]string{
- headerAuthorisation: g.itoken,
- headerAuthX: g.token,
- headerAccept: mimeGitLFS,
- headerContentType: mimeGitLFS,
+ headerAuthorization: g.authToken,
+ headerGiteaInternalAuth: g.internalAuth,
+ headerAccept: mimeGitLFS,
+ headerContentType: mimeGitLFS,
}
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
resp, err := req.Response()
urlq.RawQuery = v.Encode()
url := urlq.String()
headers := map[string]string{
- headerAuthorisation: g.itoken,
- headerAuthX: g.token,
- headerAccept: mimeGitLFS,
- headerContentType: mimeGitLFS,
+ headerAuthorization: g.authToken,
+ headerGiteaInternalAuth: g.internalAuth,
+ headerAccept: mimeGitLFS,
+ headerContentType: mimeGitLFS,
}
req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
resp, err := req.Response()
// HTTP headers
const (
- headerAccept = "Accept"
- headerAuthorisation = "Authorization"
- headerAuthX = "X-Auth"
- headerContentType = "Content-Type"
- headerContentLength = "Content-Length"
+ headerAccept = "Accept"
+ headerAuthorization = "Authorization"
+ headerGiteaInternalAuth = "X-Gitea-Internal-Auth"
+ headerContentType = "Content-Type"
+ headerContentLength = "Content-Length"
)
// MIME types
req := httplib.NewRequest(url, method).
SetContext(ctx).
Header("X-Real-IP", getClientIP()).
- Header("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken)).
+ Header("X-Gitea-Internal-Auth", fmt.Sprintf("Bearer %s", setting.InternalToken)).
SetTLSClientConfig(&tls.Config{
InsecureSkipVerify: true,
ServerName: setting.Domain,
import (
"net/http"
"net/url"
+ "reflect"
"strings"
"code.gitea.io/gitea/modules/setting"
return strings.TrimSuffix(newPattern, "/")
}
+func isNilOrFuncNil(v any) bool {
+ if v == nil {
+ return true
+ }
+ r := reflect.ValueOf(v)
+ return r.Kind() == reflect.Func && r.IsNil()
+}
+
func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) {
handlerProviders := make([]func(http.Handler) http.Handler, 0, len(r.curMiddlewares)+len(h)+1)
for _, m := range r.curMiddlewares {
- if m != nil {
+ if !isNilOrFuncNil(m) {
handlerProviders = append(handlerProviders, toHandlerProvider(m))
}
}
for _, m := range h {
- if h != nil {
+ if !isNilOrFuncNil(m) {
handlerProviders = append(handlerProviders, toHandlerProvider(m))
}
}
--- /dev/null
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package common
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/lfs"
+)
+
+func AddOwnerRepoGitLFSRoutes(m *web.Router, middlewares ...any) {
+ // shared by web and internal routers
+ m.Group("/{username}/{reponame}/info/lfs", func() {
+ m.Post("/objects/batch", lfs.CheckAcceptMediaType, lfs.BatchHandler)
+ m.Put("/objects/{oid}/{size}", lfs.UploadHandler)
+ m.Get("/objects/{oid}/{filename}", lfs.DownloadHandler)
+ m.Get("/objects/{oid}", lfs.DownloadHandler)
+ m.Post("/verify", lfs.CheckAcceptMediaType, lfs.VerifyHandler)
+ m.Group("/locks", func() {
+ m.Get("/", lfs.GetListLockHandler)
+ m.Post("/", lfs.PostLockHandler)
+ m.Post("/verify", lfs.VerifyLockHandler)
+ m.Post("/{lid}/unlock", lfs.UnLockHandler)
+ }, lfs.CheckAcceptMediaType)
+ m.Any("/*", http.NotFound)
+ }, middlewares...)
+}
package private
import (
+ "crypto/subtle"
"net/http"
"strings"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/lfs"
"gitea.com/go-chi/binding"
chi_middleware "github.com/go-chi/chi/v5/middleware"
)
-// CheckInternalToken check internal token is set
-func CheckInternalToken(next http.Handler) http.Handler {
+const RouterMockPointInternalLFS = "internal-lfs"
+
+func authInternal(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- tokens := req.Header.Get("Authorization")
- fields := strings.SplitN(tokens, " ", 2)
if setting.InternalToken == "" {
log.Warn(`The INTERNAL_TOKEN setting is missing from the configuration file: %q, internal API can't work.`, setting.CustomConf)
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
- if len(fields) != 2 || fields[0] != "Bearer" || fields[1] != setting.InternalToken {
+
+ tokens := req.Header.Get("X-Gitea-Internal-Auth") // TODO: use something like JWT or HMAC to avoid passing the token in the clear
+ after, found := strings.CutPrefix(tokens, "Bearer ")
+ authSucceeded := found && subtle.ConstantTimeCompare([]byte(after), []byte(setting.InternalToken)) == 1
+ if !authSucceeded {
log.Debug("Forbidden attempt to access internal url: Authorization header: %s", tokens)
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
- } else {
- next.ServeHTTP(w, req)
+ return
}
+ next.ServeHTTP(w, req)
})
}
}
}
-// SwapAuthToken swaps Authorization header with X-Auth header
-func swapAuthToken(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- req.Header.Set("Authorization", req.Header.Get("X-Auth"))
- next.ServeHTTP(w, req)
- })
-}
-
// Routes registers all internal APIs routes to web application.
// These APIs will be invoked by internal commands for example `gitea serv` and etc.
func Routes() *web.Router {
r := web.NewRouter()
r.Use(context.PrivateContexter())
- r.Use(CheckInternalToken)
+ r.Use(authInternal)
// Log the real ip address of the request from SSH is really helpful for diagnosing sometimes.
// Since internal API will be sent only from Gitea sub commands and it's under control (checked by InternalToken), we can trust the headers.
r.Use(chi_middleware.RealIP)
r.Post("/restore_repo", RestoreRepo)
r.Post("/actions/generate_actions_runner_token", GenerateActionsRunnerToken)
- r.Group("/repo/{username}/{reponame}", func() {
- r.Group("/info/lfs", func() {
- r.Post("/objects/batch", lfs.CheckAcceptMediaType, lfs.BatchHandler)
- r.Put("/objects/{oid}/{size}", lfs.UploadHandler)
- r.Get("/objects/{oid}/{filename}", lfs.DownloadHandler)
- r.Get("/objects/{oid}", lfs.DownloadHandler)
- r.Post("/verify", lfs.CheckAcceptMediaType, lfs.VerifyHandler)
- r.Group("/locks", func() {
- r.Get("/", lfs.GetListLockHandler)
- r.Post("/", lfs.PostLockHandler)
- r.Post("/verify", lfs.VerifyLockHandler)
- r.Post("/{lid}/unlock", lfs.UnLockHandler)
- }, lfs.CheckAcceptMediaType)
- r.Any("/*", func(ctx *context.Context) {
- ctx.NotFound("", nil)
- })
- }, swapAuthToken)
- }, common.Sessioner(), context.Contexter())
- // end "/repo/{username}/{reponame}": git (LFS) API mirror
+ r.Group("/repo", func() {
+ // FIXME: it is not right to use context.Contexter here because all routes here should use PrivateContext
+ common.AddOwnerRepoGitLFSRoutes(r, func(ctx *context.PrivateContext) {
+ webContext := &context.Context{Base: ctx.Base}
+ ctx.AppendContextValue(context.WebContextKey, webContext)
+ }, web.RouterMockPoint(RouterMockPointInternalLFS))
+ })
return r
}
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/lfs"
_ "code.gitea.io/gitea/modules/session" // to registers all internal adapters
m.Post("/action/{action}", reqSignIn, repo.Action)
}, ignSignIn, context.RepoAssignment, context.RepoRef())
+ common.AddOwnerRepoGitLFSRoutes(m, ignSignInAndCsrf, lfsServerEnabled)
m.Group("/{username}/{reponame}", func() {
- m.Group("/info/lfs", func() {
- m.Post("/objects/batch", lfs.CheckAcceptMediaType, lfs.BatchHandler)
- m.Put("/objects/{oid}/{size}", lfs.UploadHandler)
- m.Get("/objects/{oid}/{filename}", lfs.DownloadHandler)
- m.Get("/objects/{oid}", lfs.DownloadHandler)
- m.Post("/verify", lfs.CheckAcceptMediaType, lfs.VerifyHandler)
- m.Group("/locks", func() {
- m.Get("/", lfs.GetListLockHandler)
- m.Post("/", lfs.PostLockHandler)
- m.Post("/verify", lfs.VerifyLockHandler)
- m.Post("/{lid}/unlock", lfs.UnLockHandler)
- }, lfs.CheckAcceptMediaType)
- m.Any("/*", func(ctx *context.Context) {
- ctx.NotFound("", nil)
- })
- }, ignSignInAndCsrf, lfsServerEnabled)
gitHTTPRouters(m)
})
// end "/{username}/{reponame}.git": git support
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)
--- /dev/null
+// 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"))
+ }
+}
--- /dev/null
+// 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/....")
+ })
+}
--- /dev/null
+// 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)
+ })
+}
+++ /dev/null
-// 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)
- })
-}
"database/sql"
"fmt"
"os"
- "path"
"path/filepath"
"testing"
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)
}
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
}
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 {