]> source.dussan.org Git - gitea.git/commitdiff
Refactor LFS SSH and internal routers (#32473)
authorwxiaoguang <wxiaoguang@gmail.com>
Tue, 12 Nov 2024 02:38:22 +0000 (10:38 +0800)
committerGitHub <noreply@github.com>
Tue, 12 Nov 2024 02:38:22 +0000 (02:38 +0000)
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.

18 files changed:
cmd/serv.go
models/fixtures/lfs_meta_object.yml
models/migrations/v1_21/v276.go
modules/git/batch_reader.go
modules/lfstransfer/backend/backend.go
modules/lfstransfer/backend/lock.go
modules/lfstransfer/backend/util.go
modules/private/internal.go
modules/web/route.go
routers/common/lfs.go [new file with mode: 0644]
routers/private/internal.go
routers/web/web.go
tests/integration/api_repo_file_get_test.go
tests/integration/git_general_test.go [new file with mode: 0644]
tests/integration/git_lfs_ssh_test.go [new file with mode: 0644]
tests/integration/git_misc_test.go [new file with mode: 0644]
tests/integration/git_test.go [deleted file]
tests/test_utils.go

index 2d2df8aa23b88a50acfb8b8897d9fe3e1fa7bdd3..d2271b68d29e5bffb70176bc7a11af11c63a3046 100644 (file)
@@ -111,12 +111,10 @@ func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error
                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)
        }
@@ -288,10 +286,10 @@ func runServ(c *cli.Context) error {
        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]
index 1c29e02d44da60cab0048a826935eb46bbb36609..5430506d70a623834f0c64ade38dd1d04785258f 100644 (file)
@@ -1,4 +1,11 @@
 # 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
@@ -11,7 +18,7 @@
 
   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
index ed1bc3bda52416f66705964fb58f4e9db976eb9e..15177bf0406dfc4f81e154968e9d1a4385cdc913 100644 (file)
@@ -12,6 +12,7 @@ import (
        "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"
 )
@@ -163,7 +164,9 @@ func migratePushMirrors(x *xorm.Engine) error {
 
 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)
index 3b1a466b2eaa90a17b7bf74c1602a39ec144900a..7dfda721554ddab50f50fe15ed9f072ca7c315bf 100644 (file)
@@ -146,9 +146,8 @@ func catFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi
 }
 
 // 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 {
index d4523e1abfab52db5e417cb5e8524e2b7f3ec83e..2b1fe49fdab283cbcc1bd7d1489a4a1ab3be6170 100644 (file)
@@ -33,12 +33,12 @@ var _ transfer.Backend = &GiteaBackend{}
 
 // 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) {
@@ -48,7 +48,7 @@ func New(ctx context.Context, repo, op, token string, logger transfer.Logger) (t
                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
@@ -73,10 +73,10 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
        }
        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()
@@ -119,7 +119,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
                                }
                                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
                                }
@@ -142,7 +142,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
                                }
                                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
                                }
@@ -183,9 +183,9 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser,
        }
        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()
@@ -229,10 +229,10 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer
        }
        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 {
@@ -279,10 +279,10 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans
        }
        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()
index f72ffd5b6f96da24811e8c6b912ff693718f9492..f094cce1db657f0a8737589139f6cad1531dcb39 100644 (file)
@@ -21,17 +21,17 @@ import (
 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
@@ -45,10 +45,10 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) {
        }
        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()
@@ -97,10 +97,10 @@ func (g *giteaLockBackend) Unlock(lock transfer.Lock) error {
        }
        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()
@@ -180,10 +180,10 @@ func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, er
        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()
index 126ac001753dbc00e0c3f3359d3894272b3c9c22..cffefef375c79b2ff45c6e7b90898c3713ef317f 100644 (file)
@@ -20,11 +20,11 @@ import (
 
 // 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
index 9c330a24a865b8ab942874dab8760802e08b1fdd..c7e7773524d7902109b32b18954e037cebca22a3 100644 (file)
@@ -43,7 +43,7 @@ Ensure you are running in the correct environment or set the correct configurati
        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,
index b02f66802ee504077452644b543fdc1a8f1cbe89..77c411a97b911aff8656b76f6daf708e87afcbc4 100644 (file)
@@ -6,6 +6,7 @@ package web
 import (
        "net/http"
        "net/url"
+       "reflect"
        "strings"
 
        "code.gitea.io/gitea/modules/setting"
@@ -82,15 +83,23 @@ func (r *Router) getPattern(pattern string) string {
        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))
                }
        }
diff --git a/routers/common/lfs.go b/routers/common/lfs.go
new file mode 100644 (file)
index 0000000..ba6e116
--- /dev/null
@@ -0,0 +1,29 @@
+// 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...)
+}
index f9adff388cfd05d99c0cddad0cd88612228e49ad..db074238c6eb9dfd5ae5269a891b8cbfd6515107 100644 (file)
@@ -5,6 +5,7 @@
 package private
 
 import (
+       "crypto/subtle"
        "net/http"
        "strings"
 
@@ -14,28 +15,30 @@ import (
        "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)
        })
 }
 
@@ -48,20 +51,12 @@ func bind[T any](_ T) any {
        }
 }
 
-// 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)
@@ -90,25 +85,13 @@ func Routes() *web.Router {
        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
 }
