merge of upstream/main + limitsize w/o DB

This commit is contained in:
DmitryFrolovTri committed 2026-01-22 11:19:26 +00:00
commit ca9489a137
23 files changed
+457 -862

No files matched your search

@@ -468,33 +468,3 @@ func doAPIAddRepoToOrganizationTeam(ctx APITestContext, teamID int64, orgName, r
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
}
}
func doAPISetRepoSizeLimit(ctx APITestContext, owner, repo string, size int64) func(*testing.T) {
return func(t *testing.T) {
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s",
owner, repo)
req := NewRequestWithJSON(t, http.MethodPatch, urlStr, &api.EditRepoOption{SizeLimit: &size}).
AddTokenAuth(ctx.Token)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
ctx.Session.MakeRequest(t, req, 200)
}
}
func doAPISetRepoLFSSizeLimit(ctx APITestContext, owner, repo string, size int64) func(*testing.T) {
return func(t *testing.T) {
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s",
owner, repo)
req := NewRequestWithJSON(t, http.MethodPatch, urlStr, &api.EditRepoOption{LFSSizeLimit: &size}).
AddTokenAuth(ctx.Token)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
ctx.Session.MakeRequest(t, req, 200)
}
}
-33
View File
@@ -85,39 +85,6 @@ func testGitGeneral(t *testing.T, u *url.URL) {
rawTest(t, &httpContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
mediaTest(t, &httpContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
t.Run("SizeLimit", func(t *testing.T) {
dstForkedPath := t.TempDir()
setting.SaveGlobalRepositorySetting(0, 0, false)
t.Run("Under", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
doCommitAndPush(t, testFileSizeSmall, dstPath, "data-file-")
})
t.Run("Over", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
u.Path = forkedUserCtx.GitPath()
u.User = url.UserPassword(forkedUserCtx.Username, userPassword)
t.Run("Clone", doGitClone(dstForkedPath, u))
t.Run("APISetRepoSizeLimit", doAPISetRepoSizeLimit(forkedUserCtx, forkedUserCtx.Username, forkedUserCtx.Reponame, testFileSizeSmall))
doCommitAndPushWithExpectedError(t, testFileSizeLarge, dstForkedPath, "data-file-")
})
t.Run("UnderAfterResize", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
t.Run("APISetRepoSizeLimit", doAPISetRepoSizeLimit(forkedUserCtx, forkedUserCtx.Username, forkedUserCtx.Reponame, testFileSizeLarge*10))
doCommitAndPush(t, testFileSizeSmall, dstPath, "data-file-")
})
t.Run("Deletion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
doCommitAndPush(t, testFileSizeSmall, dstPath, "data-file-")
bigFileName := doCommitAndPush(t, testFileSizeLarge, dstPath, "data-file-")
oldRepoSize := doGetRemoteRepoSizeViaAPI(t, forkedUserCtx)
lastCommitID := doGetAddCommitID(t, dstPath, bigFileName)
doDeleteAndPush(t, dstPath, bigFileName)
doRebaseCommitAndPush(t, dstPath, lastCommitID)
newRepoSize := doGetRemoteRepoSizeViaAPI(t, forkedUserCtx)
assert.LessOrEqual(t, newRepoSize, oldRepoSize)
setting.SaveGlobalRepositorySetting(-1, -1, false)
})
})
t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "test/head"))
t.Run("CreateProtectedBranch", doCreateProtectedBranch(&httpContext, dstPath))
t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))
-411
View File
@@ -1,411 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/url"
"os"
"path/filepath"
"testing"
"time"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
func base62(n int64) string {
if n == 0 {
return string(alphabet[0])
}
var buf [11]byte
i := len(buf)
for n > 0 {
i--
buf[i] = alphabet[n%62]
n /= 62
}
return string(buf[i:])
}
// ID5: 5-char ID derived from unix seconds.
func ID5() string {
sec := time.Now().Unix()
s := base62(sec)
if len(s) < 5 {
return fmt.Sprintf("%05s", s)
}
return s[len(s)-5:]
}
// TestLFSSizeLimit exercises the pre-receive size limit logic.
// Runs sequentially (no t.Parallel) because it mutates GLOBAL settings.
//
// Enable/disable via:
//
// setting.SaveGlobalRepositorySetting(repoLimit, lfsLimit, lfsSizeInRepoSize)
//
// Scenarios:
// - repo-only limit: blocks git blobs but not LFS objects (when lfsSizeInRepoSize=false, lfsLimit=0)
// - LFS-only limit: blocks LFS objects but not git blobs
// - combined repo+LFS (LFS counted into repo size): blocks LFS when it would exceed repo limit
// - per-repo LFS override wins over global default (retry same push after increasing per-repo LFS limit)
func TestLFSSizeLimit(t *testing.T) {
onGiteaRun(t, testLFSSizeLimit)
}
func testLFSSizeLimit(t *testing.T, baseURL *url.URL) {
// Always disable at the end so we don't leak to other integration tests.
t.Cleanup(func() {
setting.SaveGlobalRepositorySetting(-1, -1, false)
})
if !setting.LFS.StartServer {
t.Skip("LFS server disabled")
}
// ---- sizes ----
// Repo-size checks are not precise (compression), so use very different sizes vs limit.
const (
repoLimitBytes = int64(64 * 1024) // 64KiB
gitUnderBytes = int(8 * 1024) // 8KiB
gitOverBytes = int(4 * 1024 * 1024) // 4MiB (far above limit)
lfsBigBytes = int(1 * 1024 * 1024) // 1MiB (should still pass repo-only when not counted)
)
// LFS-only checks are precise.
const (
lfsLimitBytes = int64(64 * 1024) // 64KiB
lfsUnderBytes = int(32 * 1024) // 32KiB
lfsOverBytes = int(128 * 1024) // 128KiB
)
// Combined repo+LFS (LFS counted into repo size).
const (
combinedRepoLimitBytes = int64(128 * 1024) // 128KiB
combinedLFSUnderBytes = int(64 * 1024) // 64KiB
combinedLFSOverBytes = int(512 * 1024) // 512KiB
)
// ---- helpers ----
newLimitRepo := func(t *testing.T, suffix string) APITestContext {
t.Helper()
// Make a unique repo name without relying on tests.GetTestUID / CreateRepo.
repoName := fmt.Sprintf(
"lfsl-%s-%s",
suffix,
ID5(),
)
ctx := NewAPITestContext(
t,
"user2",
repoName,
auth_model.AccessTokenScopeWriteRepository,
auth_model.AccessTokenScopeWriteUser,
)
// Create via API (same style as git_general_test.go)
doAPICreateRepository(ctx, false)(t)
return ctx
}
cloneHTTP := func(t *testing.T, ctx APITestContext) string {
t.Helper()
u := *baseURL // copy
u.Path = ctx.GitPath()
u.User = url.UserPassword(ctx.Username, userPassword)
dst := t.TempDir()
doGitClone(dst, &u)(t)
return dst
}
commitSignature := func() *git.Signature {
return &git.Signature{
Email: "user2@example.com",
Name: "User Two",
When: time.Now(),
}
}
configureLFSForPrefix := func(t *testing.T, repoPath, prefix string) {
t.Helper()
// git lfs install
err := gitcmd.NewCommand("lfs").AddArguments("install").WithDir(repoPath).Run(t.Context())
require.NoError(t, err)
// track prefix*
_, _, err = gitcmd.NewCommand("lfs").
AddArguments("track").
AddDynamicArguments(prefix + "*").
WithDir(repoPath).
RunStdString(t.Context())
require.NoError(t, err)
// commit .gitattributes
err = git.AddChanges(t.Context(), repoPath, false, ".gitattributes")
require.NoError(t, err)
sig := commitSignature()
err = git.CommitChanges(t.Context(), repoPath, git.CommitChangesOptions{
Committer: sig,
Author: sig,
Message: "configure LFS tracking",
})
require.NoError(t, err)
}
pushCurrentBranch := func(t *testing.T, repoPath string) error {
t.Helper()
_, _, err := gitcmd.NewCommand("push", "origin", "master").WithDir(repoPath).RunStdString(t.Context())
return err
}
// Push a git blob by creating a new random file (random-ish content reduces compression effects).
pushGitBlob := func(t *testing.T, ctx APITestContext, size int) error {
t.Helper()
repoPath := cloneHTTP(t, ctx)
_, err := generateCommitWithNewData(
t.Context(),
size,
repoPath,
"user2@example.com",
"User Two",
"git-data-",
)
require.NoError(t, err)
return pushCurrentBranch(t, repoPath)
}
// Push an LFS object by tracking prefix* and then committing a file created by generateCommitWithNewData.
pushLFSObjectOnce := func(t *testing.T, ctx APITestContext, size int) error {
t.Helper()
repoPath := cloneHTTP(t, ctx)
const prefix = "lfs-data-"
configureLFSForPrefix(t, repoPath, prefix)
_, err := generateCommitWithNewData(
t.Context(),
size,
repoPath,
"user2@example.com",
"User Two",
prefix,
)
require.NoError(t, err)
return pushCurrentBranch(t, repoPath)
}
// Prepare a single LFS commit in a clone and return the clone path, so we can:
// - push (expect fail)
// - change limits
// - push SAME COMMIT again (expect pass)
prepareLFSPushForRetry := func(t *testing.T, ctx APITestContext, size int) string {
t.Helper()
repoPath := cloneHTTP(t, ctx)
const prefix = "lfs-retry-"
configureLFSForPrefix(t, repoPath, prefix)
_, err := generateCommitWithNewData(
t.Context(),
size,
repoPath,
"user2@example.com",
"User Two",
prefix,
)
require.NoError(t, err)
// Ensure the repoPath is used (avoid accidental cleanup / unused).
_, err = os.Stat(filepath.Join(repoPath, ".git"))
require.NoError(t, err)
return repoPath
}
runGlobalThenPerRepo := func(
t *testing.T,
name string,
global func(t *testing.T),
perRepo func(t *testing.T),
) {
t.Helper()
t.Run(name+"/Global", func(t *testing.T) { global(t) })
t.Run(name+"/PerRepo", func(t *testing.T) { perRepo(t) })
}
// ---- tests ----
runGlobalThenPerRepo(t,
"GlobalAndRepoOnlyLimit_BlocksGitButNotLFS",
func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Global: repo limit ON, LFS limit OFF, LFS not counted into repo size.
setting.SaveGlobalRepositorySetting(repoLimitBytes, 0, false)
ctx := newLimitRepo(t, "ggit")
// push over on git (NOK)
err := pushGitBlob(t, ctx, gitOverBytes)
assert.Error(t, err, "git file well over repo limit should be rejected")
// push LFS (OK)
err = pushLFSObjectOnce(t, ctx, lfsBigBytes)
assert.NoError(t, err, "LFS push should be allowed when repo-only limit is enabled and LFSSizeInRepoSize=false")
// push under on git (OK)
err = pushGitBlob(t, ctx, gitUnderBytes)
assert.NoError(t, err, "small git file should be accepted if under repo limit")
},
func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
setting.SaveGlobalRepositorySetting(0, 0, false)
ctx := newLimitRepo(t, "rgit")
// set per-repo repo limit
t.Run("APISetRepoSizeLimit", doAPISetRepoSizeLimit(ctx, ctx.Username, ctx.Reponame, repoLimitBytes))
// push over on git (NOK)
err := pushGitBlob(t, ctx, gitOverBytes)
assert.Error(t, err, "git file well over per-repo repo limit should be rejected")
// push LFS (OK)
err = pushLFSObjectOnce(t, ctx, lfsBigBytes)
assert.NoError(t, err, "LFS push should be allowed when only per-repo repo limit is set and LFSSizeInRepoSize=false")
// push under on git (OK)
err = pushGitBlob(t, ctx, gitUnderBytes)
assert.NoError(t, err, "small git file should be accepted under per-repo repo limit")
},
)
runGlobalThenPerRepo(t,
"GlobalAndLFSOnlyLimit_BlocksLFSButNotGit",
func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Global: LFS limit ON, repo limit OFF, LFS not counted into repo size.
setting.SaveGlobalRepositorySetting(0, lfsLimitBytes, false)
ctx := newLimitRepo(t, "glfs")
// push on git (OK)
err := pushGitBlob(t, ctx, gitOverBytes)
assert.NoError(t, err, "git push should be allowed when repo limit is 0")
// push over on LFS (NOK)
err = pushLFSObjectOnce(t, ctx, lfsOverBytes)
assert.Error(t, err, "LFS push above global LFS limit must be rejected")
// push under on LFS (OK) (under is last)
err = pushLFSObjectOnce(t, ctx, lfsUnderBytes)
assert.NoError(t, err, "LFS push under global LFS limit must be accepted")
},
func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
setting.SaveGlobalRepositorySetting(0, 0, false)
ctx := newLimitRepo(t, "rlfs")
// set per-repo LFS limit
t.Run("APISetRepoLFSSizeLimit", doAPISetRepoLFSSizeLimit(ctx, ctx.Username, ctx.Reponame, lfsLimitBytes))
// push on git (OK)
err := pushGitBlob(t, ctx, gitOverBytes)
assert.NoError(t, err, "git push should be allowed when repo limit is 0")
// push over on LFS (NOK)
err = pushLFSObjectOnce(t, ctx, lfsOverBytes)
assert.Error(t, err, "LFS push above per-repo LFS limit must be rejected")
},
)
runGlobalThenPerRepo(t,
"GlobalOrCombinedRepoAndLFSLimits_BlocksLFSandGit",
func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Global: repo limit ON, no LFS-specific limit, but LFS counted into repo size.
setting.SaveGlobalRepositorySetting(combinedRepoLimitBytes, 0, true)
ctx := newLimitRepo(t, "gc")
// push over via LFS (NOK)
err := pushLFSObjectOnce(t, ctx, combinedLFSOverBytes)
assert.Error(t, err, "LFS push must be rejected when it would exceed repo limit and LFSSizeInRepoSize=true")
// push under via LFS (OK)
err = pushLFSObjectOnce(t, ctx, combinedLFSUnderBytes)
assert.NoError(t, err, "LFS push under repo limit must be accepted when LFSSizeInRepoSize=true")
},
func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Per-repo: enable checking globally with LFSSizeInRepoSize=true but no global limits.
setting.SaveGlobalRepositorySetting(0, 0, true)
ctx := newLimitRepo(t, "rc")
// set per-repo repo limit
t.Run("APISetRepoSizeLimit", doAPISetRepoSizeLimit(ctx, ctx.Username, ctx.Reponame, combinedRepoLimitBytes))
// push over via LFS (NOK)
err := pushLFSObjectOnce(t, ctx, combinedLFSOverBytes)
assert.Error(t, err, "LFS push must be rejected when it would exceed per-repo repo limit and LFSSizeInRepoSize=true")
// push under via LFS (OK)
err = pushLFSObjectOnce(t, ctx, combinedLFSUnderBytes)
assert.NoError(t, err, "LFS push under per-repo repo limit must be accepted when LFSSizeInRepoSize=true")
},
)
t.Run("PerRepoLFSOverride_WinsOverGlobalDefault", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Global: strict LFS limit, no repo limit.
setting.SaveGlobalRepositorySetting(-1, lfsLimitBytes, false)
ctx := newLimitRepo(t, "rwing")
// Prepare one LFS commit and keep the clone so we can retry pushing SAME commit.
repoPath := prepareLFSPushForRetry(t, ctx, lfsOverBytes)
// First push must fail.
err := pushCurrentBranch(t, repoPath)
assert.Error(t, err, "push must fail under strict global LFS limit")
// Increase per-repo LFS limit to allow the previously rejected object.
// Use a clearly larger limit than the file size.
newPerRepoLimit := int64(lfsOverBytes) * 4
t.Run("APISetRepoLFSSizeLimit", doAPISetRepoLFSSizeLimit(ctx, ctx.Username, ctx.Reponame, newPerRepoLimit))
// Retry pushing SAME commit must succeed now.
err = pushCurrentBranch(t, repoPath)
assert.NoError(t, err, "per-repo LFS limit must override global default and allow previously rejected push")
})
}
+338
View File
@@ -0,0 +1,338 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"crypto/rand"
"io"
"net/url"
"os"
"path"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestSizeLimit(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
t.Run("Git", func(t *testing.T) {
testGitSizeLimitInternal(t, u)
})
t.Run("LFS", func(t *testing.T) {
testLFSSizeLimitInternal(t, u)
})
})
}
func testGitSizeLimitInternal(t *testing.T, u *url.URL) {
username := "user2"
u.User = url.UserPassword(username, userPassword)
t.Run("Under", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
repoName := "repo-git-under"
ctx := NewAPITestContext(t, username, repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// Cleanup: reset limits and delete repository
defer func() {
setting.Repository.GitSizeMax = -1
setting.Repository.LFSSizeMax = -1
}()
defer doAPIDeleteRepository(ctx)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
// Phase 1: Push with no limit
setting.Repository.GitSizeMax = -1
setting.Repository.LFSSizeMax = -1
doCommitAndPush(t, 1024, dstPath, "under-phase1-")
// Phase 2: Push with limit enabled but not exceeded
setting.Repository.GitSizeMax = 50 * 1024 // 50 KiB
doCommitAndPush(t, 1024, dstPath, "under-phase2-")
})
t.Run("Over", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
repoName := "repo-git-over"
ctx := NewAPITestContext(t, username, repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// Cleanup: reset limits and delete repository
defer func() {
setting.Repository.GitSizeMax = -1
setting.Repository.LFSSizeMax = -1
}()
defer doAPIDeleteRepository(ctx)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
// Set restrictive limit and attempt push
setting.Repository.GitSizeMax = 100
setting.Repository.LFSSizeMax = -1
doCommitAndPushWithExpectedError(t, 1024, dstPath, "over-")
})
t.Run("UnderAfterResize", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
repoName := "repo-git-resize"
ctx := NewAPITestContext(t, username, repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// Cleanup: reset limits and delete repository
defer func() {
setting.Repository.GitSizeMax = -1
setting.Repository.LFSSizeMax = -1
}()
defer doAPIDeleteRepository(ctx)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
// Attempt push with restrictive limit - should fail
setting.Repository.GitSizeMax = 100
setting.Repository.LFSSizeMax = -1
doCommitAndPushWithExpectedError(t, 1024, dstPath, "resize-")
// Increase limit and retry same push - should succeed
setting.Repository.GitSizeMax = 30 * 1024 // 30 KiB
_, _, err := gitcmd.NewCommand("push", "origin", "master").WithDir(dstPath).RunStdString(t.Context())
assert.NoError(t, err, "Push should succeed after limit increase")
})
t.Run("DeletionAndSoftEnforcement", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
repoName := "repo-git-soft"
ctx := NewAPITestContext(t, username, repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// Cleanup: reset limits and delete repository
defer func() {
setting.Repository.GitSizeMax = -1
setting.Repository.LFSSizeMax = -1
}()
defer doAPIDeleteRepository(ctx)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
// Step 1: Push 1KB file with no limit
setting.Repository.GitSizeMax = -1
setting.Repository.LFSSizeMax = -1
doCommitAndPush(t, 1024, dstPath, "soft-base-")
// Step 2: Push 10KB file
doCommitAndPush(t, 10*1024, dstPath, "soft-big-")
// Step 3: Delete big file using reset
_, _, err := gitcmd.NewCommand("reset", "--hard", "HEAD~1").WithDir(dstPath).RunStdString(t.Context())
assert.NoError(t, err, "Reset should succeed")
// Step 4: Set very restrictive limit
setting.Repository.GitSizeMax = 10 // 10 bytes
// Step 5: Force push - should succeed (soft enforcement)
_, _, err = gitcmd.NewCommand("push", "--force-with-lease", "origin", "master").WithDir(dstPath).RunStdString(t.Context())
assert.NoError(t, err, "Force push should succeed with soft enforcement")
// Step 6: Try to push another 1KB file - should fail
doCommitAndPushWithExpectedError(t, 1024, dstPath, "soft-new-")
})
}
func testLFSSizeLimitInternal(t *testing.T, u *url.URL) {
if !setting.LFS.StartServer {
t.Skip("LFS server disabled")
}
username := "user2"
u.User = url.UserPassword(username, userPassword)
// Helper to track LFS
setupLFS := func(t *testing.T, dstPath string) {
err := os.WriteFile(path.Join(dstPath, ".gitattributes"), []byte("*.dat filter=lfs diff=lfs merge=lfs -text\n"), 0o644)
assert.NoError(t, err)
err = gitcmd.NewCommand("add", ".gitattributes").WithDir(dstPath).Run(t.Context())
assert.NoError(t, err)
err = gitcmd.NewCommand("commit", "-m", "Track LFS").WithDir(dstPath).Run(t.Context())
assert.NoError(t, err)
err = gitcmd.NewCommand("push", "origin", "master").WithDir(dstPath).Run(t.Context())
assert.NoError(t, err)
}
t.Run("PushUnderLimit", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
repoName := "repo-lfs-under"
ctx := NewAPITestContext(t, username, repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// Cleanup: reset limits and delete repository
defer func() {
setting.Repository.GitSizeMax = -1
setting.Repository.LFSSizeMax = -1
}()
defer doAPIDeleteRepository(ctx)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
setupLFS(t, dstPath)
// Push with limit enabled but not exceeded
setting.Repository.GitSizeMax = -1
setting.Repository.LFSSizeMax = 10000
doCommitAndPushWithData(t, dstPath, "data-under.dat", "some-content-under")
})
t.Run("PushOverLimit", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
repoName := "repo-lfs-over"
ctx := NewAPITestContext(t, username, repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// Cleanup: reset limits and delete repository
defer func() {
setting.Repository.GitSizeMax = -1
setting.Repository.LFSSizeMax = -1
}()
defer doAPIDeleteRepository(ctx)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
setupLFS(t, dstPath)
// Push with restrictive limit - should fail
setting.Repository.GitSizeMax = -1
setting.Repository.LFSSizeMax = 5
doCommitAndPushWithDataWithExpectedError(t, dstPath, "data-over.dat", "some-content-over-limit")
})
t.Run("SoftEnforcement", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
repoName := "repo-lfs-soft-enforce"
ctx := NewAPITestContext(t, username, repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// Cleanup: reset limits and delete repository
defer func() {
setting.Repository.GitSizeMax = -1
setting.Repository.LFSSizeMax = -1
}()
defer doAPIDeleteRepository(ctx)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
setupLFS(t, dstPath)
// Step 1 & 2: Init LFS and push 1024B file with random content (Commit 1)
setting.Repository.GitSizeMax = -1
setting.Repository.LFSSizeMax = -1
doCommitAndPushLFSWithRandomData(t, dstPath, "data-soft-1.dat", 1024)
// Step 3: Push 10240B LFS file with random content (Commit 2)
doCommitAndPushLFSWithRandomData(t, dstPath, "data-soft-2.dat", 10240)
// Step 4: Set limit to 10 KiB (1 KiB below current ~11 KiB)
setting.Repository.LFSSizeMax = 10 * 1024
// Step 5: Try to push 1KB LFS file - should fail (Commit 3, local only)
err := os.WriteFile(path.Join(dstPath, "data-soft-3.dat"), generateRandomData(1024), 0o644)
assert.NoError(t, err)
err = gitcmd.NewCommand("add", "data-soft-3.dat").WithDir(dstPath).Run(t.Context())
assert.NoError(t, err)
err = gitcmd.NewCommand("commit", "-m", "Add data-soft-3.dat").WithDir(dstPath).Run(t.Context())
assert.NoError(t, err)
err = gitcmd.NewCommand("push", "origin", "master").WithDir(dstPath).Run(t.Context())
assert.Error(t, err, "Push should fail when exceeding LFS limit")
// Step 6: Reset to Commit 1 (removes Commits 2 & 3)
_, _, err = gitcmd.NewCommand("reset", "--hard", "HEAD~2").WithDir(dstPath).RunStdString(t.Context())
assert.NoError(t, err, "Reset should succeed")
_, _, err = gitcmd.NewCommand("push", "--force-with-lease", "origin", "master").WithDir(dstPath).RunStdString(t.Context())
assert.NoError(t, err, "Force push should succeed with soft enforcement")
// Step 7: Try to push 1024B LFS file - should still fail
doCommitAndPushLFSWithRandomDataWithExpectedError(t, dstPath, "data-soft-new.dat", 1024)
})
}
// Helper functions
func doCommitAndPushWithData(t *testing.T, repoPath, filename, content string) {
err := os.WriteFile(path.Join(repoPath, filename), []byte(content), 0o644)
assert.NoError(t, err)
err = gitcmd.NewCommand("add").AddDynamicArguments(filename).WithDir(repoPath).Run(t.Context())
assert.NoError(t, err)
err = gitcmd.NewCommand("commit", "-m").AddDynamicArguments("Add " + filename).WithDir(repoPath).Run(t.Context())
assert.NoError(t, err)
err = gitcmd.NewCommand("push", "origin", "master").WithDir(repoPath).Run(t.Context())
assert.NoError(t, err)
}
func doCommitAndPushWithDataWithExpectedError(t *testing.T, repoPath, filename, content string) {
err := os.WriteFile(path.Join(repoPath, filename), []byte(content), 0o644)
assert.NoError(t, err)
err = gitcmd.NewCommand("add").AddDynamicArguments(filename).WithDir(repoPath).Run(t.Context())
assert.NoError(t, err)
err = gitcmd.NewCommand("commit", "-m").AddDynamicArguments("Add " + filename).WithDir(repoPath).Run(t.Context())
assert.NoError(t, err)
err = gitcmd.NewCommand("push", "origin", "master").WithDir(repoPath).Run(t.Context())
assert.Error(t, err)
}
func generateRandomData(size int) []byte {
data := make([]byte, size)
_, _ = io.ReadFull(rand.Reader, data)
return data
}
func doCommitAndPushLFSWithRandomData(t *testing.T, repoPath, filename string, size int) {
err := os.WriteFile(path.Join(repoPath, filename), generateRandomData(size), 0o644)
assert.NoError(t, err)
err = gitcmd.NewCommand("add").AddDynamicArguments(filename).WithDir(repoPath).Run(t.Context())
assert.NoError(t, err)
// Verify file is tracked by LFS
stdout, _, err := gitcmd.NewCommand("lfs", "ls-files").WithDir(repoPath).RunStdString(t.Context())
assert.NoError(t, err, "git lfs ls-files should succeed")
assert.Contains(t, stdout, filename, "File %s should be tracked by LFS", filename)
err = gitcmd.NewCommand("commit", "-m").AddDynamicArguments("Add " + filename).WithDir(repoPath).Run(t.Context())
assert.NoError(t, err)
err = gitcmd.NewCommand("push", "origin", "master").WithDir(repoPath).Run(t.Context())
assert.NoError(t, err)
}
func doCommitAndPushLFSWithRandomDataWithExpectedError(t *testing.T, repoPath, filename string, size int) {
err := os.WriteFile(path.Join(repoPath, filename), generateRandomData(size), 0o644)
assert.NoError(t, err)
err = gitcmd.NewCommand("add").AddDynamicArguments(filename).WithDir(repoPath).Run(t.Context())
assert.NoError(t, err)
// Verify file is tracked by LFS
stdout, _, err := gitcmd.NewCommand("lfs", "ls-files").WithDir(repoPath).RunStdString(t.Context())
assert.NoError(t, err, "git lfs ls-files should succeed")
assert.Contains(t, stdout, filename, "File %s should be tracked by LFS", filename)
err = gitcmd.NewCommand("commit", "-m").AddDynamicArguments("Add " + filename).WithDir(repoPath).Run(t.Context())
assert.NoError(t, err)
err = gitcmd.NewCommand("push", "origin", "master").WithDir(repoPath).Run(t.Context())
assert.Error(t, err)
}
// Reuse global helpers for Git: doCommitAndPush, doCommitAndPushWithExpectedError