diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 96582200ce..95160dd8e5 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -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 diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index cc801479fb..9975729fd6 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -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 } diff --git a/models/migrations/v1_26/v326.go b/models/migrations/v1_26/v326.go deleted file mode 100644 index 5fc9774cdb..0000000000 --- a/models/migrations/v1_26/v326.go +++ /dev/null @@ -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)) -} diff --git a/models/repo/repo.go b/models/repo/repo.go index 59b4fe627e..6ee0e9c860 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -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. diff --git a/modules/base/tool.go b/modules/base/tool.go index bae62c491c..8446f6904c 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -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. diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 1a09d3cd89..4e925ccc5a 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -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() diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 0dcc0096da..90ac1688e4 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -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"` diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index f336605066..83338ad7fe 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -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" -} +} \ No newline at end of file diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index ada70dda4c..bb6bda587d 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -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 diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index 0a78a45fc9..449769d085 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -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") } diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index 0697b27cf2..7b04340576 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -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") } diff --git a/routers/web/admin/repos_test.go b/routers/web/admin/repos_test.go deleted file mode 100644 index 17594fad2e..0000000000 --- a/routers/web/admin/repos_test.go +++ /dev/null @@ -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) -} diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 2e4682c246..bc2b0264c0 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -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) diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index ab3d28514e..2f18e5bf28 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -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 diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index ac5980984c..4d51a36ca3 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -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 diff --git a/services/lfs/server.go b/services/lfs/server.go index 2958c5eb7f..e375e0dd57 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -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) } diff --git a/services/repository/create.go b/services/repository/create.go index 129e7d1d54..bfac83419d 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -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, diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 9e9bea8e31..c70dfa9b15 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -1,28 +1,20 @@ {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin")}}

- {{ctx.Locale.Tr "admin.config.global_repo_size_limit_manage_panel"}} + {{ctx.Locale.Tr "admin.repos.settings"}}

{{.CsrfTokenHtml}} -
- - +
+ +
-
- - -
-
- -
- -
-
-
- +
+ +
+

diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index f0ddfd68db..c54ef79358 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -14,33 +14,19 @@
{{FileSize .Repository.Size}} - {{if and .ActualSizeLimit (or (gt .RepoSizeLimit -1) (gt .LFSSizeLimit -1))}} - / {{FileSize .ActualSizeLimit}} + {{if gt .GitSizeMax -1}} + / {{FileSize .GitSizeMax}} {{end}}
{{FileSize .Repository.LFSSize}} - {{if and .ActualLFSSizeLimit (or (gt .RepoSizeLimit -1) (gt .LFSSizeLimit -1))}} - / {{FileSize .ActualLFSSizeLimit}} + {{if gt .LFSSizeMax -1}} + / {{FileSize .LFSSizeMax}} {{end}}
-
- - -
-
- - -
diff --git a/tests/integration/api_helper_for_declarative_test.go b/tests/integration/api_helper_for_declarative_test.go index fa315aa259..a5caafedbb 100644 --- a/tests/integration/api_helper_for_declarative_test.go +++ b/tests/integration/api_helper_for_declarative_test.go @@ -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) - } -} diff --git a/tests/integration/git_general_test.go b/tests/integration/git_general_test.go index 195dbdc5c7..c6c2f60685 100644 --- a/tests/integration/git_general_test.go +++ b/tests/integration/git_general_test.go @@ -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)) diff --git a/tests/integration/lfs_size_limit_test.go b/tests/integration/lfs_size_limit_test.go deleted file mode 100644 index 08e000879c..0000000000 --- a/tests/integration/lfs_size_limit_test.go +++ /dev/null @@ -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") - }) -} diff --git a/tests/integration/size_limit_test.go b/tests/integration/size_limit_test.go new file mode 100644 index 0000000000..8f4fbb0ddf --- /dev/null +++ b/tests/integration/size_limit_test.go @@ -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