0
0
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:
DmitryFrolovTri 2026-01-22 10:31:57 +00:00
commit ca9489a137
No known key found for this signature in database
GPG Key ID: 0FBA4D49377CDDC3
23 changed files with 457 additions and 862 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,13 +18,6 @@ import (
"gitea.com/go-chi/binding"
)
// UpdateGlobalRepoFrom for updating global repository setting
type UpdateGlobalRepoFrom struct {
RepoSizeLimit string
LFSSizeLimit string
LFSSizeInRepoSize bool
}
// CreateRepoForm form for creating repository
type CreateRepoForm struct {
UID int64 `binding:"Required"`
@ -50,7 +43,11 @@ type CreateRepoForm struct {
ForkSingleBranch string
ObjectFormatName string
SizeLimit int64
}
type UpdateGlobalRepoFrom struct {
GitSizeMax string `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

View File

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

View File

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

View File

@ -1,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">

View File

@ -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">

View File

@ -468,33 +468,3 @@ func doAPIAddRepoToOrganizationTeam(ctx APITestContext, teamID int64, orgName, r
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
}
}
func doAPISetRepoSizeLimit(ctx APITestContext, owner, repo string, size int64) func(*testing.T) {
return func(t *testing.T) {
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s",
owner, repo)
req := NewRequestWithJSON(t, http.MethodPatch, urlStr, &api.EditRepoOption{SizeLimit: &size}).
AddTokenAuth(ctx.Token)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
ctx.Session.MakeRequest(t, req, 200)
}
}
func doAPISetRepoLFSSizeLimit(ctx APITestContext, owner, repo string, size int64) func(*testing.T) {
return func(t *testing.T) {
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s",
owner, repo)
req := NewRequestWithJSON(t, http.MethodPatch, urlStr, &api.EditRepoOption{LFSSizeLimit: &size}).
AddTokenAuth(ctx.Token)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
ctx.Session.MakeRequest(t, req, 200)
}
}

View File

@ -85,39 +85,6 @@ func testGitGeneral(t *testing.T, u *url.URL) {
rawTest(t, &httpContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
mediaTest(t, &httpContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
t.Run("SizeLimit", func(t *testing.T) {
dstForkedPath := t.TempDir()
setting.SaveGlobalRepositorySetting(0, 0, false)
t.Run("Under", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
doCommitAndPush(t, testFileSizeSmall, dstPath, "data-file-")
})
t.Run("Over", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
u.Path = forkedUserCtx.GitPath()
u.User = url.UserPassword(forkedUserCtx.Username, userPassword)
t.Run("Clone", doGitClone(dstForkedPath, u))
t.Run("APISetRepoSizeLimit", doAPISetRepoSizeLimit(forkedUserCtx, forkedUserCtx.Username, forkedUserCtx.Reponame, testFileSizeSmall))
doCommitAndPushWithExpectedError(t, testFileSizeLarge, dstForkedPath, "data-file-")
})
t.Run("UnderAfterResize", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
t.Run("APISetRepoSizeLimit", doAPISetRepoSizeLimit(forkedUserCtx, forkedUserCtx.Username, forkedUserCtx.Reponame, testFileSizeLarge*10))
doCommitAndPush(t, testFileSizeSmall, dstPath, "data-file-")
})
t.Run("Deletion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
doCommitAndPush(t, testFileSizeSmall, dstPath, "data-file-")
bigFileName := doCommitAndPush(t, testFileSizeLarge, dstPath, "data-file-")
oldRepoSize := doGetRemoteRepoSizeViaAPI(t, forkedUserCtx)
lastCommitID := doGetAddCommitID(t, dstPath, bigFileName)
doDeleteAndPush(t, dstPath, bigFileName)
doRebaseCommitAndPush(t, dstPath, lastCommitID)
newRepoSize := doGetRemoteRepoSizeViaAPI(t, forkedUserCtx)
assert.LessOrEqual(t, newRepoSize, oldRepoSize)
setting.SaveGlobalRepositorySetting(-1, -1, false)
})
})
t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "test/head"))
t.Run("CreateProtectedBranch", doCreateProtectedBranch(&httpContext, dstPath))
t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))

View File

@ -1,411 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/url"
"os"
"path/filepath"
"testing"
"time"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
func base62(n int64) string {
if n == 0 {
return string(alphabet[0])
}
var buf [11]byte
i := len(buf)
for n > 0 {
i--
buf[i] = alphabet[n%62]
n /= 62
}
return string(buf[i:])
}
// ID5: 5-char ID derived from unix seconds.
func ID5() string {
sec := time.Now().Unix()
s := base62(sec)
if len(s) < 5 {
return fmt.Sprintf("%05s", s)
}
return s[len(s)-5:]
}
// TestLFSSizeLimit exercises the pre-receive size limit logic.
// Runs sequentially (no t.Parallel) because it mutates GLOBAL settings.
//
// Enable/disable via:
//
// setting.SaveGlobalRepositorySetting(repoLimit, lfsLimit, lfsSizeInRepoSize)
//
// Scenarios:
// - repo-only limit: blocks git blobs but not LFS objects (when lfsSizeInRepoSize=false, lfsLimit=0)
// - LFS-only limit: blocks LFS objects but not git blobs
// - combined repo+LFS (LFS counted into repo size): blocks LFS when it would exceed repo limit
// - per-repo LFS override wins over global default (retry same push after increasing per-repo LFS limit)
func TestLFSSizeLimit(t *testing.T) {
onGiteaRun(t, testLFSSizeLimit)
}
func testLFSSizeLimit(t *testing.T, baseURL *url.URL) {
// Always disable at the end so we don't leak to other integration tests.
t.Cleanup(func() {
setting.SaveGlobalRepositorySetting(-1, -1, false)
})
if !setting.LFS.StartServer {
t.Skip("LFS server disabled")
}
// ---- sizes ----
// Repo-size checks are not precise (compression), so use very different sizes vs limit.
const (
repoLimitBytes = int64(64 * 1024) // 64KiB
gitUnderBytes = int(8 * 1024) // 8KiB
gitOverBytes = int(4 * 1024 * 1024) // 4MiB (far above limit)
lfsBigBytes = int(1 * 1024 * 1024) // 1MiB (should still pass repo-only when not counted)
)
// LFS-only checks are precise.
const (
lfsLimitBytes = int64(64 * 1024) // 64KiB
lfsUnderBytes = int(32 * 1024) // 32KiB
lfsOverBytes = int(128 * 1024) // 128KiB
)
// Combined repo+LFS (LFS counted into repo size).
const (
combinedRepoLimitBytes = int64(128 * 1024) // 128KiB
combinedLFSUnderBytes = int(64 * 1024) // 64KiB
combinedLFSOverBytes = int(512 * 1024) // 512KiB
)
// ---- helpers ----
newLimitRepo := func(t *testing.T, suffix string) APITestContext {
t.Helper()
// Make a unique repo name without relying on tests.GetTestUID / CreateRepo.
repoName := fmt.Sprintf(
"lfsl-%s-%s",
suffix,
ID5(),
)
ctx := NewAPITestContext(
t,
"user2",
repoName,
auth_model.AccessTokenScopeWriteRepository,
auth_model.AccessTokenScopeWriteUser,
)
// Create via API (same style as git_general_test.go)
doAPICreateRepository(ctx, false)(t)
return ctx
}
cloneHTTP := func(t *testing.T, ctx APITestContext) string {
t.Helper()
u := *baseURL // copy
u.Path = ctx.GitPath()
u.User = url.UserPassword(ctx.Username, userPassword)
dst := t.TempDir()
doGitClone(dst, &u)(t)
return dst
}
commitSignature := func() *git.Signature {
return &git.Signature{
Email: "user2@example.com",
Name: "User Two",
When: time.Now(),
}
}
configureLFSForPrefix := func(t *testing.T, repoPath, prefix string) {
t.Helper()
// git lfs install
err := gitcmd.NewCommand("lfs").AddArguments("install").WithDir(repoPath).Run(t.Context())
require.NoError(t, err)
// track prefix*
_, _, err = gitcmd.NewCommand("lfs").
AddArguments("track").
AddDynamicArguments(prefix + "*").
WithDir(repoPath).
RunStdString(t.Context())
require.NoError(t, err)
// commit .gitattributes
err = git.AddChanges(t.Context(), repoPath, false, ".gitattributes")
require.NoError(t, err)
sig := commitSignature()
err = git.CommitChanges(t.Context(), repoPath, git.CommitChangesOptions{
Committer: sig,
Author: sig,
Message: "configure LFS tracking",
})
require.NoError(t, err)
}
pushCurrentBranch := func(t *testing.T, repoPath string) error {
t.Helper()
_, _, err := gitcmd.NewCommand("push", "origin", "master").WithDir(repoPath).RunStdString(t.Context())
return err
}
// Push a git blob by creating a new random file (random-ish content reduces compression effects).
pushGitBlob := func(t *testing.T, ctx APITestContext, size int) error {
t.Helper()
repoPath := cloneHTTP(t, ctx)
_, err := generateCommitWithNewData(
t.Context(),
size,
repoPath,
"user2@example.com",
"User Two",
"git-data-",
)
require.NoError(t, err)
return pushCurrentBranch(t, repoPath)
}
// Push an LFS object by tracking prefix* and then committing a file created by generateCommitWithNewData.
pushLFSObjectOnce := func(t *testing.T, ctx APITestContext, size int) error {
t.Helper()
repoPath := cloneHTTP(t, ctx)
const prefix = "lfs-data-"
configureLFSForPrefix(t, repoPath, prefix)
_, err := generateCommitWithNewData(
t.Context(),
size,
repoPath,
"user2@example.com",
"User Two",
prefix,
)
require.NoError(t, err)
return pushCurrentBranch(t, repoPath)
}
// Prepare a single LFS commit in a clone and return the clone path, so we can:
// - push (expect fail)
// - change limits
// - push SAME COMMIT again (expect pass)
prepareLFSPushForRetry := func(t *testing.T, ctx APITestContext, size int) string {
t.Helper()
repoPath := cloneHTTP(t, ctx)
const prefix = "lfs-retry-"
configureLFSForPrefix(t, repoPath, prefix)
_, err := generateCommitWithNewData(
t.Context(),
size,
repoPath,
"user2@example.com",
"User Two",
prefix,
)
require.NoError(t, err)
// Ensure the repoPath is used (avoid accidental cleanup / unused).
_, err = os.Stat(filepath.Join(repoPath, ".git"))
require.NoError(t, err)
return repoPath
}
runGlobalThenPerRepo := func(
t *testing.T,
name string,
global func(t *testing.T),
perRepo func(t *testing.T),
) {
t.Helper()
t.Run(name+"/Global", func(t *testing.T) { global(t) })
t.Run(name+"/PerRepo", func(t *testing.T) { perRepo(t) })
}
// ---- tests ----
runGlobalThenPerRepo(t,
"GlobalAndRepoOnlyLimit_BlocksGitButNotLFS",
func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Global: repo limit ON, LFS limit OFF, LFS not counted into repo size.
setting.SaveGlobalRepositorySetting(repoLimitBytes, 0, false)
ctx := newLimitRepo(t, "ggit")
// push over on git (NOK)
err := pushGitBlob(t, ctx, gitOverBytes)
assert.Error(t, err, "git file well over repo limit should be rejected")
// push LFS (OK)
err = pushLFSObjectOnce(t, ctx, lfsBigBytes)
assert.NoError(t, err, "LFS push should be allowed when repo-only limit is enabled and LFSSizeInRepoSize=false")
// push under on git (OK)
err = pushGitBlob(t, ctx, gitUnderBytes)
assert.NoError(t, err, "small git file should be accepted if under repo limit")
},
func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
setting.SaveGlobalRepositorySetting(0, 0, false)
ctx := newLimitRepo(t, "rgit")
// set per-repo repo limit
t.Run("APISetRepoSizeLimit", doAPISetRepoSizeLimit(ctx, ctx.Username, ctx.Reponame, repoLimitBytes))
// push over on git (NOK)
err := pushGitBlob(t, ctx, gitOverBytes)
assert.Error(t, err, "git file well over per-repo repo limit should be rejected")
// push LFS (OK)
err = pushLFSObjectOnce(t, ctx, lfsBigBytes)
assert.NoError(t, err, "LFS push should be allowed when only per-repo repo limit is set and LFSSizeInRepoSize=false")
// push under on git (OK)
err = pushGitBlob(t, ctx, gitUnderBytes)
assert.NoError(t, err, "small git file should be accepted under per-repo repo limit")
},
)
runGlobalThenPerRepo(t,
"GlobalAndLFSOnlyLimit_BlocksLFSButNotGit",
func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Global: LFS limit ON, repo limit OFF, LFS not counted into repo size.
setting.SaveGlobalRepositorySetting(0, lfsLimitBytes, false)
ctx := newLimitRepo(t, "glfs")
// push on git (OK)
err := pushGitBlob(t, ctx, gitOverBytes)
assert.NoError(t, err, "git push should be allowed when repo limit is 0")
// push over on LFS (NOK)
err = pushLFSObjectOnce(t, ctx, lfsOverBytes)
assert.Error(t, err, "LFS push above global LFS limit must be rejected")
// push under on LFS (OK) (under is last)
err = pushLFSObjectOnce(t, ctx, lfsUnderBytes)
assert.NoError(t, err, "LFS push under global LFS limit must be accepted")
},
func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
setting.SaveGlobalRepositorySetting(0, 0, false)
ctx := newLimitRepo(t, "rlfs")
// set per-repo LFS limit
t.Run("APISetRepoLFSSizeLimit", doAPISetRepoLFSSizeLimit(ctx, ctx.Username, ctx.Reponame, lfsLimitBytes))
// push on git (OK)
err := pushGitBlob(t, ctx, gitOverBytes)
assert.NoError(t, err, "git push should be allowed when repo limit is 0")
// push over on LFS (NOK)
err = pushLFSObjectOnce(t, ctx, lfsOverBytes)
assert.Error(t, err, "LFS push above per-repo LFS limit must be rejected")
},
)
runGlobalThenPerRepo(t,
"GlobalOrCombinedRepoAndLFSLimits_BlocksLFSandGit",
func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Global: repo limit ON, no LFS-specific limit, but LFS counted into repo size.
setting.SaveGlobalRepositorySetting(combinedRepoLimitBytes, 0, true)
ctx := newLimitRepo(t, "gc")
// push over via LFS (NOK)
err := pushLFSObjectOnce(t, ctx, combinedLFSOverBytes)
assert.Error(t, err, "LFS push must be rejected when it would exceed repo limit and LFSSizeInRepoSize=true")
// push under via LFS (OK)
err = pushLFSObjectOnce(t, ctx, combinedLFSUnderBytes)
assert.NoError(t, err, "LFS push under repo limit must be accepted when LFSSizeInRepoSize=true")
},
func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Per-repo: enable checking globally with LFSSizeInRepoSize=true but no global limits.
setting.SaveGlobalRepositorySetting(0, 0, true)
ctx := newLimitRepo(t, "rc")
// set per-repo repo limit
t.Run("APISetRepoSizeLimit", doAPISetRepoSizeLimit(ctx, ctx.Username, ctx.Reponame, combinedRepoLimitBytes))
// push over via LFS (NOK)
err := pushLFSObjectOnce(t, ctx, combinedLFSOverBytes)
assert.Error(t, err, "LFS push must be rejected when it would exceed per-repo repo limit and LFSSizeInRepoSize=true")
// push under via LFS (OK)
err = pushLFSObjectOnce(t, ctx, combinedLFSUnderBytes)
assert.NoError(t, err, "LFS push under per-repo repo limit must be accepted when LFSSizeInRepoSize=true")
},
)
t.Run("PerRepoLFSOverride_WinsOverGlobalDefault", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Global: strict LFS limit, no repo limit.
setting.SaveGlobalRepositorySetting(-1, lfsLimitBytes, false)
ctx := newLimitRepo(t, "rwing")
// Prepare one LFS commit and keep the clone so we can retry pushing SAME commit.
repoPath := prepareLFSPushForRetry(t, ctx, lfsOverBytes)
// First push must fail.
err := pushCurrentBranch(t, repoPath)
assert.Error(t, err, "push must fail under strict global LFS limit")
// Increase per-repo LFS limit to allow the previously rejected object.
// Use a clearly larger limit than the file size.
newPerRepoLimit := int64(lfsOverBytes) * 4
t.Run("APISetRepoLFSSizeLimit", doAPISetRepoLFSSizeLimit(ctx, ctx.Username, ctx.Reponame, newPerRepoLimit))
// Retry pushing SAME commit must succeed now.
err = pushCurrentBranch(t, repoPath)
assert.NoError(t, err, "per-repo LFS limit must override global default and allow previously rejected push")
})
}

View File

@ -0,0 +1,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