0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-06-21 16:52:02 +02:00

SizeLimit w/o DB changes

This commit is contained in:
DmitryFrolovTri 2026-01-13 09:18:03 +00:00
parent 7b10eb20a5
commit 7942523c3f
No known key found for this signature in database
GPG Key ID: 0FBA4D49377CDDC3
22 changed files with 303 additions and 880 deletions

View File

@ -1087,22 +1087,15 @@ LEVEL = Info
;; Allow to fork repositories without maximum number limit
;ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT = true
;
;; Specify a global repository size limit in bytes to apply for each repository. -1 - Disabled, 0 - Enabled with no limit
;; If repository has it's own limit set in repositiry settings UI it will override the global setting
;; Specify a global repository Git size limit in bytes. -1 - Disabled, 0 - No growth allowed
;; Standard units of measurements for size can be used like B, KB, KiB, ... , EB, EiB, etc or if not provided - bytes
;; This is experimental and subject to change
;REPO_SIZE_LIMIT = -1
;GIT_SIZE_MAX = -1
;; Specify a global LFS size limit in bytes to apply for each repository. -1 - Disabled, 0 - Enabled with no limit
;; If repository has it's own limit set in repository settings UI it will override the global setting
;; Specify a global repository LFS size limit in bytes. -1 - Disabled, 0 - No growth allowed
;; Standard units of measurements for size can be used like B, KB, KiB, ... , EB, EiB, etc or if not provided - bytes
;; This is experimental and subject to change
;LFS_SIZE_LIMIT = -1
;;
;; If true, LFS size will be included in the repository size calculation.
;; Which means even if LFS_SIZE_LIMIT is not set (-1) pushes will be rejected if LFS size + repository size exceeds REPO_SIZE_LIMIT
;; This is experimental and subject to change
;LFS_SIZE_IN_REPO_SIZE = false
;LFS_SIZE_MAX = -1
;; Allow to fork repositories into the same owner (user or organization)
;; This feature is experimental, not fully tested, and may be changed in the future

View File

@ -398,7 +398,6 @@ func prepareMigrationTasks() []*migration {
// Gitea 1.25.0 ends at migration ID number 322 (database version 323)
newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency),
newMigration(324, "Add support for repository size limit - SizeLimit and LFSSizeLimit columns to repository table", v1_26.AddSizeLimitOnRepo),
}
return preparedMigrations
}

View File

@ -1,18 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"xorm.io/xorm"
)
func AddSizeLimitOnRepo(x *xorm.Engine) error {
type Repository struct {
ID int64 `xorm:"pk autoincr"`
SizeLimit int64 `xorm:"NOT NULL DEFAULT 0"`
LFSSizeLimit int64 `xorm:"NOT NULL DEFAULT 0"`
}
return x.Sync2(new(Repository))
}

View File

