mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-10 03:43:16 +02:00
merge of upstream/main + limitsize w/o DB
This commit is contained in:
commit
ca9489a137
@ -1081,22 +1081,17 @@ 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 - limit to zero bytes
|
||||
;; Standard units of measurements for size can be used like B, KB, KiB, ... , EB, EiB, etc or if not provided - bytes
|
||||
;; If limit is reached and operation doesn't increase disk consumption operation would be allowed
|
||||
;; 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 - limit to zero bytes
|
||||
;; Standard units of measurements for size can be used like B, KB, KiB, ... , EB, EiB, etc or if not provided - bytes
|
||||
;; If limit is reached and operation doesn't increase disk consumption operation would be allowed
|
||||
;; 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
|
||||
|
||||
@ -400,7 +400,6 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency),
|
||||
newMigration(324, "Fix closed milestone completeness for milestones with no issues", v1_26.FixClosedMilestoneCompleteness),
|
||||
newMigration(325, "Fix missed repo_id when migrate attachments", v1_26.FixMissedRepoIDWhenMigrateAttachments),
|
||||
newMigration(326, "Add support for repository size limit - SizeLimit and LFSSizeLimit columns to repository table", v1_26.AddSizeLimitOnRepo),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -93,21 +93,18 @@ func CreateTimeLimitCode[T time.Time | string](data string, minutes int, startTi
|
||||
|
||||
// FileSize calculates the file size and generate user-friendly string.
|
||||
func FileSize(s int64) string {
|
||||
if s == -1 {
|
||||
return "-1"
|
||||
}
|
||||
return humanize.IBytes(uint64(s))
|
||||
}
|
||||
|
||||
// Get FileSize bytes value from String.
|
||||
// GetFileSize gets FileSize bytes value from String.
|
||||
func GetFileSize(s string) (int64, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "-1" {
|
||||
return -1, nil
|
||||
// default to bytes if no unit is provided
|
||||
if _, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||
s += " B"
|
||||
}
|
||||
v, err := humanize.ParseBytes(s)
|
||||
iv := int64(v)
|
||||
return iv, err
|
||||
return int64(v), err
|
||||
}
|
||||
|
||||
// StringsToInt64s converts a slice of string to a slice of int64.
|
||||
|
||||
@ -56,6 +56,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 +278,33 @@ 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
|
||||
func UpdateGlobalRepositoryLimit(gitSizeMax, lfsSizeMax int64) {
|
||||
Repository.GitSizeMax = gitSizeMax
|
||||
Repository.LFSSizeMax = lfsSizeMax
|
||||
}
|
||||
|
||||
cfg, err := CfgProvider.PrepareSaving()
|
||||
if err != nil {
|
||||
return err
|
||||
// FormatRepositorySizeLimit returns "-1" for disabled limits, otherwise returns human-readable size.
|
||||
func FormatRepositorySizeLimit(sizeInBytes int64) string {
|
||||
if sizeInBytes == -1 {
|
||||
return "-1"
|
||||
}
|
||||
return humanize.IBytes(uint64(sizeInBytes))
|
||||
}
|
||||
|
||||
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)))
|
||||
// ParseRepositorySizeLimit accepts "-1" to disable the limit, otherwise parses as a byte size.
|
||||
func ParseRepositorySizeLimit(s string) (int64, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "-1" {
|
||||
return -1, nil
|
||||
}
|
||||
if LFSSizeLimit == -1 {
|
||||
sec.Key("LFS_SIZE_LIMIT").SetValue("-1")
|
||||
} else {
|
||||
sec.Key("LFS_SIZE_LIMIT").SetValue(humanize.Bytes(uint64(LFSSizeLimit)))
|
||||
// default to bytes if no unit is provided
|
||||
if _, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||
s += " B"
|
||||
}
|
||||
sec.Key("LFS_SIZE_IN_REPO_SIZE").SetValue(strconv.FormatBool(LFSSizeInRepoSize))
|
||||
return cfg.Save()
|
||||
v, err := humanize.ParseBytes(s)
|
||||
return int64(v), err
|
||||
}
|
||||
|
||||
func parseSize(sec ConfigSection, key string, def int64) int64 {
|
||||
@ -328,9 +327,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()
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -1082,6 +1082,7 @@
|
||||
"repo.form.name_reserved": "The repository name \"%s\" is reserved.",
|
||||
"repo.form.name_pattern_not_allowed": "The pattern \"%s\" is not allowed in a repository name.",
|
||||
"repo.need_auth": "Authorization",
|
||||
"repo.lfs_size": "LFS Size",
|
||||
"repo.migrate_options": "Migration Options",
|
||||
"repo.migrate_service": "Migration Service",
|
||||
"repo.migrate_options_mirror_helper": "This repository will be a mirror",
|
||||
@ -3025,8 +3026,15 @@
|
||||
"admin.repos.name": "Name",
|
||||
"admin.repos.private": "Private",
|
||||
"admin.repos.issues": "Issues",
|
||||
"admin.repos.size": "Size",
|
||||
"admin.repos.size": "Git Size",
|
||||
"admin.repos.lfs_size": "LFS Size",
|
||||
"admin.repos.settings": "Global Repository Settings",
|
||||
"admin.repos.git_size_max": "Max Git Size (Global), Bytes",
|
||||
"admin.repos.git_size_max_helper": "Maximum Git size allowed for a single repository. Set to -1 for unlimited.",
|
||||
"admin.repos.lfs_size_max": "Max LFS Size (Global), Bytes",
|
||||
"admin.repos.lfs_size_max_helper": "Maximum LFS size allowed for a single repository. Set to -1 for unlimited.",
|
||||
"admin.repos.update_settings": "Update Settings",
|
||||
"admin.repos.update_success": "Global repository limits have been updated.",
|
||||
"admin.packages.package_manage_panel": "Package Management",
|
||||
"admin.packages.total_size": "Total Size: %s",
|
||||
"admin.packages.unreferenced_size": "Unreferenced Size: %s",
|
||||
@ -3729,4 +3737,4 @@
|
||||
"git.filemode.executable_file": "Executable",
|
||||
"git.filemode.symbolic_link": "Symlink",
|
||||
"git.filemode.submodule": "Submodule"
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -396,8 +396,6 @@ func sumLFSSizes(m map[string]int64) int64 {
|
||||
return s
|
||||
}
|
||||
|
||||
// scanLFSPointersFromObjectIDs finds LFS pointer blobs among objectIDs and returns map[oid]size.
|
||||
// It only reads small blobs via cat-file, so it stays bounded.
|
||||
// scanLFSPointersFromObjectIDs finds LFS pointer blobs among objectIDs and returns map[oid]size.
|
||||
// It only reads small blobs via cat-file, so it stays bounded.
|
||||
func scanLFSPointersFromObjectIDs(ctx *gitea_context.PrivateContext, repoPath string, env, objectIDs []string, maxBlobSize int64) (map[string]int64, error) {
|
||||
@ -524,13 +522,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()
|
||||
@ -561,7 +552,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 {
|
||||
@ -801,7 +792,6 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) {
|
||||
|
||||
currentGit := repo.GitSize
|
||||
currentLFS := repo.LFSSize
|
||||
currentCombined := currentGit + currentLFS
|
||||
|
||||
gitDelta := addedSize - removedSize
|
||||
predictedGitAfter := currentGit + gitDelta
|
||||
@ -809,44 +799,30 @@ 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),
|
||||
setting.FormatRepositorySizeLimit(setting.Repository.GitSizeMax),
|
||||
setting.FormatRepositorySizeLimit(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,
|
||||
)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("LFS size limit exceeded: %s > then limit of %s",
|
||||
UserMsg: fmt.Sprintf("LFS size limit exceeded: %s > than limit of %s",
|
||||
base.FileSize(predictedLFSAfter),
|
||||
base.FileSize(lfsLimit),
|
||||
),
|
||||
@ -855,17 +831,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),
|
||||
),
|
||||
@ -874,25 +850,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")
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,6 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -33,9 +32,11 @@ 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
|
||||
gitSizeStr := setting.FormatRepositorySizeLimit(setting.Repository.GitSizeMax)
|
||||
lfsSizeStr := setting.FormatRepositorySizeLimit(setting.Repository.LFSSizeMax)
|
||||
log.Trace("Repos: GitSizeMax=%d -> %s, LFSSizeMax=%d -> %s", setting.Repository.GitSizeMax, gitSizeStr, setting.Repository.LFSSizeMax, lfsSizeStr)
|
||||
ctx.Data["GitSizeMax"] = gitSizeStr
|
||||
ctx.Data["LFSSizeMax"] = lfsSizeStr
|
||||
|
||||
explore.RenderRepoSearch(ctx, &explore.RepoSearchOptions{
|
||||
Private: true,
|
||||
@ -46,28 +47,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 := setting.ParseRepositorySizeLimit(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 +66,9 @@ func UpdateRepoPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
lfsSizeLimit, err := base.GetFileSize(form.LFSSizeLimit)
|
||||
lfsSizeMax, err := setting.ParseRepositorySizeLimit(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 +78,10 @@ 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)
|
||||
log.Trace("UpdateRepoPost: After update, setting.Repository.GitSizeMax=%d, LFSSizeMax=%d", setting.Repository.GitSizeMax, setting.Repository.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")
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 `form:"GitSizeMax"`
|
||||
LFSSizeMax string `form:"LFSSizeMax"`
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@ -186,13 +186,13 @@ func DownloadHandler(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- tracing helpers ----
|
||||
|
||||
// traceBatchDecision outputs a trace message for a batch decision
|
||||
func traceBatchDecision(rc *requestContext, op, msg string, args ...any) {
|
||||
prefix := fmt.Sprintf("LFS[BATCH][%s/%s][op=%s] ", rc.User, rc.Repo, op)
|
||||
log.Trace(prefix+msg, args...)
|
||||
}
|
||||
|
||||
// batchReqID returns a unique ID for a batch request
|
||||
func batchReqID(br *lfs_module.BatchRequest) string {
|
||||
h := sha1.New()
|
||||
h.Write([]byte(br.Operation))
|
||||
@ -244,92 +244,50 @@ 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,
|
||||
setting.FormatRepositorySizeLimit(repository.GetActualSizeLimit()),
|
||||
setting.FormatRepositorySizeLimit(repository.GetActualLFSSizeLimit()),
|
||||
)
|
||||
|
||||
// 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",
|
||||
reqID, base.FileSize(predictedLFS), base.FileSize(repository.GetActualLFSSizeLimit()),
|
||||
"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), setting.FormatRepositorySizeLimit(repository.GetActualLFSSizeLimit()),
|
||||
)
|
||||
writeStatusMessage(ctx, http.StatusForbidden,
|
||||
fmt.Sprintf("LFS size %s would exceed limit %s",
|
||||
base.FileSize(predictedLFS), base.FileSize(repository.GetActualLFSSizeLimit())))
|
||||
base.FileSize(predictedLFS), setting.FormatRepositorySizeLimit(repository.GetActualLFSSizeLimit())))
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,28 +1,20 @@
|
||||
{{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.repos.settings"}}
|
||||
</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 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}}" data-tooltip-content="{{ctx.Locale.Tr "admin.repos.git_size_max_helper"}}">
|
||||
</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 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}}" data-tooltip-content="{{ctx.Locale.Tr "admin.repos.lfs_size_max_helper"}}">
|
||||
</div>
|
||||
<button class="ui green button">{{ctx.Locale.Tr "admin.repos.update_settings"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
<h4 class="ui top attached header">
|
||||
|
||||
@ -14,33 +14,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">
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
tests/integration/size_limit_test.go
Normal file
338
tests/integration/size_limit_test.go
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user