index 907bf88f6f77c4671dca4ebfd2c9a30c99883b8d..e0915e6a6efdf83852d3d891471bff03c62680ec 100644 (file)
@@ -44,7 +44,6 @@ import (
        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
 
@@ -1598,23 +1597,8 @@ func registerRoutes(m *web.Router) {
                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
index 4649babad1d2927074e0888524991e3caef1109b..27bc9e25bfcd825a1be653bd8433d9c8a668c887 100644 (file)
@@ -39,7 +39,7 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
 
                        t.Run("Partial Clone", doPartialGitClone(dstPath2, u))
 
-                       lfs, _ := lfsCommitAndPushTest(t, dstPath)
+                       lfs := lfsCommitAndPushTest(t, dstPath, littleSize)[0]
 
                        reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs)
                        respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
diff --git a/tests/integration/git_general_test.go b/tests/integration/git_general_test.go
new file mode 100644 (file)
index 0000000..7fd19e7
--- /dev/null
@@ -0,0 +1,891 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+       "crypto/rand"
+       "encoding/hex"
+       "fmt"
+       "net/http"
+       "net/url"
+       "os"
+       "path"
+       "path/filepath"
+       "strconv"
+       "testing"
+       "time"
+
+       auth_model "code.gitea.io/gitea/models/auth"
+       "code.gitea.io/gitea/models/db"
+       issues_model "code.gitea.io/gitea/models/issues"
+       "code.gitea.io/gitea/models/perm"
+       repo_model "code.gitea.io/gitea/models/repo"
+       "code.gitea.io/gitea/models/unittest"
+       user_model "code.gitea.io/gitea/models/user"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/lfs"
+       "code.gitea.io/gitea/modules/setting"
+       api "code.gitea.io/gitea/modules/structs"
+       gitea_context "code.gitea.io/gitea/services/context"
+       "code.gitea.io/gitea/tests"
+
+       "github.com/stretchr/testify/assert"
+)
+
+const (
+       littleSize = 1024              // 1K
+       bigSize    = 128 * 1024 * 1024 // 128M
+)
+
+func TestGitGeneral(t *testing.T) {
+       onGiteaRun(t, testGitGeneral)
+}
+
+func testGitGeneral(t *testing.T, u *url.URL) {
+       username := "user2"
+       baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
+
+       u.Path = baseAPITestContext.GitPath()
+
+       forkedUserCtx := NewAPITestContext(t, "user4", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
+
+       t.Run("HTTP", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+               ensureAnonymousClone(t, u)
+               httpContext := baseAPITestContext
+               httpContext.Reponame = "repo-tmp-17"
+               forkedUserCtx.Reponame = httpContext.Reponame
+
+               dstPath := t.TempDir()
+
+               t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false))
+               t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, httpContext.Username, perm.AccessModeRead))
+
+               t.Run("ForkFromDifferentUser", doAPIForkRepository(httpContext, forkedUserCtx.Username))
+
+               u.Path = httpContext.GitPath()
+               u.User = url.UserPassword(username, userPassword)
+
+               t.Run("Clone", doGitClone(dstPath, u))
+
+               dstPath2 := t.TempDir()
+
+               t.Run("Partial Clone", doPartialGitClone(dstPath2, u))
+
+               pushedFilesStandard := standardCommitAndPushTest(t, dstPath, littleSize, bigSize)
+               pushedFilesLFS := lfsCommitAndPushTest(t, dstPath, littleSize, bigSize)
+               rawTest(t, &httpContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
+               mediaTest(t, &httpContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
+
+               t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "test/head"))
+               t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))
+               t.Run("AutoMerge", doAutoPRMerge(&httpContext, dstPath))
+               t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge"))
+               t.Run("MergeFork", func(t *testing.T) {
+                       defer tests.PrintCurrentTest(t)()
+                       t.Run("CreatePRAndMerge", doMergeFork(httpContext, forkedUserCtx, "master", httpContext.Username+":master"))
+                       rawTest(t, &forkedUserCtx, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
+                       mediaTest(t, &forkedUserCtx, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
+               })
+
+               t.Run("PushCreate", doPushCreate(httpContext, u))
+       })
+       t.Run("SSH", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+               sshContext := baseAPITestContext
+               sshContext.Reponame = "repo-tmp-18"
+               keyname := "my-testing-key"
+               forkedUserCtx.Reponame = sshContext.Reponame
+               t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false))
+               t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, sshContext.Username, perm.AccessModeRead))
+               t.Run("ForkFromDifferentUser", doAPIForkRepository(sshContext, forkedUserCtx.Username))
+
+               // Setup key the user ssh key
+               withKeyFile(t, keyname, func(keyFile string) {
+                       t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile))
+
+                       // Setup remote link
+                       // TODO: get url from api
+                       sshURL := createSSHUrl(sshContext.GitPath(), u)
+
+                       // Setup clone folder
+                       dstPath := t.TempDir()
+
+                       t.Run("Clone", doGitClone(dstPath, sshURL))
+
+                       pushedFilesStandard := standardCommitAndPushTest(t, dstPath, littleSize, bigSize)
+                       pushedFilesLFS := lfsCommitAndPushTest(t, dstPath, littleSize, bigSize)
+                       rawTest(t, &sshContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
+                       mediaTest(t, &sshContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
+
+                       t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "test/head2"))
+                       t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath))
+                       t.Run("MergeFork", func(t *testing.T) {
+                               defer tests.PrintCurrentTest(t)()
+                               t.Run("CreatePRAndMerge", doMergeFork(sshContext, forkedUserCtx, "master", sshContext.Username+":master"))
+                               rawTest(t, &forkedUserCtx, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
+                               mediaTest(t, &forkedUserCtx, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
+                       })
+
+                       t.Run("PushCreate", doPushCreate(sshContext, sshURL))
+               })
+       })
+}
+
+func ensureAnonymousClone(t *testing.T, u *url.URL) {
+       dstLocalPath := t.TempDir()
+       t.Run("CloneAnonymous", doGitClone(dstLocalPath, u))
+}
+
+func standardCommitAndPushTest(t *testing.T, dstPath string, sizes ...int) (pushedFiles []string) {
+       t.Run("CommitAndPushStandard", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+               pushedFiles = commitAndPushTest(t, dstPath, "data-file-", sizes...)
+       })
+       return pushedFiles
+}
+
+func lfsCommitAndPushTest(t *testing.T, dstPath string, sizes ...int) (pushedFiles []string) {
+       t.Run("CommitAndPushLFS", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+               prefix := "lfs-data-file-"
+               err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("install").Run(&git.RunOpts{Dir: dstPath})
+               assert.NoError(t, err)
+               _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("track").AddDynamicArguments(prefix + "*").RunStdString(&git.RunOpts{Dir: dstPath})
+               assert.NoError(t, err)
+               err = git.AddChanges(dstPath, false, ".gitattributes")
+               assert.NoError(t, err)
+
+               err = git.CommitChangesWithArgs(dstPath, git.AllowLFSFiltersArgs(), git.CommitChangesOptions{
+                       Committer: &git.Signature{
+                               Email: "user2@example.com",
+                               Name:  "User Two",
+                               When:  time.Now(),
+                       },
+                       Author: &git.Signature{
+                               Email: "user2@example.com",
+                               Name:  "User Two",
+                               When:  time.Now(),
+                       },
+                       Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
+               })
+               assert.NoError(t, err)
+
+               pushedFiles = commitAndPushTest(t, dstPath, prefix, sizes...)
+               t.Run("Locks", func(t *testing.T) {
+                       defer tests.PrintCurrentTest(t)()
+                       lockTest(t, dstPath)
+               })
+       })
+       return pushedFiles
+}
+
+func commitAndPushTest(t *testing.T, dstPath, prefix string, sizes ...int) (pushedFiles []string) {
+       for _, size := range sizes {
+               t.Run("PushCommit Size-"+strconv.Itoa(size), func(t *testing.T) {
+                       defer tests.PrintCurrentTest(t)()
+                       pushedFiles = append(pushedFiles, doCommitAndPush(t, size, dstPath, prefix))
+               })
+       }
+       return pushedFiles
+}
+
+func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) {
+       t.Run("Raw", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+               username := ctx.Username
+               reponame := ctx.Reponame
+
+               session := loginUser(t, username)
+
+               // Request raw paths
+               req := NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", little))
+               resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
+               assert.Equal(t, littleSize, resp.Length)
+
+               if setting.LFS.StartServer {
+                       req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS))
+                       resp := session.MakeRequest(t, req, http.StatusOK)
+                       assert.NotEqual(t, littleSize, resp.Body.Len())
+                       assert.LessOrEqual(t, resp.Body.Len(), 1024)
+                       if resp.Body.Len() != littleSize && resp.Body.Len() <= 1024 {
+                               assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier)
+                       }
+               }
+
+               if !testing.Short() {
+                       req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big))
+                       resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
+                       assert.Equal(t, bigSize, resp.Length)
+
+                       if setting.LFS.StartServer {
+                               req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS))
+                               resp := session.MakeRequest(t, req, http.StatusOK)
+                               assert.NotEqual(t, bigSize, resp.Body.Len())
+                               if resp.Body.Len() != bigSize && resp.Body.Len() <= 1024 {
+                                       assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier)
+                               }
+                       }
+               }
+       })
+}
+
+func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) {
+       t.Run("Media", func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+
+               username := ctx.Username
+               reponame := ctx.Reponame
+
+               session := loginUser(t, username)
+
+               // Request media paths
+               req := NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", little))
+               resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
+               assert.Equal(t, littleSize, resp.Length)
+
+               req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS))
+               resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
+               assert.Equal(t, littleSize, resp.Length)
+
+               if !testing.Short() {
+                       req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big))
+                       resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
+                       assert.Equal(t, bigSize, resp.Length)
+
+                       if setting.LFS.StartServer {
+                               req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS))
+                               resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
+                               assert.Equal(t, bigSize, resp.Length)
+                       }
+               }
+       })
+}
+
+func lockTest(t *testing.T, repoPath string) {
+       lockFileTest(t, "README.md", repoPath)
+}
+
+func lockFileTest(t *testing.T, filename, repoPath string) {
+       _, _, err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("locks").RunStdString(&git.RunOpts{Dir: repoPath})
+       assert.NoError(t, err)
+       _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("lock").AddDynamicArguments(filename).RunStdString(&git.RunOpts{Dir: repoPath})
+       assert.NoError(t, err)
+       _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("locks").RunStdString(&git.RunOpts{Dir: repoPath})
+       assert.NoError(t, err)
+       _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("unlock").AddDynamicArguments(filename).RunStdString(&git.RunOpts{Dir: repoPath})
+       assert.NoError(t, err)
+}
+
+func doCommitAndPush(t *testing.T, size int, repoPath, prefix string) string {
+       name, err := generateCommitWithNewData(size, repoPath, "user2@example.com", "User Two", prefix)
+       assert.NoError(t, err)
+       _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "master").RunStdString(&git.RunOpts{Dir: repoPath}) // Push
+       assert.NoError(t, err)
+       return name
+}
+
+func generateCommitWithNewData(size int, repoPath, email, fullName, prefix string) (string, error) {
+       // Generate random file
+       bufSize := 4 * 1024
+       if bufSize > size {
+               bufSize = size
+       }
+
+       buffer := make([]byte, bufSize)
+
+       tmpFile, err := os.CreateTemp(repoPath, prefix)
+       if err != nil {
+               return "", err
+       }
+       defer tmpFile.Close()
+       written := 0
+       for written < size {
+               n := size - written
+               if n > bufSize {
+                       n = bufSize
+               }
+               _, err := rand.Read(buffer[:n])
+               if err != nil {
+                       return "", err
+               }
+               n, err = tmpFile.Write(buffer[:n])
+               if err != nil {
+                       return "", err
+               }
+               written += n
+       }
+
+       // Commit
+       // Now here we should explicitly allow lfs filters to run
+       globalArgs := git.AllowLFSFiltersArgs()
+       err = git.AddChangesWithArgs(repoPath, globalArgs, false, filepath.Base(tmpFile.Name()))
+       if err != nil {
+               return "", err
+       }
+       err = git.CommitChangesWithArgs(repoPath, globalArgs, git.CommitChangesOptions{
+               Committer: &git.Signature{
+                       Email: email,
+                       Name:  fullName,
+                       When:  time.Now(),
+               },
+               Author: &git.Signature{
+                       Email: email,
+                       Name:  fullName,
+                       When:  time.Now(),
+               },
+               Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
+       })
+       return filepath.Base(tmpFile.Name()), err
+}
+
+func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
+       return func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+               t.Run("CreateBranchProtected", doGitCreateBranch(dstPath, "protected"))
+               t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
+
+               ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
+
+               // Protect branch without any whitelisting
+               t.Run("ProtectBranchNoWhitelist", func(t *testing.T) {
+                       doProtectBranch(ctx, "protected", "", "", "")
+               })
+
+               // Try to push without permissions, which should fail
+               t.Run("TryPushWithoutPermissions", func(t *testing.T) {
+                       _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+                       assert.NoError(t, err)
+                       doGitPushTestRepositoryFail(dstPath, "origin", "protected")
+               })
+
+               // Set up permissions for normal push but not force push
+               t.Run("SetupNormalPushPermissions", func(t *testing.T) {
+                       doProtectBranch(ctx, "protected", baseCtx.Username, "", "")
+               })
+
+               // Normal push should work
+               t.Run("NormalPushWithPermissions", func(t *testing.T) {
+                       _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+                       assert.NoError(t, err)
+                       doGitPushTestRepository(dstPath, "origin", "protected")
+               })
+
+               // Try to force push without force push permissions, which should fail
+               t.Run("ForcePushWithoutForcePermissions", func(t *testing.T) {
+                       t.Run("CreateDivergentHistory", func(t *testing.T) {
+                               git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath})
+                               _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-new")
+                               assert.NoError(t, err)
+                       })
+                       doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")
+               })
+
+               // Set up permissions for force push but not normal push
+               t.Run("SetupForcePushPermissions", func(t *testing.T) {
+                       doProtectBranch(ctx, "protected", "", baseCtx.Username, "")
+               })
+
+               // Try to force push without normal push permissions, which should fail
+               t.Run("ForcePushWithoutNormalPermissions", func(t *testing.T) {
+                       doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")
+               })
+
+               // Set up permissions for normal and force push (both are required to force push)
+               t.Run("SetupNormalAndForcePushPermissions", func(t *testing.T) {
+                       doProtectBranch(ctx, "protected", baseCtx.Username, baseCtx.Username, "")
+               })
+
+               // Force push should now work
+               t.Run("ForcePushWithPermissions", func(t *testing.T) {
+                       doGitPushTestRepository(dstPath, "-f", "origin", "protected")
+               })
+
+               t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", "", ""))
+               t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected"))
+               var pr api.PullRequest
+               var err error
+               t.Run("CreatePullRequest", func(t *testing.T) {
+                       pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected")(t)
+                       assert.NoError(t, err)
+               })
+               t.Run("GenerateCommit", func(t *testing.T) {
+                       _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+                       assert.NoError(t, err)
+               })
+               t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected-2"))
+               var pr2 api.PullRequest
+               t.Run("CreatePullRequest", func(t *testing.T) {
+                       pr2, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "unprotected", "unprotected-2")(t)
+                       assert.NoError(t, err)
+               })
+               t.Run("MergePR2", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr2.Index))
+               t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+               t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
+
+               t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "", "unprotected-file-*"))
+               t.Run("GenerateCommit", func(t *testing.T) {
+                       _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "unprotected-file-")
+                       assert.NoError(t, err)
+               })
+               t.Run("PushUnprotectedFilesToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
+
+               t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, "", ""))
+
+               t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master"))
+               t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce"))
+               t.Run("GenerateCommit", func(t *testing.T) {
+                       _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+                       assert.NoError(t, err)
+               })
+               t.Run("FailToForcePushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "toforce:protected"))
+               t.Run("MergeProtectedToToforce", doGitMerge(dstPath, "protected"))
+               t.Run("PushToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "toforce:protected"))
+               t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master"))
+       }
+}
+
+func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns string) func(t *testing.T) {
+       // We are going to just use the owner to set the protection.
+       return func(t *testing.T) {
+               csrf := GetUserCSRFToken(t, ctx.Session)
+
+               formData := map[string]string{
+                       "_csrf":                     csrf,
+                       "rule_name":                 branch,
+                       "unprotected_file_patterns": unprotectedFilePatterns,
+               }
+
+               if userToWhitelistPush != "" {
+                       user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistPush)
+                       assert.NoError(t, err)
+                       formData["whitelist_users"] = strconv.FormatInt(user.ID, 10)
+                       formData["enable_push"] = "whitelist"
+                       formData["enable_whitelist"] = "on"
+               }
+
+               if userToWhitelistForcePush != "" {
+                       user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistForcePush)
+                       assert.NoError(t, err)
+                       formData["force_push_allowlist_users"] = strconv.FormatInt(user.ID, 10)
+                       formData["enable_force_push"] = "whitelist"
+                       formData["enable_force_push_allowlist"] = "on"
+               }
+
+               // Send the request to update branch protection settings
+               req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), formData)
+               ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
+
+               // Check if master branch has been locked successfully
+               flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash)
+               assert.NotNil(t, flashCookie)
+               assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2522"+url.QueryEscape(branch)+"%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
+       }
+}
+
+func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) func(t *testing.T) {
+       return func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+               var pr api.PullRequest
+               var err error
+
+               // Create a test pullrequest
+               t.Run("CreatePullRequest", func(t *testing.T) {
+                       pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t)
+                       assert.NoError(t, err)
+               })
+
+               // Ensure the PR page works
+               t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
+
+               // Then get the diff string
+               var diffHash string
+               var diffLength int
+               t.Run("GetDiff", func(t *testing.T) {
+                       req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(baseCtx.Username), url.PathEscape(baseCtx.Reponame), pr.Index))
+                       resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK)
+                       diffHash = string(resp.Hash.Sum(nil))
+                       diffLength = resp.Length
+               })
+
+               // Now: Merge the PR & make sure that doesn't break the PR page or change its diff
+               t.Run("MergePR", doAPIMergePullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+               t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
+               t.Run("CheckPR", func(t *testing.T) {
+                       oldMergeBase := pr.MergeBase
+                       pr2, err := doAPIGetPullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
+                       assert.NoError(t, err)
+                       assert.Equal(t, oldMergeBase, pr2.MergeBase)
+               })
+               t.Run("EnsurDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
+
+               // Then: Delete the head branch & make sure that doesn't break the PR page or change its diff
+               t.Run("DeleteHeadBranch", doBranchDelete(baseCtx, baseCtx.Username, baseCtx.Reponame, headBranch))
+               t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
+               t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
+
+               // Delete the head repository & make sure that doesn't break the PR page or change its diff
+               t.Run("DeleteHeadRepository", doAPIDeleteRepository(ctx))
+               t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
+               t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
+       }
+}
+
+func doCreatePRAndSetManuallyMerged(ctx, baseCtx APITestContext, dstPath, baseBranch, headBranch string) func(t *testing.T) {
+       return func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+               var (
+                       pr           api.PullRequest
+                       err          error
+                       lastCommitID string
+               )
+
+               trueBool := true
+               falseBool := false
+
+               t.Run("AllowSetManuallyMergedAndSwitchOffAutodetectManualMerge", doAPIEditRepository(baseCtx, &api.EditRepoOption{
+                       HasPullRequests:       &trueBool,
+                       AllowManualMerge:      &trueBool,
+                       AutodetectManualMerge: &falseBool,
+               }))
+
+               t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
+               t.Run("PushToHeadBranch", doGitPushTestRepository(dstPath, "origin", headBranch))
+               t.Run("CreateEmptyPullRequest", func(t *testing.T) {
+                       pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t)
+                       assert.NoError(t, err)
+               })
+               lastCommitID = pr.Base.Sha
+               t.Run("ManuallyMergePR", doAPIManuallyMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, lastCommitID, pr.Index))
+       }
+}
+
+func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest) func(t *testing.T) {
+       return func(t *testing.T) {
+               req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
+               ctx.Session.MakeRequest(t, req, http.StatusOK)
+               req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/files", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
+               ctx.Session.MakeRequest(t, req, http.StatusOK)
+               req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
+               ctx.Session.MakeRequest(t, req, http.StatusOK)
+       }
+}
+
+func doEnsureDiffNoChange(ctx APITestContext, pr api.PullRequest, diffHash string, diffLength int) func(t *testing.T) {
+       return func(t *testing.T) {
+               req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
+               resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK)
+               actual := string(resp.Hash.Sum(nil))
+               actualLength := resp.Length
+
+               equal := diffHash == actual
+               assert.True(t, equal, "Unexpected change in the diff string: expected hash: %s size: %d but was actually: %s size: %d", hex.EncodeToString([]byte(diffHash)), diffLength, hex.EncodeToString([]byte(actual)), actualLength)
+       }
+}
+
+func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) {
+       return func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+
+               // create a context for a currently non-existent repository
+               ctx.Reponame = fmt.Sprintf("repo-tmp-push-create-%s", u.Scheme)
+               u.Path = ctx.GitPath()
+
+               // Create a temporary directory
+               tmpDir := t.TempDir()
+
+               // Now create local repository to push as our test and set its origin
+               t.Run("InitTestRepository", doGitInitTestRepository(tmpDir))
+               t.Run("AddRemote", doGitAddRemote(tmpDir, "origin", u))
+
+               // Disable "Push To Create" and attempt to push
+               setting.Repository.EnablePushCreateUser = false
+               t.Run("FailToPushAndCreateTestRepository", doGitPushTestRepositoryFail(tmpDir, "origin", "master"))
+
+               // Enable "Push To Create"
+               setting.Repository.EnablePushCreateUser = true
+
+               // Assert that cloning from a non-existent repository does not create it and that it definitely wasn't create above
+               t.Run("FailToCloneFromNonExistentRepository", doGitCloneFail(u))
+
+               // Then "Push To Create"x
+               t.Run("SuccessfullyPushAndCreateTestRepository", doGitPushTestRepository(tmpDir, "origin", "master"))
+
+               // Finally, fetch repo from database and ensure the correct repository has been created
+               repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ctx.Username, ctx.Reponame)
+               assert.NoError(t, err)
+               assert.False(t, repo.IsEmpty)
+               assert.True(t, repo.IsPrivate)
+
+               // Now add a remote that is invalid to "Push To Create"
+               invalidCtx := ctx
+               invalidCtx.Reponame = fmt.Sprintf("invalid/repo-tmp-push-create-%s", u.Scheme)
+               u.Path = invalidCtx.GitPath()
+               t.Run("AddInvalidRemote", doGitAddRemote(tmpDir, "invalid", u))
+
+               // Fail to "Push To Create" the invalid
+               t.Run("FailToPushAndCreateInvalidTestRepository", doGitPushTestRepositoryFail(tmpDir, "invalid", "master"))
+       }
+}
+
+func doBranchDelete(ctx APITestContext, owner, repo, branch string) func(*testing.T) {
+       return func(t *testing.T) {
+               csrf := GetUserCSRFToken(t, ctx.Session)
+
+               req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/branches/delete?name=%s", url.PathEscape(owner), url.PathEscape(repo), url.QueryEscape(branch)), map[string]string{
+                       "_csrf": csrf,
+               })
+               ctx.Session.MakeRequest(t, req, http.StatusOK)
+       }
+}
+
+func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
+       return func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+
+               ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
+
+               t.Run("CheckoutProtected", doGitCheckoutBranch(dstPath, "protected"))
+               t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
+               t.Run("GenerateCommit", func(t *testing.T) {
+                       _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+                       assert.NoError(t, err)
+               })
+               t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected3"))
+               var pr api.PullRequest
+               var err error
+               t.Run("CreatePullRequest", func(t *testing.T) {
+                       pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected3")(t)
+                       assert.NoError(t, err)
+               })
+
+               // Request repository commits page
+               req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", baseCtx.Username, baseCtx.Reponame, pr.Index))
+               resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
+               doc := NewHTMLParser(t, resp.Body)
+
+               // Get first commit URL
+               commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
+               assert.True(t, exists)
+               assert.NotEmpty(t, commitURL)
+
+               commitID := path.Base(commitURL)
+
+               addCommitStatus := func(status api.CommitStatusState) func(*testing.T) {
+                       return doAPICreateCommitStatus(ctx, commitID, api.CreateStatusOption{
+                               State:       status,
+                               TargetURL:   "http://test.ci/",
+                               Description: "",
+                               Context:     "testci",
+                       })
+               }
+
+               // Call API to add Pending status for commit
+               t.Run("CreateStatus", addCommitStatus(api.CommitStatusPending))
+
+               // Cancel not existing auto merge
+               ctx.ExpectedCode = http.StatusNotFound
+               t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+
+               // Add auto merge request
+               ctx.ExpectedCode = http.StatusCreated
+               t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+
+               // Can not create schedule twice
+               ctx.ExpectedCode = http.StatusConflict
+               t.Run("AutoMergePRTwice", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+
+               // Cancel auto merge request
+               ctx.ExpectedCode = http.StatusNoContent
+               t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+
+               // Add auto merge request
+               ctx.ExpectedCode = http.StatusCreated
+               t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
+
+               // Check pr status
+               ctx.ExpectedCode = 0
+               pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
+               assert.NoError(t, err)
+               assert.False(t, pr.HasMerged)
+
+               // Call API to add Failure status for commit
+               t.Run("CreateStatus", addCommitStatus(api.CommitStatusFailure))
+
+               // Check pr status
+               pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
+               assert.NoError(t, err)
+               assert.False(t, pr.HasMerged)
+
+               // Call API to add Success status for commit
+               t.Run("CreateStatus", addCommitStatus(api.CommitStatusSuccess))
+
+               // wait to let gitea merge stuff
+               time.Sleep(time.Second)
+
+               // test pr status
+               pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
+               assert.NoError(t, err)
+               assert.True(t, pr.HasMerged)
+       }
+}
+
+func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string) func(t *testing.T) {
+       return func(t *testing.T) {
+               defer tests.PrintCurrentTest(t)()
+
+               // skip this test if git version is low
+               if !git.DefaultFeatures().SupportProcReceive {
+                       return
+               }
+
+               gitRepo, err := git.OpenRepository(git.DefaultContext, dstPath)
+               if !assert.NoError(t, err) {
+                       return
+               }
+               defer gitRepo.Close()
+
+               var (
+                       pr1, pr2 *issues_model.PullRequest
+                       commit   string
+               )
+               repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ctx.Username, ctx.Reponame)
+               if !assert.NoError(t, err) {
+                       return
+               }
+
+               pullNum := unittest.GetCount(t, &issues_model.PullRequest{})
+
+               t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
+
+               t.Run("AddCommit", func(t *testing.T) {
+                       err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
+                       if !assert.NoError(t, err) {
+                               return
+                       }
+
+                       err = git.AddChanges(dstPath, true)
+                       assert.NoError(t, err)
+
+                       err = git.CommitChanges(dstPath, git.CommitChangesOptions{
+                               Committer: &git.Signature{
+                                       Email: "user2@example.com",
+                                       Name:  "user2",
+                                       When:  time.Now(),
+                               },
+                               Author: &git.Signature{
+                                       Email: "user2@example.com",
+                                       Name:  "user2",
+                                       When:  time.Now(),
+                               },
+                               Message: "Testing commit 1",
+                       })
+                       assert.NoError(t, err)
+                       commit, err = gitRepo.GetRefCommitID("HEAD")
+                       assert.NoError(t, err)
+               })
+
+               t.Run("Push", func(t *testing.T) {
+                       err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + headBranch).Run(&git.RunOpts{Dir: dstPath})
+                       if !assert.NoError(t, err) {
+                               return
+                       }
+                       unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+1)
+                       pr1 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
+                               HeadRepoID: repo.ID,
+                               Flow:       issues_model.PullRequestFlowAGit,
+                       })
+                       if !assert.NotEmpty(t, pr1) {
+                               return
+                       }
+                       prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t)
+                       if !assert.NoError(t, err) {
+                               return
+                       }
+                       assert.Equal(t, "user2/"+headBranch, pr1.HeadBranch)
+                       assert.False(t, prMsg.HasMerged)
+                       assert.Contains(t, "Testing commit 1", prMsg.Body)
+                       assert.Equal(t, commit, prMsg.Head.Sha)
+
+                       _, _, err = git.NewCommand(git.DefaultContext, "push", "origin").AddDynamicArguments("HEAD:refs/for/master/test/" + headBranch).RunStdString(&git.RunOpts{Dir: dstPath})
+                       if !assert.NoError(t, err) {
+                               return
+                       }
+                       unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
+                       pr2 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
+                               HeadRepoID: repo.ID,
+                               Index:      pr1.Index + 1,
+                               Flow:       issues_model.PullRequestFlowAGit,
+                       })
+                       if !assert.NotEmpty(t, pr2) {
+                               return
+                       }
+                       prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t)
+                       if !assert.NoError(t, err) {
+                               return
+                       }
+                       assert.Equal(t, "user2/test/"+headBranch, pr2.HeadBranch)
+                       assert.False(t, prMsg.HasMerged)
+               })
+
+               if pr1 == nil || pr2 == nil {
+                       return
+               }
+
+               t.Run("AddCommit2", func(t *testing.T) {
+                       err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content \n ## test content 2"), 0o666)
+                       if !assert.NoError(t, err) {
+                               return
+                       }
+
+                       err = git.AddChanges(dstPath, true)
+                       assert.NoError(t, err)
+
+                       err = git.CommitChanges(dstPath, git.CommitChangesOptions{
+                               Committer: &git.Signature{
+                                       Email: "user2@example.com",
+                                       Name:  "user2",
+                                       When:  time.Now(),
+                               },
+                               Author: &git.Signature{
+                                       Email: "user2@example.com",
+                                       Name:  "user2",
+                                       When:  time.Now(),
+                               },
+                               Message: "Testing commit 2",
+                       })
+                       assert.NoError(t, err)
+                       commit, err = gitRepo.GetRefCommitID("HEAD")
+                       assert.NoError(t, err)
+               })
+
+               t.Run("Push2", func(t *testing.T) {
+                       err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + headBranch).Run(&git.RunOpts{Dir: dstPath})
+                       if !assert.NoError(t, err) {
+                               return
+                       }
+                       unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
+                       prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t)
+                       if !assert.NoError(t, err) {
+                               return
+                       }
+                       assert.False(t, prMsg.HasMerged)
+                       assert.Equal(t, commit, prMsg.Head.Sha)
+
+                       _, _, err = git.NewCommand(git.DefaultContext, "push", "origin").AddDynamicArguments("HEAD:refs/for/master/test/" + headBranch).RunStdString(&git.RunOpts{Dir: dstPath})
+                       if !assert.NoError(t, err) {
+                               return
+                       }
+                       unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
+                       prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t)
+                       if !assert.NoError(t, err) {
+                               return
+                       }
+                       assert.False(t, prMsg.HasMerged)
+                       assert.Equal(t, commit, prMsg.Head.Sha)
+               })
+               t.Run("Merge", doAPIMergePullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index))
+               t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master"))
+       }
+}
diff --git a/tests/integration/git_lfs_ssh_test.go b/tests/integration/git_lfs_ssh_test.go
new file mode 100644 (file)
index 0000000..33c2fba
--- /dev/null
@@ -0,0 +1,61 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+       "net/url"
+       "sync"
+       "testing"
+
+       auth_model "code.gitea.io/gitea/models/auth"
+       "code.gitea.io/gitea/modules/setting"
+       "code.gitea.io/gitea/modules/web"
+       "code.gitea.io/gitea/routers/private"
+       "code.gitea.io/gitea/services/context"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func TestGitLFSSSH(t *testing.T) {
+       onGiteaRun(t, func(t *testing.T, u *url.URL) {
+               dstPath := t.TempDir()
+               apiTestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
+
+               var mu sync.Mutex
+               var routerCalls []string
+               web.RouteMock(private.RouterMockPointInternalLFS, func(ctx *context.PrivateContext) {
+                       mu.Lock()
+                       routerCalls = append(routerCalls, ctx.Req.Method+" "+ctx.Req.URL.Path)
+                       mu.Unlock()
+               })
+
+               withKeyFile(t, "my-testing-key", func(keyFile string) {
+                       t.Run("CreateUserKey", doAPICreateUserKey(apiTestContext, "test-key", keyFile))
+                       cloneURL := createSSHUrl(apiTestContext.GitPath(), u)
+                       t.Run("Clone", doGitClone(dstPath, cloneURL))
+
+                       cfg, err := setting.CfgProvider.PrepareSaving()
+                       require.NoError(t, err)
+                       cfg.Section("server").Key("LFS_ALLOW_PURE_SSH").SetValue("true")
+                       setting.LFS.AllowPureSSH = true
+                       require.NoError(t, cfg.Save())
+
+                       // do LFS SSH transfer?
+                       lfsCommitAndPushTest(t, dstPath, 10)
+               })
+
+               // FIXME: Here we only see the following calls, but actually there should be calls to "PUT"?
+               // 0 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks"
+               // 1 = {string} "POST /api/internal/repo/user2/repo1.git/info/lfs/objects/batch"
+               // 2 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks"
+               // 3 = {string} "POST /api/internal/repo/user2/repo1.git/info/lfs/locks"
+               // 4 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks"
+               // 5 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks"
+               // 6 = {string} "GET /api/internal/repo/user2/repo1.git/info/lfs/locks"
+               // 7 = {string} "POST /api/internal/repo/user2/repo1.git/info/lfs/locks/24/unlock"
+               assert.NotEmpty(t, routerCalls)
+               // assert.Contains(t, routerCalls, "PUT /api/internal/repo/user2/repo1.git/info/lfs/objects/....")
+       })
+}
diff --git a/tests/integration/git_misc_test.go b/tests/integration/git_misc_test.go
new file mode 100644 (file)
index 0000000..82ab184
--- /dev/null
@@ -0,0 +1,138 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+       "bytes"
+       "context"
+       "io"
+       "net/url"
+       "sync"
+       "testing"
+
+       auth_model "code.gitea.io/gitea/models/auth"
+       "code.gitea.io/gitea/models/db"
+       issues_model "code.gitea.io/gitea/models/issues"
+       repo_model "code.gitea.io/gitea/models/repo"
+       "code.gitea.io/gitea/models/unittest"
+       user_model "code.gitea.io/gitea/models/user"
+       "code.gitea.io/gitea/modules/git"
+       "code.gitea.io/gitea/modules/gitrepo"
+       files_service "code.gitea.io/gitea/services/repository/files"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestDataAsyncDoubleRead_Issue29101(t *testing.T) {
+       onGiteaRun(t, func(t *testing.T, u *url.URL) {
+               user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+               repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+               testContent := bytes.Repeat([]byte{'a'}, 10000)
+               resp, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{
+                       Files: []*files_service.ChangeRepoFile{
+                               {
+                                       Operation:     "create",
+                                       TreePath:      "test.txt",
+                                       ContentReader: bytes.NewReader(testContent),
+                               },
+                       },
+                       OldBranch: repo.DefaultBranch,
+                       NewBranch: repo.DefaultBranch,
+               })
+               assert.NoError(t, err)
+
+               sha := resp.Commit.SHA
+
+               gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo)
+               assert.NoError(t, err)
+
+               commit, err := gitRepo.GetCommit(sha)
+               assert.NoError(t, err)
+
+               entry, err := commit.GetTreeEntryByPath("test.txt")
+               assert.NoError(t, err)
+
+               b := entry.Blob()
+               r1, err := b.DataAsync()
+               assert.NoError(t, err)
+               defer r1.Close()
+               r2, err := b.DataAsync()
+               assert.NoError(t, err)
+               defer r2.Close()
+
+               var data1, data2 []byte
+               wg := sync.WaitGroup{}
+               wg.Add(2)
+               go func() {
+                       data1, _ = io.ReadAll(r1)
+                       assert.NoError(t, err)
+                       wg.Done()
+               }()
+               go func() {
+                       data2, _ = io.ReadAll(r2)
+                       assert.NoError(t, err)
+                       wg.Done()
+               }()
+               wg.Wait()
+               assert.Equal(t, testContent, data1)
+               assert.Equal(t, testContent, data2)
+       })
+}
+
+func TestAgitPullPush(t *testing.T) {
+       onGiteaRun(t, func(t *testing.T, u *url.URL) {
+               baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
+
+               u.Path = baseAPITestContext.GitPath()
+               u.User = url.UserPassword("user2", userPassword)
+
+               dstPath := t.TempDir()
+               doGitClone(dstPath, u)(t)
+
+               gitRepo, err := git.OpenRepository(context.Background(), dstPath)
+               assert.NoError(t, err)
+               defer gitRepo.Close()
+
+               doGitCreateBranch(dstPath, "test-agit-push")
+
+               // commit 1
+               _, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
+               assert.NoError(t, err)
+
+               // push to create an agit pull request
+               err = git.NewCommand(git.DefaultContext, "push", "origin",
+                       "-o", "title=test-title", "-o", "description=test-description",
+                       "HEAD:refs/for/master/test-agit-push",
+               ).Run(&git.RunOpts{Dir: dstPath})
+               assert.NoError(t, err)
+
+               // check pull request exist
+               pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: 1, Flow: issues_model.PullRequestFlowAGit, HeadBranch: "user2/test-agit-push"})
+               assert.NoError(t, pr.LoadIssue(db.DefaultContext))
+               assert.Equal(t, "test-title", pr.Issue.Title)
+               assert.Equal(t, "test-description", pr.Issue.Content)
+
+               // commit 2
+               _, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-2-")
+               assert.NoError(t, err)
+
+               // push 2
+               err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").Run(&git.RunOpts{Dir: dstPath})
+               assert.NoError(t, err)
+
+               // reset to first commit
+               err = git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath})
+               assert.NoError(t, err)
+
+               // test force push without confirm
+               _, stderr, err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").RunStdString(&git.RunOpts{Dir: dstPath})
+               assert.Error(t, err)
+               assert.Contains(t, stderr, "[remote rejected] HEAD -> refs/for/master/test-agit-push (request `force-push` push option)")
+
+               // test force push with confirm
+               err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push", "-o", "force-push").Run(&git.RunOpts{Dir: dstPath})
+               assert.NoError(t, err)
+       })
+}
diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go
deleted file mode 100644 (file)
index 76db3c6..0000000
+++ /dev/null
@@ -1,1002 +0,0 @@
-// Copyright 2017 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package integration
-
-import (
-       "bytes"
-       "context"
-       "crypto/rand"
-       "encoding/hex"
-       "fmt"
-       "net/http"
-       "net/url"
-       "os"
-       "path"
-       "path/filepath"
-       "strconv"
-       "testing"
-       "time"
-
-       auth_model "code.gitea.io/gitea/models/auth"
-       "code.gitea.io/gitea/models/db"
-       issues_model "code.gitea.io/gitea/models/issues"
-       "code.gitea.io/gitea/models/perm"
-       repo_model "code.gitea.io/gitea/models/repo"
-       "code.gitea.io/gitea/models/unittest"
-       user_model "code.gitea.io/gitea/models/user"
-       "code.gitea.io/gitea/modules/git"
-       "code.gitea.io/gitea/modules/gitrepo"
-       "code.gitea.io/gitea/modules/lfs"
-       "code.gitea.io/gitea/modules/setting"
-       api "code.gitea.io/gitea/modules/structs"
-       gitea_context "code.gitea.io/gitea/services/context"
-       files_service "code.gitea.io/gitea/services/repository/files"
-       "code.gitea.io/gitea/tests"
-
-       "github.com/stretchr/testify/assert"
-)
-
-const (
-       littleSize = 1024              // 1ko
-       bigSize    = 128 * 1024 * 1024 // 128Mo
-)
-
-func TestGit(t *testing.T) {
-       onGiteaRun(t, testGit)
-}
-
-func testGit(t *testing.T, u *url.URL) {
-       username := "user2"
-       baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
-
-       u.Path = baseAPITestContext.GitPath()
-
-       forkedUserCtx := NewAPITestContext(t, "user4", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
-
-       t.Run("HTTP", func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-               ensureAnonymousClone(t, u)
-               httpContext := baseAPITestContext
-               httpContext.Reponame = "repo-tmp-17"
-               forkedUserCtx.Reponame = httpContext.Reponame
-
-               dstPath := t.TempDir()
-
-               t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false))
-               t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, httpContext.Username, perm.AccessModeRead))
-
-               t.Run("ForkFromDifferentUser", doAPIForkRepository(httpContext, forkedUserCtx.Username))
-
-               u.Path = httpContext.GitPath()
-               u.User = url.UserPassword(username, userPassword)
-
-               t.Run("Clone", doGitClone(dstPath, u))
-
-               dstPath2 := t.TempDir()
-
-               t.Run("Partial Clone", doPartialGitClone(dstPath2, u))
-
-               little, big := standardCommitAndPushTest(t, dstPath)
-               littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath)
-               rawTest(t, &httpContext, little, big, littleLFS, bigLFS)
-               mediaTest(t, &httpContext, little, big, littleLFS, bigLFS)
-
-               t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "test/head"))
-               t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))
-               t.Run("AutoMerge", doAutoPRMerge(&httpContext, dstPath))
-               t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge"))
-               t.Run("MergeFork", func(t *testing.T) {
-                       defer tests.PrintCurrentTest(t)()
-                       t.Run("CreatePRAndMerge", doMergeFork(httpContext, forkedUserCtx, "master", httpContext.Username+":master"))
-                       rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
-                       mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
-               })
-
-               t.Run("PushCreate", doPushCreate(httpContext, u))
-       })
-       t.Run("SSH", func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-               sshContext := baseAPITestContext
-               sshContext.Reponame = "repo-tmp-18"
-               keyname := "my-testing-key"
-               forkedUserCtx.Reponame = sshContext.Reponame
-               t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false))
-               t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, sshContext.Username, perm.AccessModeRead))
-               t.Run("ForkFromDifferentUser", doAPIForkRepository(sshContext, forkedUserCtx.Username))
-
-               // Setup key the user ssh key
-               withKeyFile(t, keyname, func(keyFile string) {
-                       t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile))
-
-                       // Setup remote link
-                       // TODO: get url from api
-                       sshURL := createSSHUrl(sshContext.GitPath(), u)
-
-                       // Setup clone folder
-                       dstPath := t.TempDir()
-
-                       t.Run("Clone", doGitClone(dstPath, sshURL))
-
-                       little, big := standardCommitAndPushTest(t, dstPath)
-                       littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath)
-                       rawTest(t, &sshContext, little, big, littleLFS, bigLFS)
-                       mediaTest(t, &sshContext, little, big, littleLFS, bigLFS)
-
-                       t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "test/head2"))
-                       t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath))
-                       t.Run("MergeFork", func(t *testing.T) {
-                               defer tests.PrintCurrentTest(t)()
-                               t.Run("CreatePRAndMerge", doMergeFork(sshContext, forkedUserCtx, "master", sshContext.Username+":master"))
-                               rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
-                               mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
-                       })
-
-                       t.Run("PushCreate", doPushCreate(sshContext, sshURL))
-               })
-       })
-}
-
-func ensureAnonymousClone(t *testing.T, u *url.URL) {
-       dstLocalPath := t.TempDir()
-       t.Run("CloneAnonymous", doGitClone(dstLocalPath, u))
-}
-
-func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string) {
-       t.Run("Standard", func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-               little, big = commitAndPushTest(t, dstPath, "data-file-")
-       })
-       return little, big
-}
-
-func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) {
-       t.Run("LFS", func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-               prefix := "lfs-data-file-"
-               err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("install").Run(&git.RunOpts{Dir: dstPath})
-               assert.NoError(t, err)
-               _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("track").AddDynamicArguments(prefix + "*").RunStdString(&git.RunOpts{Dir: dstPath})
-               assert.NoError(t, err)
-               err = git.AddChanges(dstPath, false, ".gitattributes")
-               assert.NoError(t, err)
-
-               err = git.CommitChangesWithArgs(dstPath, git.AllowLFSFiltersArgs(), git.CommitChangesOptions{
-                       Committer: &git.Signature{
-                               Email: "user2@example.com",
-                               Name:  "User Two",
-                               When:  time.Now(),
-                       },
-                       Author: &git.Signature{
-                               Email: "user2@example.com",
-                               Name:  "User Two",
-                               When:  time.Now(),
-                       },
-                       Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
-               })
-               assert.NoError(t, err)
-
-               littleLFS, bigLFS = commitAndPushTest(t, dstPath, prefix)
-
-               t.Run("Locks", func(t *testing.T) {
-                       defer tests.PrintCurrentTest(t)()
-                       lockTest(t, dstPath)
-               })
-       })
-       return littleLFS, bigLFS
-}
-
-func commitAndPushTest(t *testing.T, dstPath, prefix string) (little, big string) {
-       t.Run("PushCommit", func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-               t.Run("Little", func(t *testing.T) {
-                       defer tests.PrintCurrentTest(t)()
-                       little = doCommitAndPush(t, littleSize, dstPath, prefix)
-               })
-               t.Run("Big", func(t *testing.T) {
-                       if testing.Short() {
-                               t.Skip("Skipping test in short mode.")
-                               return
-                       }
-                       defer tests.PrintCurrentTest(t)()
-                       big = doCommitAndPush(t, bigSize, dstPath, prefix)
-               })
-       })
-       return little, big
-}
-
-func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) {
-       t.Run("Raw", func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-               username := ctx.Username
-               reponame := ctx.Reponame
-
-               session := loginUser(t, username)
-
-               // Request raw paths
-               req := NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", little))
-               resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
-               assert.Equal(t, littleSize, resp.Length)
-
-               if setting.LFS.StartServer {
-                       req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS))
-                       resp := session.MakeRequest(t, req, http.StatusOK)
-                       assert.NotEqual(t, littleSize, resp.Body.Len())
-                       assert.LessOrEqual(t, resp.Body.Len(), 1024)
-                       if resp.Body.Len() != littleSize && resp.Body.Len() <= 1024 {
-                               assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier)
-                       }
-               }
-
-               if !testing.Short() {
-                       req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big))
-                       resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
-                       assert.Equal(t, bigSize, resp.Length)
-
-                       if setting.LFS.StartServer {
-                               req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS))
-                               resp := session.MakeRequest(t, req, http.StatusOK)
-                               assert.NotEqual(t, bigSize, resp.Body.Len())
-                               if resp.Body.Len() != bigSize && resp.Body.Len() <= 1024 {
-                                       assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier)
-                               }
-                       }
-               }
-       })
-}
-
-func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) {
-       t.Run("Media", func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-
-               username := ctx.Username
-               reponame := ctx.Reponame
-
-               session := loginUser(t, username)
-
-               // Request media paths
-               req := NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", little))
-               resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
-               assert.Equal(t, littleSize, resp.Length)
-
-               req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS))
-               resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
-               assert.Equal(t, littleSize, resp.Length)
-
-               if !testing.Short() {
-                       req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big))
-                       resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
-                       assert.Equal(t, bigSize, resp.Length)
-
-                       if setting.LFS.StartServer {
-                               req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS))
-                               resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
-                               assert.Equal(t, bigSize, resp.Length)
-                       }
-               }
-       })
-}
-
-func lockTest(t *testing.T, repoPath string) {
-       lockFileTest(t, "README.md", repoPath)
-}
-
-func lockFileTest(t *testing.T, filename, repoPath string) {
-       _, _, err := git.NewCommand(git.DefaultContext, "lfs").AddArguments("locks").RunStdString(&git.RunOpts{Dir: repoPath})
-       assert.NoError(t, err)
-       _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("lock").AddDynamicArguments(filename).RunStdString(&git.RunOpts{Dir: repoPath})
-       assert.NoError(t, err)
-       _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("locks").RunStdString(&git.RunOpts{Dir: repoPath})
-       assert.NoError(t, err)
-       _, _, err = git.NewCommand(git.DefaultContext, "lfs").AddArguments("unlock").AddDynamicArguments(filename).RunStdString(&git.RunOpts{Dir: repoPath})
-       assert.NoError(t, err)
-}
-
-func doCommitAndPush(t *testing.T, size int, repoPath, prefix string) string {
-       name, err := generateCommitWithNewData(size, repoPath, "user2@example.com", "User Two", prefix)
-       assert.NoError(t, err)
-       _, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "master").RunStdString(&git.RunOpts{Dir: repoPath}) // Push
-       assert.NoError(t, err)
-       return name
-}
-
-func generateCommitWithNewData(size int, repoPath, email, fullName, prefix string) (string, error) {
-       // Generate random file
-       bufSize := 4 * 1024
-       if bufSize > size {
-               bufSize = size
-       }
-
-       buffer := make([]byte, bufSize)
-
-       tmpFile, err := os.CreateTemp(repoPath, prefix)
-       if err != nil {
-               return "", err
-       }
-       defer tmpFile.Close()
-       written := 0
-       for written < size {
-               n := size - written
-               if n > bufSize {
-                       n = bufSize
-               }
-               _, err := rand.Read(buffer[:n])
-               if err != nil {
-                       return "", err
-               }
-               n, err = tmpFile.Write(buffer[:n])
-               if err != nil {
-                       return "", err
-               }
-               written += n
-       }
-
-       // Commit
-       // Now here we should explicitly allow lfs filters to run
-       globalArgs := git.AllowLFSFiltersArgs()
-       err = git.AddChangesWithArgs(repoPath, globalArgs, false, filepath.Base(tmpFile.Name()))
-       if err != nil {
-               return "", err
-       }
-       err = git.CommitChangesWithArgs(repoPath, globalArgs, git.CommitChangesOptions{
-               Committer: &git.Signature{
-                       Email: email,
-                       Name:  fullName,
-                       When:  time.Now(),
-               },
-               Author: &git.Signature{
-                       Email: email,
-                       Name:  fullName,
-                       When:  time.Now(),
-               },
-               Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
-       })
-       return filepath.Base(tmpFile.Name()), err
-}
-
-func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
-       return func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-               t.Run("CreateBranchProtected", doGitCreateBranch(dstPath, "protected"))
-               t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
-
-               ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
-
-               // Protect branch without any whitelisting
-               t.Run("ProtectBranchNoWhitelist", func(t *testing.T) {
-                       doProtectBranch(ctx, "protected", "", "", "")
-               })
-
-               // Try to push without permissions, which should fail
-               t.Run("TryPushWithoutPermissions", func(t *testing.T) {
-                       _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
-                       assert.NoError(t, err)
-                       doGitPushTestRepositoryFail(dstPath, "origin", "protected")
-               })
-
-               // Set up permissions for normal push but not force push
-               t.Run("SetupNormalPushPermissions", func(t *testing.T) {
-                       doProtectBranch(ctx, "protected", baseCtx.Username, "", "")
-               })
-
-               // Normal push should work
-               t.Run("NormalPushWithPermissions", func(t *testing.T) {
-                       _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
-                       assert.NoError(t, err)
-                       doGitPushTestRepository(dstPath, "origin", "protected")
-               })
-
-               // Try to force push without force push permissions, which should fail
-               t.Run("ForcePushWithoutForcePermissions", func(t *testing.T) {
-                       t.Run("CreateDivergentHistory", func(t *testing.T) {
-                               git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath})
-                               _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-new")
-                               assert.NoError(t, err)
-                       })
-                       doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")
-               })
-
-               // Set up permissions for force push but not normal push
-               t.Run("SetupForcePushPermissions", func(t *testing.T) {
-                       doProtectBranch(ctx, "protected", "", baseCtx.Username, "")
-               })
-
-               // Try to force push without normal push permissions, which should fail
-               t.Run("ForcePushWithoutNormalPermissions", func(t *testing.T) {
-                       doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")
-               })
-
-               // Set up permissions for normal and force push (both are required to force push)
-               t.Run("SetupNormalAndForcePushPermissions", func(t *testing.T) {
-                       doProtectBranch(ctx, "protected", baseCtx.Username, baseCtx.Username, "")
-               })
-
-               // Force push should now work
-               t.Run("ForcePushWithPermissions", func(t *testing.T) {
-                       doGitPushTestRepository(dstPath, "-f", "origin", "protected")
-               })
-
-               t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", "", ""))
-               t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected"))
-               var pr api.PullRequest
-               var err error
-               t.Run("CreatePullRequest", func(t *testing.T) {
-                       pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected")(t)
-                       assert.NoError(t, err)
-               })
-               t.Run("GenerateCommit", func(t *testing.T) {
-                       _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
-                       assert.NoError(t, err)
-               })
-               t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected-2"))
-               var pr2 api.PullRequest
-               t.Run("CreatePullRequest", func(t *testing.T) {
-                       pr2, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "unprotected", "unprotected-2")(t)
-                       assert.NoError(t, err)
-               })
-               t.Run("MergePR2", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr2.Index))
-               t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
-               t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
-
-               t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "", "unprotected-file-*"))
-               t.Run("GenerateCommit", func(t *testing.T) {
-                       _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "unprotected-file-")
-                       assert.NoError(t, err)
-               })
-               t.Run("PushUnprotectedFilesToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
-
-               t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, "", ""))
-
-               t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master"))
-               t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce"))
-               t.Run("GenerateCommit", func(t *testing.T) {
-                       _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
-                       assert.NoError(t, err)
-               })
-               t.Run("FailToForcePushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "toforce:protected"))
-               t.Run("MergeProtectedToToforce", doGitMerge(dstPath, "protected"))
-               t.Run("PushToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "toforce:protected"))
-               t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master"))
-       }
-}
-
-func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns string) func(t *testing.T) {
-       // We are going to just use the owner to set the protection.
-       return func(t *testing.T) {
-               csrf := GetUserCSRFToken(t, ctx.Session)
-
-               formData := map[string]string{
-                       "_csrf":                     csrf,
-                       "rule_name":                 branch,
-                       "unprotected_file_patterns": unprotectedFilePatterns,
-               }
-
-               if userToWhitelistPush != "" {
-                       user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistPush)
-                       assert.NoError(t, err)
-                       formData["whitelist_users"] = strconv.FormatInt(user.ID, 10)
-                       formData["enable_push"] = "whitelist"
-                       formData["enable_whitelist"] = "on"
-               }
-
-               if userToWhitelistForcePush != "" {
-                       user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistForcePush)
-                       assert.NoError(t, err)
-                       formData["force_push_allowlist_users"] = strconv.FormatInt(user.ID, 10)
-                       formData["enable_force_push"] = "whitelist"
-                       formData["enable_force_push_allowlist"] = "on"
-               }
-
-               // Send the request to update branch protection settings
-               req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), formData)
-               ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
-
-               // Check if master branch has been locked successfully
-               flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash)
-               assert.NotNil(t, flashCookie)
-               assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2522"+url.QueryEscape(branch)+"%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
-       }
-}
-
-func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) func(t *testing.T) {
-       return func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-               var pr api.PullRequest
-               var err error
-
-               // Create a test pullrequest
-               t.Run("CreatePullRequest", func(t *testing.T) {
-                       pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t)
-                       assert.NoError(t, err)
-               })
-
-               // Ensure the PR page works
-               t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
-
-               // Then get the diff string
-               var diffHash string
-               var diffLength int
-               t.Run("GetDiff", func(t *testing.T) {
-                       req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(baseCtx.Username), url.PathEscape(baseCtx.Reponame), pr.Index))
-                       resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK)
-                       diffHash = string(resp.Hash.Sum(nil))
-                       diffLength = resp.Length
-               })
-
-               // Now: Merge the PR & make sure that doesn't break the PR page or change its diff
-               t.Run("MergePR", doAPIMergePullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index))
-               t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
-               t.Run("CheckPR", func(t *testing.T) {
-                       oldMergeBase := pr.MergeBase
-                       pr2, err := doAPIGetPullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
-                       assert.NoError(t, err)
-                       assert.Equal(t, oldMergeBase, pr2.MergeBase)
-               })
-               t.Run("EnsurDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
-
-               // Then: Delete the head branch & make sure that doesn't break the PR page or change its diff
-               t.Run("DeleteHeadBranch", doBranchDelete(baseCtx, baseCtx.Username, baseCtx.Reponame, headBranch))
-               t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
-               t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
-
-               // Delete the head repository & make sure that doesn't break the PR page or change its diff
-               t.Run("DeleteHeadRepository", doAPIDeleteRepository(ctx))
-               t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
-               t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
-       }
-}
-
-func doCreatePRAndSetManuallyMerged(ctx, baseCtx APITestContext, dstPath, baseBranch, headBranch string) func(t *testing.T) {
-       return func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-               var (
-                       pr           api.PullRequest
-                       err          error
-                       lastCommitID string
-               )
-
-               trueBool := true
-               falseBool := false
-
-               t.Run("AllowSetManuallyMergedAndSwitchOffAutodetectManualMerge", doAPIEditRepository(baseCtx, &api.EditRepoOption{
-                       HasPullRequests:       &trueBool,
-                       AllowManualMerge:      &trueBool,
-                       AutodetectManualMerge: &falseBool,
-               }))
-
-               t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
-               t.Run("PushToHeadBranch", doGitPushTestRepository(dstPath, "origin", headBranch))
-               t.Run("CreateEmptyPullRequest", func(t *testing.T) {
-                       pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t)
-                       assert.NoError(t, err)
-               })
-               lastCommitID = pr.Base.Sha
-               t.Run("ManuallyMergePR", doAPIManuallyMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, lastCommitID, pr.Index))
-       }
-}
-
-func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest) func(t *testing.T) {
-       return func(t *testing.T) {
-               req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
-               ctx.Session.MakeRequest(t, req, http.StatusOK)
-               req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/files", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
-               ctx.Session.MakeRequest(t, req, http.StatusOK)
-               req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
-               ctx.Session.MakeRequest(t, req, http.StatusOK)
-       }
-}
-
-func doEnsureDiffNoChange(ctx APITestContext, pr api.PullRequest, diffHash string, diffLength int) func(t *testing.T) {
-       return func(t *testing.T) {
-               req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
-               resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK)
-               actual := string(resp.Hash.Sum(nil))
-               actualLength := resp.Length
-
-               equal := diffHash == actual
-               assert.True(t, equal, "Unexpected change in the diff string: expected hash: %s size: %d but was actually: %s size: %d", hex.EncodeToString([]byte(diffHash)), diffLength, hex.EncodeToString([]byte(actual)), actualLength)
-       }
-}
-
-func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) {
-       return func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-
-               // create a context for a currently non-existent repository
-               ctx.Reponame = fmt.Sprintf("repo-tmp-push-create-%s", u.Scheme)
-               u.Path = ctx.GitPath()
-
-               // Create a temporary directory
-               tmpDir := t.TempDir()
-
-               // Now create local repository to push as our test and set its origin
-               t.Run("InitTestRepository", doGitInitTestRepository(tmpDir))
-               t.Run("AddRemote", doGitAddRemote(tmpDir, "origin", u))
-
-               // Disable "Push To Create" and attempt to push
-               setting.Repository.EnablePushCreateUser = false
-               t.Run("FailToPushAndCreateTestRepository", doGitPushTestRepositoryFail(tmpDir, "origin", "master"))
-
-               // Enable "Push To Create"
-               setting.Repository.EnablePushCreateUser = true
-
-               // Assert that cloning from a non-existent repository does not create it and that it definitely wasn't create above
-               t.Run("FailToCloneFromNonExistentRepository", doGitCloneFail(u))
-
-               // Then "Push To Create"x
-               t.Run("SuccessfullyPushAndCreateTestRepository", doGitPushTestRepository(tmpDir, "origin", "master"))
-
-               // Finally, fetch repo from database and ensure the correct repository has been created
-               repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ctx.Username, ctx.Reponame)
-               assert.NoError(t, err)
-               assert.False(t, repo.IsEmpty)
-               assert.True(t, repo.IsPrivate)
-
-               // Now add a remote that is invalid to "Push To Create"
-               invalidCtx := ctx
-               invalidCtx.Reponame = fmt.Sprintf("invalid/repo-tmp-push-create-%s", u.Scheme)
-               u.Path = invalidCtx.GitPath()
-               t.Run("AddInvalidRemote", doGitAddRemote(tmpDir, "invalid", u))
-
-               // Fail to "Push To Create" the invalid
-               t.Run("FailToPushAndCreateInvalidTestRepository", doGitPushTestRepositoryFail(tmpDir, "invalid", "master"))
-       }
-}
-
-func doBranchDelete(ctx APITestContext, owner, repo, branch string) func(*testing.T) {
-       return func(t *testing.T) {
-               csrf := GetUserCSRFToken(t, ctx.Session)
-
-               req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/branches/delete?name=%s", url.PathEscape(owner), url.PathEscape(repo), url.QueryEscape(branch)), map[string]string{
-                       "_csrf": csrf,
-               })
-               ctx.Session.MakeRequest(t, req, http.StatusOK)
-       }
-}
-
-func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
-       return func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-
-               ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
-
-               t.Run("CheckoutProtected", doGitCheckoutBranch(dstPath, "protected"))
-               t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
-               t.Run("GenerateCommit", func(t *testing.T) {
-                       _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
-                       assert.NoError(t, err)
-               })
-               t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected3"))
-               var pr api.PullRequest
-               var err error
-               t.Run("CreatePullRequest", func(t *testing.T) {
-                       pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected3")(t)
-                       assert.NoError(t, err)
-               })
-
-               // Request repository commits page
-               req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", baseCtx.Username, baseCtx.Reponame, pr.Index))
-               resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
-               doc := NewHTMLParser(t, resp.Body)
-
-               // Get first commit URL
-               commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
-               assert.True(t, exists)
-               assert.NotEmpty(t, commitURL)
-
-               commitID := path.Base(commitURL)
-
-               addCommitStatus := func(status api.CommitStatusState) func(*testing.T) {
-                       return doAPICreateCommitStatus(ctx, commitID, api.CreateStatusOption{
-                               State:       status,
-                               TargetURL:   "http://test.ci/",
-                               Description: "",
-                               Context:     "testci",
-                       })
-               }
-
-               // Call API to add Pending status for commit
-               t.Run("CreateStatus", addCommitStatus(api.CommitStatusPending))
-
-               // Cancel not existing auto merge
-               ctx.ExpectedCode = http.StatusNotFound
-               t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
-
-               // Add auto merge request
-               ctx.ExpectedCode = http.StatusCreated
-               t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
-
-               // Can not create schedule twice
-               ctx.ExpectedCode = http.StatusConflict
-               t.Run("AutoMergePRTwice", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
-
-               // Cancel auto merge request
-               ctx.ExpectedCode = http.StatusNoContent
-               t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
-
-               // Add auto merge request
-               ctx.ExpectedCode = http.StatusCreated
-               t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
-
-               // Check pr status
-               ctx.ExpectedCode = 0
-               pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
-               assert.NoError(t, err)
-               assert.False(t, pr.HasMerged)
-
-               // Call API to add Failure status for commit
-               t.Run("CreateStatus", addCommitStatus(api.CommitStatusFailure))
-
-               // Check pr status
-               pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
-               assert.NoError(t, err)
-               assert.False(t, pr.HasMerged)
-
-               // Call API to add Success status for commit
-               t.Run("CreateStatus", addCommitStatus(api.CommitStatusSuccess))
-
-               // wait to let gitea merge stuff
-               time.Sleep(time.Second)
-
-               // test pr status
-               pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
-               assert.NoError(t, err)
-               assert.True(t, pr.HasMerged)
-       }
-}
-
-func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string) func(t *testing.T) {
-       return func(t *testing.T) {
-               defer tests.PrintCurrentTest(t)()
-
-               // skip this test if git version is low
-               if !git.DefaultFeatures().SupportProcReceive {
-                       return
-               }
-
-               gitRepo, err := git.OpenRepository(git.DefaultContext, dstPath)
-               if !assert.NoError(t, err) {
-                       return
-               }
-               defer gitRepo.Close()
-
-               var (
-                       pr1, pr2 *issues_model.PullRequest
-                       commit   string
-               )
-               repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ctx.Username, ctx.Reponame)
-               if !assert.NoError(t, err) {
-                       return
-               }
-
-               pullNum := unittest.GetCount(t, &issues_model.PullRequest{})
-
-               t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
-
-               t.Run("AddCommit", func(t *testing.T) {
-                       err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
-                       if !assert.NoError(t, err) {
-                               return
-                       }
-
-                       err = git.AddChanges(dstPath, true)
-                       assert.NoError(t, err)
-
-                       err = git.CommitChanges(dstPath, git.CommitChangesOptions{
-                               Committer: &git.Signature{
-                                       Email: "user2@example.com",
-                                       Name:  "user2",
-                                       When:  time.Now(),
-                               },
-                               Author: &git.Signature{
-                                       Email: "user2@example.com",
-                                       Name:  "user2",
-                                       When:  time.Now(),
-                               },
-                               Message: "Testing commit 1",
-                       })
-                       assert.NoError(t, err)
-                       commit, err = gitRepo.GetRefCommitID("HEAD")
-                       assert.NoError(t, err)
-               })
-
-               t.Run("Push", func(t *testing.T) {
-                       err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + headBranch).Run(&git.RunOpts{Dir: dstPath})
-                       if !assert.NoError(t, err) {
-                               return
-                       }
-                       unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+1)
-                       pr1 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
-                               HeadRepoID: repo.ID,
-                               Flow:       issues_model.PullRequestFlowAGit,
-                       })
-                       if !assert.NotEmpty(t, pr1) {
-                               return
-                       }
-                       prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t)
-                       if !assert.NoError(t, err) {
-                               return
-                       }
-                       assert.Equal(t, "user2/"+headBranch, pr1.HeadBranch)
-                       assert.False(t, prMsg.HasMerged)
-                       assert.Contains(t, "Testing commit 1", prMsg.Body)
-                       assert.Equal(t, commit, prMsg.Head.Sha)
-
-                       _, _, err = git.NewCommand(git.DefaultContext, "push", "origin").AddDynamicArguments("HEAD:refs/for/master/test/" + headBranch).RunStdString(&git.RunOpts{Dir: dstPath})
-                       if !assert.NoError(t, err) {
-                               return
-                       }
-                       unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
-                       pr2 = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
-                               HeadRepoID: repo.ID,
-                               Index:      pr1.Index + 1,
-                               Flow:       issues_model.PullRequestFlowAGit,
-                       })
-                       if !assert.NotEmpty(t, pr2) {
-                               return
-                       }
-                       prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t)
-                       if !assert.NoError(t, err) {
-                               return
-                       }
-                       assert.Equal(t, "user2/test/"+headBranch, pr2.HeadBranch)
-                       assert.False(t, prMsg.HasMerged)
-               })
-
-               if pr1 == nil || pr2 == nil {
-                       return
-               }
-
-               t.Run("AddCommit2", func(t *testing.T) {
-                       err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content \n ## test content 2"), 0o666)
-                       if !assert.NoError(t, err) {
-                               return
-                       }
-
-                       err = git.AddChanges(dstPath, true)
-                       assert.NoError(t, err)
-
-                       err = git.CommitChanges(dstPath, git.CommitChangesOptions{
-                               Committer: &git.Signature{
-                                       Email: "user2@example.com",
-                                       Name:  "user2",
-                                       When:  time.Now(),
-                               },
-                               Author: &git.Signature{
-                                       Email: "user2@example.com",
-                                       Name:  "user2",
-                                       When:  time.Now(),
-                               },
-                               Message: "Testing commit 2",
-                       })
-                       assert.NoError(t, err)
-                       commit, err = gitRepo.GetRefCommitID("HEAD")
-                       assert.NoError(t, err)
-               })
-
-               t.Run("Push2", func(t *testing.T) {
-                       err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + headBranch).Run(&git.RunOpts{Dir: dstPath})
-                       if !assert.NoError(t, err) {
-                               return
-                       }
-                       unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
-                       prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t)
-                       if !assert.NoError(t, err) {
-                               return
-                       }
-                       assert.False(t, prMsg.HasMerged)
-                       assert.Equal(t, commit, prMsg.Head.Sha)
-
-                       _, _, err = git.NewCommand(git.DefaultContext, "push", "origin").AddDynamicArguments("HEAD:refs/for/master/test/" + headBranch).RunStdString(&git.RunOpts{Dir: dstPath})
-                       if !assert.NoError(t, err) {
-                               return
-                       }
-                       unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
-                       prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t)
-                       if !assert.NoError(t, err) {
-                               return
-                       }
-                       assert.False(t, prMsg.HasMerged)
-                       assert.Equal(t, commit, prMsg.Head.Sha)
-               })
-               t.Run("Merge", doAPIMergePullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index))
-               t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master"))
-       }
-}
-
-func TestDataAsync_Issue29101(t *testing.T) {
-       onGiteaRun(t, func(t *testing.T, u *url.URL) {
-               user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
-               repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
-
-               resp, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{
-                       Files: []*files_service.ChangeRepoFile{
-                               {
-                                       Operation:     "create",
-                                       TreePath:      "test.txt",
-                                       ContentReader: bytes.NewReader(make([]byte, 10000)),
-                               },
-                       },
-                       OldBranch: repo.DefaultBranch,
-                       NewBranch: repo.DefaultBranch,
-               })
-               assert.NoError(t, err)
-
-               sha := resp.Commit.SHA
-
-               gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo)
-               assert.NoError(t, err)
-
-               commit, err := gitRepo.GetCommit(sha)
-               assert.NoError(t, err)
-
-               entry, err := commit.GetTreeEntryByPath("test.txt")
-               assert.NoError(t, err)
-
-               b := entry.Blob()
-
-               r, err := b.DataAsync()
-               assert.NoError(t, err)
-               defer r.Close()
-
-               r2, err := b.DataAsync()
-               assert.NoError(t, err)
-               defer r2.Close()
-       })
-}
-
-func TestAgitPullPush(t *testing.T) {
-       onGiteaRun(t, func(t *testing.T, u *url.URL) {
-               baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
-
-               u.Path = baseAPITestContext.GitPath()
-               u.User = url.UserPassword("user2", userPassword)
-
-               dstPath := t.TempDir()
-               doGitClone(dstPath, u)(t)
-
-               gitRepo, err := git.OpenRepository(context.Background(), dstPath)
-               assert.NoError(t, err)
-               defer gitRepo.Close()
-
-               doGitCreateBranch(dstPath, "test-agit-push")
-
-               // commit 1
-               _, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
-               assert.NoError(t, err)
-
-               // push to create an agit pull request
-               err = git.NewCommand(git.DefaultContext, "push", "origin",
-                       "-o", "title=test-title", "-o", "description=test-description",
-                       "HEAD:refs/for/master/test-agit-push",
-               ).Run(&git.RunOpts{Dir: dstPath})
-               assert.NoError(t, err)
-
-               // check pull request exist
-               pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: 1, Flow: issues_model.PullRequestFlowAGit, HeadBranch: "user2/test-agit-push"})
-               assert.NoError(t, pr.LoadIssue(db.DefaultContext))
-               assert.Equal(t, "test-title", pr.Issue.Title)
-               assert.Equal(t, "test-description", pr.Issue.Content)
-
-               // commit 2
-               _, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-2-")
-               assert.NoError(t, err)
-
-               // push 2
-               err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").Run(&git.RunOpts{Dir: dstPath})
-               assert.NoError(t, err)
-
-               // reset to first commit
-               err = git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath})
-               assert.NoError(t, err)
-
-               // test force push without confirm
-               _, stderr, err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").RunStdString(&git.RunOpts{Dir: dstPath})
-               assert.Error(t, err)
-               assert.Contains(t, stderr, "[remote rejected] HEAD -> refs/for/master/test-agit-push (request `force-push` push option)")
-
-               // test force push with confirm
-               err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push", "-o", "force-push").Run(&git.RunOpts{Dir: dstPath})
-               assert.NoError(t, err)
-       })
-}
index e6ce3cce0e0e210a7358d3883057d087abb84a52..3503ca1975826d98220e9d194617ef92b3e27d7e 100644 (file)
@@ -9,7 +9,6 @@ import (
        "database/sql"
        "fmt"
        "os"
-       "path"
        "path/filepath"
        "testing"
 
@@ -53,7 +52,7 @@ func InitTest(requireGitea bool) {
                if setting.IsWindows {
                        giteaBinary += ".exe"
                }
-               setting.AppPath = path.Join(giteaRoot, giteaBinary)
+               setting.AppPath = filepath.Join(giteaRoot, giteaBinary)
                if _, err := os.Stat(setting.AppPath); err != nil {
                        exitf("Could not find gitea binary at %s", setting.AppPath)
                }
@@ -70,7 +69,7 @@ func InitTest(requireGitea bool) {
                        exitf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify`)
                }
        }
-       if !path.IsAbs(giteaConf) {
+       if !filepath.IsAbs(giteaConf) {
                setting.CustomConf = filepath.Join(giteaRoot, giteaConf)
        } else {
                setting.CustomConf = giteaConf
@@ -193,8 +192,12 @@ func PrepareAttachmentsStorage(t testing.TB) {
 }
 
 func PrepareGitRepoDirectory(t testing.TB) {
+       if !assert.NotEmpty(t, setting.RepoRootPath) {
+               return
+       }
+
        assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
-       assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
+       assert.NoError(t, unittest.CopyDir(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
 
        ownerDirs, err := os.ReadDir(setting.RepoRootPath)
        if err != nil {