@ -201,11 +201,9 @@ type Repository struct {
BaseRepo *Repository `xorm:"-"`
IsTemplate bool `xorm:"INDEX NOT NULL DEFAULT false"`
TemplateID int64 `xorm:"INDEX"`
SizeLimit int64 `xorm:"NOT NULL DEFAULT 0"`
Size int64 `xorm:"NOT NULL DEFAULT 0"`
GitSize int64 `xorm:"NOT NULL DEFAULT 0"`
LFSSize int64 `xorm:"NOT NULL DEFAULT 0"`
LFSSizeLimit int64 `xorm:"NOT NULL DEFAULT 0"`
CodeIndexerStatus *RepoIndexerStatus `xorm:"-"`
StatsIndexerStatus *RepoIndexerStatus `xorm:"-"`
IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"`
@ -633,58 +631,43 @@ func (repo *Repository) IsOwnedBy(userID int64) bool {
}
// GetActualSizeLimit returns repository size limit in bytes
// or global repository limit setting if per repository size limit is not set
func (repo *Repository) GetActualSizeLimit() int64 {
sizeLimit := repo.SizeLimit
if setting.RepoSizeLimit > 0 && sizeLimit == 0 {
sizeLimit = setting.RepoSizeLimit
}
return sizeLimit
return setting.Repository.GitSizeMax
}
// RepoSizeIsOversized return true if is over size limitation
func (repo *Repository) IsRepoSizeOversized(additionalSize int64) bool {
return repo.ShouldCheckRepoSize() && repo.GitSize+additionalSize > repo.GetActualSizeLimit()
limit := repo.GetActualSizeLimit()
if limit < 0 {
return false
}
newSize := repo.GitSize + additionalSize
return newSize > limit && newSize > repo.GitSize
}
// ShouldCheckRepoSize returns true if size limit checking is enabled and limit is non zero for this specific repository
// this is used to enable size checking during pre-receive hook
// ShouldCheckRepoSize returns true if size limit checking is enabled
func (repo *Repository) ShouldCheckRepoSize() bool {
return setting.RepoSizeLimit > -1 && repo.GetActualSizeLimit() > 0
return setting.Repository.GitSizeMax > -1
}
// GetActualLFSSizeLimit returns repository LFS size limit in bytes
// or global LFS size limit setting if per repository LFS size limit is not set
func (repo *Repository) GetActualLFSSizeLimit() int64 {
lfsSizeLimit := repo.LFSSizeLimit
if setting.LFSSizeLimit > 0 && lfsSizeLimit == 0 {
lfsSizeLimit = setting.LFSSizeLimit
}
return lfsSizeLimit
return setting.Repository.LFSSizeMax
}
// ShouldCheckLFSSize returns true if LFS size limit checking is enabled for this repository
// ShouldCheckLFSSize returns true if LFS size limit checking is enabled
func (repo *Repository) ShouldCheckLFSSize() bool {
return setting.LFSSizeLimit > -1 && repo.GetActualLFSSizeLimit() > 0
return setting.Repository.LFSSizeMax > -1
}
// IsLFSSizeOversized returns true if adding additionalSize would exceed the LFS size limit
func (repo *Repository) IsLFSSizeOversized(additionalSize int64) bool {
return repo.ShouldCheckLFSSize() &&
repo.LFSSize+additionalSize > repo.GetActualLFSSizeLimit()
}
// IsRepoAndLFSSizeOversized checks if combined repo + LFS size exceeds repo size limit
// This is used when LFS_SIZE_IN_REPO_SIZE is enabled
func (repo *Repository) IsRepoAndLFSSizeOversized(additionalGitSize, additionalLFSSize int64) bool {
if !setting.LFSSizeInRepoSize || setting.RepoSizeLimit == -1 {
limit := repo.GetActualLFSSizeLimit()
if limit < 0 {
return false
}
limit := repo.GetActualSizeLimit()
if limit == 0 {
return false
}
return (repo.GitSize+additionalGitSize)+(repo.LFSSize+additionalLFSSize) > limit
newSize := repo.LFSSize + additionalSize
return newSize > limit && newSize > repo.LFSSize
}
// CanCreateBranch returns true if repository meets the requirements for creating new branches.

View File

@ -6,7 +6,6 @@ package setting
import (
"os/exec"
"path/filepath"
"strconv"
"strings"
"code.gitea.io/gitea/modules/log"
@ -56,6 +55,8 @@ var (
DisableDownloadSourceArchives bool
AllowForkWithoutMaximumLimit bool
AllowForkIntoSameOwner bool
GitSizeMax int64 `ini:"GIT_SIZE_MAX"`
LFSSizeMax int64 `ini:"LFS_SIZE_MAX"`
// StreamArchives makes Gitea stream git archive files to the client directly instead of creating an archive first.
// Ideally all users should use this streaming method. However, at the moment we don't know whether there are
@ -276,36 +277,11 @@ var (
}
RepoRootPath string
ScriptType = "bash"
// Repository size limits
RepoSizeLimit int64 = -1
LFSSizeLimit int64 = -1
LFSSizeInRepoSize bool
)
func SaveGlobalRepositorySetting(repoSizeLimit, lfsSizeLimit int64, lfsSizeInRepoSize bool) error {
RepoSizeLimit = repoSizeLimit
LFSSizeLimit = lfsSizeLimit
LFSSizeInRepoSize = lfsSizeInRepoSize
cfg, err := CfgProvider.PrepareSaving()
if err != nil {
return err
}
sec := cfg.Section("repository")
if RepoSizeLimit == -1 {
sec.Key("REPO_SIZE_LIMIT").SetValue("-1")
} else {
sec.Key("REPO_SIZE_LIMIT").SetValue(humanize.Bytes(uint64(RepoSizeLimit)))
}
if LFSSizeLimit == -1 {
sec.Key("LFS_SIZE_LIMIT").SetValue("-1")
} else {
sec.Key("LFS_SIZE_LIMIT").SetValue(humanize.Bytes(uint64(LFSSizeLimit)))
}
sec.Key("LFS_SIZE_IN_REPO_SIZE").SetValue(strconv.FormatBool(LFSSizeInRepoSize))
return cfg.Save()
func UpdateGlobalRepositoryLimit(gitSizeMax, lfsSizeMax int64) {
Repository.GitSizeMax = gitSizeMax
Repository.LFSSizeMax = lfsSizeMax
}
func parseSize(sec ConfigSection, key string, def int64) int64 {
@ -328,9 +304,8 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
// Determine and create root git repository path.
sec := rootCfg.Section("repository")
RepoSizeLimit = parseSize(sec, "REPO_SIZE_LIMIT", -1)
LFSSizeLimit = parseSize(sec, "LFS_SIZE_LIMIT", -1)
LFSSizeInRepoSize = sec.Key("LFS_SIZE_IN_REPO_SIZE").MustBool(false)
Repository.GitSizeMax = parseSize(sec, "GIT_SIZE_MAX", -1)
Repository.LFSSizeMax = parseSize(sec, "LFS_SIZE_MAX", -1)
Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool()
Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").MustBool()

View File

@ -153,10 +153,6 @@ type CreateRepoOption struct {
// ObjectFormatName of the underlying git repository
// enum: sha1,sha256
ObjectFormatName string `json:"object_format_name" binding:"MaxSize(6)"`
// SizeLimit of the repository
SizeLimit int64 `json:"size_limit"`
// LFSSizeLimit of the repository
LFSSizeLimit int64 `json:"lfs_size_limit"`
}
// EditRepoOption options when editing a repository's properties
@ -227,11 +223,7 @@ type EditRepoOption struct {
DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`
// set to `true` to archive this repository.
Archived *bool `json:"archived,omitempty"`
// SizeLimit of the repository.
SizeLimit *int64 `json:"size_limit,omitempty"`
// LFSSizeLimit of the repository.
LFSSizeLimit *int64 `json:"lfs_size_limit,omitempty"`
// set to a string like `8h30m0s` to set the mirror interval time
// set a string like `8h30m0s` to set the mirror interval time
MirrorInterval *string `json:"mirror_interval,omitempty"`
// enable prune - remove obsolete remote-tracking references when mirroring
EnablePrune *bool `json:"enable_prune,omitempty"`

View File

@ -987,9 +987,7 @@
"repo_name_profile_private_hint": ".profile-private is a special repository that you can use to add a README.md to your organization member profile, visible only to organization members. Make sure it's private and initialize it with a README in the profile directory to get started.",
"repo_name_helper": "Good repository names use short, memorable and unique keywords. A repository named \".profile\" or \".profile-private\" could be used to add a README.md for the user/organization profile.",
"repo_size": "Repository Size",
"repo_size_limit": "Repository Size Limit",
"lfs_size": "LFS Size",
"lfs_size_limit": "LFS Size Limit",
"template": "Template",
"template_select": "Select a template.",
"template_helper": "Make repository a template",
@ -1116,14 +1114,6 @@
"form.reach_limit_of_creation_n": "The owner has already reached the limit of %d repositories.",
"form.name_reserved": "The repository name \"%s\" is reserved.",
"form.name_pattern_not_allowed": "The pattern \"%s\" is not allowed in a repository name.",
"form.invalid_repo_size_limit": "Invalid format. Use -1, 0, or size (e.g. 500MiB).",
"form.invalid_repo_size_limit_repo": "Invalid format. Use 0 or size (e.g. 500MiB).",
"form.invalid_lfs_size_limit": "Invalid format. Use -1, 0, or size (e.g. 500MiB).",
"form.invalid_lfs_size_limit_repo": "Invalid format. Use 0 or size (e.g. 500MiB).",
"form.repo_size_limit_only_by_admins": "Only administrators can change the repository size limitation.",
"form.lfs_size_limit_only_by_admins": "Only administrators can change the LFS size limitation.",
"repo_size_limit_helper": "Set to 0 to use the global limit.",
"lfs_size_limit_helper": "Set to 0 to use the global limit.",
"need_auth": "Authorization",
"migrate_options": "Migration Options",
"migrate_service": "Migration Service",
@ -3064,6 +3054,7 @@
"orgs.members": "Members",
"orgs.new_orga": "New Organization",
"repos.repo_manage_panel": "Repository Management",
"repos.settings": "Global Repository Settings",
"repos.unadopted": "Unadopted Repositories",
"repos.unadopted.no_more": "No more unadopted repositories found",
"repos.owner": "Owner",
@ -3072,6 +3063,11 @@
"repos.issues": "Issues",
"repos.size": "Size",
"repos.lfs_size": "LFS Size",
"repos.git_size_max": "Max Git Size (Global)",
"repos.git_size_max_helper": "Maximum Git size allowed for a single repository. Set to -1 for unlimited.",
"repos.lfs_size_max": "Max LFS Size (Global)",
"repos.lfs_size_max_helper": "Maximum LFS size allowed for a single repository. Set to -1 for unlimited.",
"repos.update_success": "Global repository limits have been updated.",
"packages.package_manage_panel": "Package Management",
"packages.total_size": "Total Size: %s",
"packages.unreferenced_size": "Unreferenced Size: %s",
@ -3321,19 +3317,6 @@
"config.open_with_editor_app_help": "The \"Open with\" editors for the clone menu. If left empty, the default will be used. Expand to see the default.",
"config.git_guide_remote_name": "Repository remote name for git commands in the guide",
"config.repository_config": "Repository Configuration",
"config.enable_size_limit": "Enable Size Limit",
"config.repo_size_limit": "Default Repository Size Limit",
"config.repo_size_limit.desc": "Maximum repository size in MiB. Leave empty for no limit.",
"config.lfs_size_limit": "Default LFS Size Limit",
"config.lfs_size_limit.desc": "Maximum LFS size in MiB. Leave empty for no limit.",
"config.lfs_size_in_repo_size": "Count LFS in Repository Size",
"config.lfs_size_in_repo_size.desc": "When enabled, LFS size is included in repository size calculations",
"config.global_repo_size_limit_manage_panel": "Manage Global Repository Size Limit",
"config.update_settings": "Update Settings",
"config.invalid_repo_size": "Invalid format: %s. Use -1, 0, or size (e.g. 500MiB).",
"config.invalid_lfs_size": "Invalid format: %s. Use -1, 0, or size (e.g. 500MiB).",
"config.save_repo_size_setting_failed": "Failed to save global repository settings %s",
"config.repository_setting_success": "Global repository setting has been updated",
"config.git_config": "Git Configuration",
"config.git_disable_diff_highlight": "Disable Diff Syntax Highlight",
"config.git_max_diff_lines": "Max Diff Lines (for a single file)",
@ -3812,4 +3795,4 @@
"symbolic_link": "Symbolic link",
"submodule": "Submodule"
}
}
}

View File

@ -263,8 +263,6 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
TrustModel: repo_model.ToTrustModel(opt.TrustModel),
IsTemplate: opt.Template,
ObjectFormatName: opt.ObjectFormatName,
SizeLimit: opt.SizeLimit,
LFSSizeLimit: opt.LFSSizeLimit,
})
if err != nil {
if repo_model.IsErrRepoAlreadyExist(err) {
@ -751,13 +749,6 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
}
}
if opts.SizeLimit != nil {
repo.SizeLimit = *opts.SizeLimit
}
if opts.LFSSizeLimit != nil {
repo.LFSSizeLimit = *opts.LFSSizeLimit
}
if err := repo_service.UpdateRepository(ctx, repo, visibilityChanged); err != nil {
ctx.APIErrorInternal(err)
return err

View File

@ -550,13 +550,6 @@ func scanLFSPointersFromObjectIDs(ctx *gitea_context.PrivateContext, repoPath st
return out, nil
}
func maxInt64(a, b int64) int64 {
if a > b {
return a
}
return b
}
// HookPreReceive checks whether a individual commit is acceptable
func HookPreReceive(ctx *gitea_context.PrivateContext) {
startTime := time.Now()
@ -587,7 +580,7 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
var duration time.Duration
needGitDelta := repo.ShouldCheckRepoSize()
needLFSDelta := repo.ShouldCheckLFSSize() || setting.LFSSizeInRepoSize
needLFSDelta := repo.ShouldCheckLFSSize()
// Only do CountObjects (push/repo) when we're doing the repo-size limit at all
if needGitDelta {
@ -827,7 +820,6 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
currentGit := repo.GitSize
currentLFS := repo.LFSSize
currentCombined := currentGit + currentLFS
gitDelta := addedSize - removedSize
predictedGitAfter := currentGit + gitDelta
@ -835,38 +827,24 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
lfsDelta := incomingNewToRepoLFS - removedLFSSize
predictedLFSAfter := currentLFS + lfsDelta
predictedCombinedAfter := predictedGitAfter
if setting.LFSSizeInRepoSize {
predictedCombinedAfter = predictedGitAfter + predictedLFSAfter
}
// Avoid nil panic when repo-size check is disabled but LFS delta is enabled (combined mode / LFS limit)
pushBytes := int64(0)
if pushSize != nil {
pushBytes = maxInt64(0, pushSize.Size+pushSize.SizePack)
}
// One summary line (time included here only)
if repo.ShouldCheckRepoSize() || repo.ShouldCheckLFSSize() {
log.Warn(
"SizeCheck summary: took=%s repo=%s/%s git(pred=%s cur=%s delta=%s) lfs(pred=%s cur=%s delta=%s) combined(pred=%s) limits(repo=%s lfs=%s) LFSSizeInRepoSize=%v push=%s",
"SizeCheck summary: took=%s repo=%s/%s git(pred=%s cur=%s delta=%s) lfs(pred=%s cur=%s delta=%s) limits(git=%s lfs=%s)",
duration,
repo.OwnerName, repo.Name,
base.FileSize(predictedGitAfter), base.FileSize(currentGit), base.FileSize(gitDelta),
base.FileSize(predictedLFSAfter), base.FileSize(currentLFS), base.FileSize(lfsDelta),
base.FileSize(predictedCombinedAfter),
base.FileSize(repo.GetActualSizeLimit()),
base.FileSize(repo.GetActualLFSSizeLimit()),
setting.LFSSizeInRepoSize,
base.FileSize(pushBytes),
base.FileSize(setting.Repository.GitSizeMax),
base.FileSize(setting.Repository.LFSSizeMax),
)
}
// 1) LFS-only limit: compare against predicted LFS after push
// 1) LFS size limit: compare against predicted LFS after push
if repo.ShouldCheckLFSSize() {
lfsLimit := repo.GetActualLFSSizeLimit()
if lfsLimit > 0 && predictedLFSAfter > lfsLimit && predictedLFSAfter > currentLFS {
log.Warn("Forbidden: LFS limit exceeded: %s > %s for repo %-v",
if lfsLimit >= 0 && predictedLFSAfter > lfsLimit && predictedLFSAfter > currentLFS {
log.Warn("Forbidden: LFS size limit exceeded: %s > %s for repo %-v",
base.FileSize(predictedLFSAfter),
base.FileSize(lfsLimit),
repo,
@ -881,17 +859,17 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
}
}
// 2) Repo (git) size limit when NOT counting LFS into repo size
if repo.ShouldCheckRepoSize() && !setting.LFSSizeInRepoSize {
// 2) Git size limit
if repo.ShouldCheckRepoSize() {
limit := repo.GetActualSizeLimit()
if limit > 0 && predictedGitAfter > limit && predictedGitAfter > currentGit {
log.Warn("Forbidden: repository size limit exceeded: %s > %s for repo %-v",
if limit >= 0 && predictedGitAfter > limit && predictedGitAfter > currentGit {
log.Warn("Forbidden: Repository git size limit exceeded: %s > %s for repo %-v",
base.FileSize(predictedGitAfter),
base.FileSize(limit),
repo,
)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Repository size limit exceeded: %s > then limit of %s",
UserMsg: fmt.Sprintf("Repository git size limit exceeded: %s > then limit of %s",
base.FileSize(predictedGitAfter),
base.FileSize(limit),
),
@ -900,25 +878,6 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
}
}
// 3) Combined limit when LFS is counted in repo size
if setting.LFSSizeInRepoSize && repo.ShouldCheckRepoSize() {
limit := repo.GetActualSizeLimit()
if limit > 0 && predictedCombinedAfter > limit && predictedCombinedAfter > currentCombined {
log.Warn("Forbidden: combined repo and LFS size limit exceeded: %s > %s for repo %-v",
base.FileSize(predictedCombinedAfter),
base.FileSize(limit),
repo,
)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Combined repository+LFS size limit exceeded: %s > then limit of %s",
base.FileSize(predictedCombinedAfter),
base.FileSize(limit),
),
})
return
}
}
ctx.PlainText(http.StatusOK, "ok")
}

View File

@ -33,9 +33,8 @@ func Repos(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.repositories")
ctx.Data["PageIsAdminRepositories"] = true
ctx.Data["RepoSizeLimit"] = base.FileSize(setting.RepoSizeLimit)
ctx.Data["LFSSizeLimit"] = base.FileSize(setting.LFSSizeLimit)
ctx.Data["LFSSizeInRepoSize"] = setting.LFSSizeInRepoSize
ctx.Data["GitSizeMax"] = base.FileSize(setting.Repository.GitSizeMax)
ctx.Data["LFSSizeMax"] = base.FileSize(setting.Repository.LFSSizeMax)
explore.RenderRepoSearch(ctx, &explore.RepoSearchOptions{
Private: true,
@ -46,28 +45,16 @@ func Repos(ctx *context.Context) {
}
func UpdateRepoPost(ctx *context.Context) {
temp := web.GetForm(ctx)
if temp == nil {
ctx.Data["Err_Repo_Size_Limit"] = ""
explore.RenderRepoSearch(ctx, &explore.RepoSearchOptions{
Private: true,
PageSize: setting.UI.Admin.RepoPagingNum,
TplName: tplRepos,
OnlyShowRelevant: false,
})
return
}
form := temp.(*forms.UpdateGlobalRepoFrom)
form := web.GetForm(ctx).(*forms.UpdateGlobalRepoFrom)
ctx.Data["Title"] = ctx.Tr("admin.repositories")
ctx.Data["PageIsAdminRepositories"] = true
ctx.Data["RepoSizeLimit"] = form.RepoSizeLimit
ctx.Data["LFSSizeLimit"] = form.LFSSizeLimit
ctx.Data["LFSSizeInRepoSize"] = form.LFSSizeInRepoSize
ctx.Data["GitSizeMax"] = form.GitSizeMax
ctx.Data["LFSSizeMax"] = form.LFSSizeMax
repoSizeLimit, err := base.GetFileSize(form.RepoSizeLimit)
gitSizeMax, err := base.GetFileSize(form.GitSizeMax)
if err != nil {
ctx.Data["Err_Repo_Size_Limit"] = form.RepoSizeLimit
ctx.Data["Err_Git_Size_Max"] = form.GitSizeMax
explore.RenderRepoSearch(ctx, &explore.RepoSearchOptions{
Private: true,
PageSize: setting.UI.Admin.RepoPagingNum,
@ -77,9 +64,9 @@ func UpdateRepoPost(ctx *context.Context) {
return
}
lfsSizeLimit, err := base.GetFileSize(form.LFSSizeLimit)
lfsSizeMax, err := base.GetFileSize(form.LFSSizeMax)
if err != nil {
ctx.Data["Err_LFS_Size_Limit"] = form.LFSSizeLimit
ctx.Data["Err_LFS_Size_Max"] = form.LFSSizeMax
explore.RenderRepoSearch(ctx, &explore.RepoSearchOptions{
Private: true,
PageSize: setting.UI.Admin.RepoPagingNum,
@ -89,19 +76,9 @@ func UpdateRepoPost(ctx *context.Context) {
return
}
err = setting.SaveGlobalRepositorySetting(repoSizeLimit, lfsSizeLimit, form.LFSSizeInRepoSize)
if err != nil {
ctx.Data["Err_Repo_Size_Save"] = err.Error()
explore.RenderRepoSearch(ctx, &explore.RepoSearchOptions{
Private: true,
PageSize: setting.UI.Admin.RepoPagingNum,
TplName: tplRepos,
OnlyShowRelevant: false,
})
return
}
setting.UpdateGlobalRepositoryLimit(gitSizeMax, lfsSizeMax)
ctx.Flash.Success(ctx.Tr("admin.config.repository_setting_success"))
ctx.Flash.Success(ctx.Tr("admin.repos.update_success"))
ctx.Redirect(setting.AppSubURL + "/-/admin/repos")
}

View File

@ -1,26 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
func TestUpdateRepoPost(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "admin/repos")
contexttest.LoadUser(t, ctx, 1)
ctx.Req.Form.Set("enable_size_limit", "on")
ctx.Req.Form.Set("repo_size_limit", "222 kcmcm")
UpdateRepoPost(ctx)
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}

View File

@ -288,7 +288,6 @@ func CreatePost(ctx *context.Context) {
IsTemplate: form.Template,
TrustModel: repo_model.DefaultTrustModel,
ObjectFormatName: form.ObjectFormatName,
SizeLimit: form.SizeLimit,
})
if err == nil {
log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)

View File

@ -15,7 +15,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/indexer/code"
@ -62,11 +61,8 @@ func SettingsCtxData(ctx *context.Context) {
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner)
ctx.Data["Err_RepoSize"] = ctx.Repo.Repository.IsRepoSizeOversized(ctx.Repo.Repository.GetActualSizeLimit() / 10) // less than 10% left
ctx.Data["ActualSizeLimit"] = ctx.Repo.Repository.GetActualSizeLimit()
ctx.Data["ActualLFSSizeLimit"] = ctx.Repo.Repository.GetActualLFSSizeLimit()
ctx.Data["RepoSizeLimit"] = setting.RepoSizeLimit
ctx.Data["LFSSizeLimit"] = setting.LFSSizeLimit
ctx.Data["LFSSizeInRepoSize"] = setting.LFSSizeInRepoSize
ctx.Data["GitSizeMax"] = ctx.Repo.Repository.GetActualSizeLimit()
ctx.Data["LFSSizeMax"] = ctx.Repo.Repository.GetActualLFSSizeLimit()
signing, _ := gitrepo.GetSigningKey(ctx)
ctx.Data["SigningKeyAvailable"] = signing != nil
@ -214,56 +210,9 @@ func handleSettingsPostUpdate(ctx *context.Context) {
repo.Name = newRepoName
repo.LowerName = strings.ToLower(newRepoName)
repo.Description = form.Description
ctx.Data["RepoSizeLimitText"] = form.RepoSizeLimit
ctx.Data["LFSSizeLimitText"] = form.LFSSizeLimit
repo.Website = form.Website
repo.IsTemplate = form.Template
var repoSizeLimit int64
var err error
if form.RepoSizeLimit != "" {
repoSizeLimit, err = base.GetFileSize(form.RepoSizeLimit)
if err != nil {
ctx.Data["Err_RepoSizeLimit"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.invalid_repo_size_limit_repo"), tplSettingsOptions, &form)
return
}
}
if repoSizeLimit < 0 {
ctx.Data["Err_RepoSizeLimit"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.invalid_repo_size_limit_repo"), tplSettingsOptions, &form)
return
}
if !ctx.Doer.IsAdmin && repo.SizeLimit != repoSizeLimit {
ctx.Data["Err_RepoSizeLimit"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.repo_size_limit_only_by_admins"), tplSettingsOptions, &form)
return
}
repo.SizeLimit = repoSizeLimit
// Handle LFS size limit (admin-only)
var lfsSizeLimit int64
if form.LFSSizeLimit != "" {
lfsSizeLimit, err = base.GetFileSize(form.LFSSizeLimit)
if err != nil {
ctx.Data["Err_LFSSizeLimit"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.invalid_lfs_size_limit_repo"), tplSettingsOptions, &form)
return
}
}
if lfsSizeLimit < 0 {
ctx.Data["Err_LFSSizeLimit"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.invalid_lfs_size_limit_repo"), tplSettingsOptions, &form)
return
}
if !ctx.Doer.IsAdmin && repo.LFSSizeLimit != lfsSizeLimit {
ctx.Data["Err_LFSSizeLimit"] = true
ctx.RenderWithErr(ctx.Tr("repo.form.lfs_size_limit_only_by_admins"), tplSettingsOptions, &form)
return
}
repo.LFSSizeLimit = lfsSizeLimit
if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
ctx.ServerError("UpdateRepository", err)
return

View File

@ -18,13 +18,6 @@ import (
"gitea.com/go-chi/binding"
)
// UpdateGlobalRepoFrom for updating global repository setting
type UpdateGlobalRepoFrom struct {
RepoSizeLimit string
LFSSizeLimit string
LFSSizeInRepoSize bool
}
// CreateRepoForm form for creating repository
type CreateRepoForm struct {
UID int64 `binding:"Required"`
@ -50,7 +43,11 @@ type CreateRepoForm struct {
ForkSingleBranch string
ObjectFormatName string
SizeLimit int64
}
type UpdateGlobalRepoFrom struct {
GitSizeMax string
LFSSizeMax string
}
// Validate validates the fields
@ -114,8 +111,6 @@ type RepoSettingForm struct {
PushMirrorInterval string
Template bool
EnablePrune bool
RepoSizeLimit string
LFSSizeLimit string
// Advanced settings
EnableCode bool

View File

@ -244,70 +244,42 @@ func BatchHandler(ctx *context.Context) {
// Create content store once, reuse for tracing + normal logic below.
contentStore := lfs_module.NewContentStore()
currCombinedTotal := repository.GitSize + repository.LFSSize
// Baseline repo stats and limits
traceBatchDecision(rc, br.Operation,
"req=%s auth=%t isUpload=%t repoID=%d sizes: git=%s lfs=%s combined=%s limits: repo=%s lfs=%s LFSSizeInRepoSize=%v",
"req=%s auth=%t isUpload=%t repoID=%d sizes: git=%s lfs=%s limits: git=%s lfs=%s",
reqID,
ctx.IsSigned || ctx.Doer != nil,
isUpload,
repository.ID,
base.FileSize(repository.GitSize),
base.FileSize(repository.LFSSize),
base.FileSize(currCombinedTotal),
base.FileSize(repository.GetActualSizeLimit()),
base.FileSize(repository.GetActualLFSSizeLimit()),
setting.LFSSizeInRepoSize,
)
// Check LFS size limits for upload operations
if isUpload && (repository.ShouldCheckLFSSize() || (setting.LFSSizeInRepoSize && repository.ShouldCheckRepoSize())) {
if isUpload && repository.ShouldCheckLFSSize() {
// Sum sizes of objects that are NEW TO THIS REPO (no meta row)
var incomingNewToRepoLFS int64
var invalidCount, metaMissingCount, metaPresentCount, storeExistsCount int64
for _, p := range br.Objects {
if !p.IsValid() {
invalidCount++
continue
}
meta, _ := git_model.GetLFSMetaObjectByOid(ctx, repository.ID, p.Oid)
exists, _ := contentStore.Exists(p)
if exists {
storeExistsCount++
}
if meta == nil {
metaMissingCount++
incomingNewToRepoLFS += p.Size
} else {
metaPresentCount++
}
}
predictedLFS := repository.LFSSize + incomingNewToRepoLFS
predictedTotal := repository.GitSize + predictedLFS
traceBatchDecision(rc, br.Operation,
"req=%s accounting: objects=%d invalid=%d metaMissing=%d metaPresent=%d storeExists=%d incomingNewToRepoLFS=%s predictedLFS=%s predictedTotal=%s",
reqID,
len(br.Objects),
invalidCount,
metaMissingCount,
metaPresentCount,
storeExistsCount,
base.FileSize(incomingNewToRepoLFS),
base.FileSize(predictedLFS),
base.FileSize(predictedTotal),
)
// LFS-only limit if we are over, but size doesn't increase allow
if repository.ShouldCheckLFSSize() && predictedLFS > repository.GetActualLFSSizeLimit() && predictedLFS > repository.LFSSize {
if predictedLFS > repository.GetActualLFSSizeLimit() && predictedLFS > repository.LFSSize {
traceBatchDecision(rc, br.Operation,
"req=%s DECISION=FORBID reason=LFS_LIMIT predictedLFS=%s limit=%s",
"req=%s DECISION=FORBID reason=LFS_LIMIT predictedLFS=%s limit=%s (NewObjects=%d ObjectsPresentInStore=%d MetaPresent=%d StoreExists=%d Invalid=%d)",
reqID, base.FileSize(predictedLFS), base.FileSize(repository.GetActualLFSSizeLimit()),
)
writeStatusMessage(ctx, http.StatusForbidden,
@ -316,20 +288,6 @@ func BatchHandler(ctx *context.Context) {
return
}
// Combined limit (conservative: ignores git delta/removals) if we are over, but combined size doesn't increase allow
if setting.LFSSizeInRepoSize && repository.ShouldCheckRepoSize() {
if predictedTotal > repository.GetActualSizeLimit() && predictedTotal > currCombinedTotal {
traceBatchDecision(rc, br.Operation,
"req=%s DECISION=FORBID reason=COMBINED_LIMIT predictedTotal=%s limit=%s",
reqID, base.FileSize(predictedTotal), base.FileSize(repository.GetActualSizeLimit()),
)
writeStatusMessage(ctx, http.StatusForbidden,
fmt.Sprintf("Repository size %s after LFS addition would exceed limit %s",
base.FileSize(predictedTotal), base.FileSize(repository.GetActualSizeLimit())))
return
}
}
traceBatchDecision(rc, br.Operation, "req=%s DECISION=ALLOW size-check passed", reqID)
}

View File

@ -52,8 +52,6 @@ type CreateRepoOptions struct {
TrustModel repo_model.TrustModelType
MirrorInterval string
ObjectFormatName string
SizeLimit int64
LFSSizeLimit int64
}
func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir string, opts CreateRepoOptions) error {
@ -249,8 +247,6 @@ func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User,
Status: opts.Status,
IsEmpty: !opts.AutoInit,
TrustModel: opts.TrustModel,
SizeLimit: opts.SizeLimit,
LFSSizeLimit: opts.LFSSizeLimit,
IsMirror: opts.IsMirror,
DefaultBranch: opts.DefaultBranch,
DefaultWikiBranch: setting.Repository.DefaultBranch,

View File

@ -1,31 +1,13 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin")}}
<div class="admin-setting-content">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.config.global_repo_size_limit_manage_panel"}}
{{ctx.Locale.Tr "admin.repositories"}}
<div class="ui right">
<span class="ui small label">{{ctx.Locale.Tr "repo.repo_size"}} Max: {{.GitSizeMax}}</span>
<span class="ui small label">{{ctx.Locale.Tr "repo.lfs_size"}} Max: {{.LFSSizeMax}}</span>
</div>
</h4>
<div class="ui attached segment">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="inline field {{if .Err_Repo_Size_Limit}}error{{end}}">
<label for="repo_size_limit">{{ctx.Locale.Tr "admin.config.repo_size_limit"}}</label>
<input id="repo_size_limit" name="repo_size_limit" value="{{.RepoSizeLimit}}" data-tooltip-content='{{ctx.Locale.Tr "admin.config.repo_size_limit.desc"}}'>
</div>
<div class="inline field {{if .Err_LFS_Size_Limit}}error{{end}}">
<label for="lfs_size_limit">{{ctx.Locale.Tr "admin.config.lfs_size_limit"}}</label>
<input id="lfs_size_limit" name="lfs_size_limit" value="{{.LFSSizeLimit}}" data-tooltip-content='{{ctx.Locale.Tr "admin.config.lfs_size_limit.desc"}}'>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "admin.config.lfs_size_in_repo_size"}}</label>
<div class="ui checkbox">
<input name="lfs_size_in_repo_size" type="checkbox" {{if .LFSSizeInRepoSize}}checked{{end}} data-tooltip-content='{{ctx.Locale.Tr "admin.config.lfs_size_in_repo_size.desc"}}'>
</div>
</div>
<div class="field">
<button class="ui green button">{{ctx.Locale.Tr "admin.config.update_settings"}}</button>
</div>
</form>
</div>
<h4 class="ui top attached header">
<h4 class="ui attached header">
{{ctx.Locale.Tr "admin.repos.repo_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
<div class="ui right">
<a class="ui primary tiny button" href="{{AppSubUrl}}/-/admin/repos/unadopted">{{ctx.Locale.Tr "admin.repos.unadopted"}}</a>
@ -126,6 +108,28 @@
{{template "base/paginate" .}}
</div>
<div class="ui container">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.repos.settings"}}
</h4>
<div class="ui attached segment">
<form class="ui form" action="{{$.Link}}" method="post">
{{$.CsrfTokenHtml}}
<div class="field {{if .Err_Git_Size_Max}}error{{end}}">
<label>{{ctx.Locale.Tr "admin.repos.git_size_max"}}</label>
<input name="GitSizeMax" value="{{if .Err_Git_Size_Max}}{{.Err_Git_Size_Max}}{{else}}{{.GitSizeMax}}{{end}}">
<p class="help">{{ctx.Locale.Tr "admin.repos.git_size_max_helper"}}</p>
</div>
<div class="field {{if .Err_LFS_Size_Max}}error{{end}}">
<label>{{ctx.Locale.Tr "admin.repos.lfs_size_max"}}</label>
<input name="LFSSizeMax" value="{{if .Err_LFS_Size_Max}}{{.Err_LFS_Size_Max}}{{else}}{{.LFSSizeMax}}{{end}}">
<p class="help">{{ctx.Locale.Tr "admin.repos.lfs_size_max_helper"}}</p>
</div>
<button class="ui green button">{{ctx.Locale.Tr "settings.update_settings"}}</button>
</form>
</div>
</div>
<form class="ui small modal form-fetch-action" id="admin-repo-delete-modal" method="post">
{{.CsrfTokenHtml}}
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.settings.delete"}}</div>

View File

@ -15,33 +15,19 @@
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.repo_size"}}</label>
<span {{if .Err_RepoSize}}class="ui text red"{{end}} {{if not (eq .Repository.Size 0)}} data-tooltip-content="{{.Repository.SizeDetailsString}}"{{end}}>{{FileSize .Repository.Size}}
{{if and .ActualSizeLimit (or (gt .RepoSizeLimit -1) (gt .LFSSizeLimit -1))}}
/ {{FileSize .ActualSizeLimit}}
{{if gt .GitSizeMax -1}}
/ {{FileSize .GitSizeMax}}
{{end}}
</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.lfs_size"}}</label>
<span>{{FileSize .Repository.LFSSize}}
{{if and .ActualLFSSizeLimit (or (gt .RepoSizeLimit -1) (gt .LFSSizeLimit -1))}}
/ {{FileSize .ActualLFSSizeLimit}}
{{if gt .LFSSizeMax -1}}
/ {{FileSize .LFSSizeMax}}
{{end}}
</span>
</div>
<div class="field {{if .Err_RepoSizeLimit}}error{{end}} {{if not .IsAdmin}}tw-hidden{{end}}">
<label for="repo_size_limit">{{ctx.Locale.Tr "repo.repo_size_limit"}}</label>
<input id="repo_size_limit" name="repo_size_limit"
{{if and (eq .RepoSizeLimit -1) (eq .LFSSizeLimit -1)}}class="ui text light grey"{{end}}
type="text" value="{{if .RepoSizeLimitText}}{{.RepoSizeLimitText}}{{else if .Repository.SizeLimit}}{{FileSize .Repository.SizeLimit}}{{end}}"
data-tooltip-content='{{ctx.Locale.Tr "repo.repo_size_limit_helper"}}'>
</div>
<div class="field {{if .Err_LFSSizeLimit}}error{{end}} {{if not .IsAdmin}}tw-hidden{{end}}">
<label for="lfs_size_limit">{{ctx.Locale.Tr "repo.lfs_size_limit"}}</label>
<input id="lfs_size_limit" name="lfs_size_limit"
{{if and (eq .RepoSizeLimit -1) (eq .LFSSizeLimit -1)}}class="ui text light grey"{{end}}
type="text" value="{{if .LFSSizeLimitText}}{{.LFSSizeLimitText}}{{else if .Repository.LFSSizeLimit}}{{FileSize .Repository.LFSSizeLimit}}{{end}}"
data-tooltip-content='{{ctx.Locale.Tr "repo.lfs_size_limit_helper"}}'>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template"}}</label>
<div class="ui checkbox">

View File

@ -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)
}
}

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))

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")
})
}

View File

@ -0,0 +1,202 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"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)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
setting.Repository.GitSizeMax = -1
doCommitAndPush(t, 1024, dstPath, "under-")
})
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)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
setting.Repository.GitSizeMax = 100
doCommitAndPushWithExpectedError(t, 1024, dstPath, "over-")
setting.Repository.GitSizeMax = -1
})
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)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
setting.Repository.GitSizeMax = 1024 * 100
doCommitAndPush(t, 1024, dstPath, "resize-")
setting.Repository.GitSizeMax = -1
})
t.Run("Deletion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
repoName := "repo-git-delete"
ctx := NewAPITestContext(t, username, repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
setting.Repository.GitSizeMax = -1
doCommitAndPush(t, 1024, dstPath, "delete-base-")
bigFileName := doCommitAndPush(t, 1024*10, dstPath, "delete-big-")
lastCommitID := doGetAddCommitID(t, dstPath, bigFileName)
doDeleteAndPush(t, dstPath, bigFileName)
doRebaseCommitAndPush(t, dstPath, lastCommitID)
})
t.Run("SoftEnforcement", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
repoName := "repo-git-soft"
ctx := NewAPITestContext(t, username, repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
setting.Repository.GitSizeMax = -1
doCommitAndPush(t, 1024, dstPath, "soft-base-")
// Lenient pass for soft enforcement
doCommitAndPush(t, 1024, dstPath, "soft-more-")
})
setting.Repository.GitSizeMax = -1
}
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)
}
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)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
setupLFS(t, dstPath)
setting.Repository.LFSSizeMax = 10000
doCommitAndPushWithData(t, dstPath, "data-under.dat", "some-content-under")
setting.Repository.LFSSizeMax = -1
})
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)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
setupLFS(t, dstPath)
setting.Repository.LFSSizeMax = 5
doCommitAndPushWithDataWithExpectedError(t, dstPath, "data-over.dat", "some-content-over-limit")
setting.Repository.LFSSizeMax = -1
})
t.Run("SoftEnforcement", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
repoName := "repo-lfs-soft"
ctx := NewAPITestContext(t, username, repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
doAPICreateRepository(ctx, false)(t)
dstPath := t.TempDir()
u.Path = ctx.GitPath()
doGitClone(dstPath, u)(t)
setupLFS(t, dstPath)
setting.Repository.LFSSizeMax = -1
doCommitAndPushWithData(t, dstPath, "data-soft.dat", "some-content-soft")
// Lenient pass for soft enforcement
doCommitAndPushWithData(t, dstPath, "data-soft-more.dat", "some-more-content")
})
setting.Repository.LFSSizeMax = -1
}
// Local helpers reuse existing logic but adapted for LFS tracking or direct data writing
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)
}
// Reuse global helpers for Git: doCommitAndPush, doCommitAndPushWithExpectedError, doDeleteAndPush, doRebaseCommitAndPush, doGetAddCommitID