Skip to content

Limit private repos #12787

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions integrations/org_count_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,12 @@ func doCheckOrgCounts(username string, orgCounts map[string]int, strict bool, ca
calcOrgCounts := map[string]int{}

for _, org := range user.Orgs {
calcOrgCounts[org.LowerName] = org.NumRepos
calcOrgCounts[org.LowerName] = org.NumPublicRepos + org.NumPrivateRepos
count, ok := canonicalCounts[org.LowerName]
if ok {
assert.True(t, count == org.NumRepos, "Number of Repos in %s is %d when we expected %d", org.Name, org.NumRepos, count)
assert.True(t, count == org.NumPublicRepos+org.NumPrivateRepos, "Number of Repos in %s is %d when we expected %d", org.Name, org.NumPublicRepos+org.NumPrivateRepos, count)
} else {
assert.False(t, strict, "Did not expect to see %s with count %d", org.Name, org.NumRepos)
assert.False(t, strict, "Did not expect to see %s with count %d", org.Name, org.NumPublicRepos+org.NumPrivateRepos)
}
}

Expand Down
2 changes: 1 addition & 1 deletion models/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
if err != nil {
return nil, fmt.Errorf("AccessibleReposEnv: %v", err)
}
if repoIDs, err = env.RepoIDs(1, opts.RequestedUser.NumRepos); err != nil {
if repoIDs, err = env.RepoIDs(1, opts.RequestedUser.NumPublicRepos+opts.RequestedUser.NumPrivateRepos); err != nil {
return nil, fmt.Errorf("GetUserRepositories: %v", err)
}

Expand Down
3 changes: 2 additions & 1 deletion models/consistency.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ func assertCount(t *testing.T, bean interface{}, expected int) {
}

func (user *User) checkForConsistency(t *testing.T) {
assertCount(t, &Repository{OwnerID: user.ID}, user.NumRepos)
assertCount(t, &Repository{OwnerID: user.ID, IsPrivate: false}, user.NumPublicRepos)
assertCount(t, &Repository{OwnerID: user.ID, IsPrivate: true}, user.NumPrivateRepos)
assertCount(t, &Star{UID: user.ID}, user.NumStars)
assertCount(t, &OrgUser{OrgID: user.ID}, user.NumMembers)
assertCount(t, &Team{OrgID: user.ID}, user.NumTeams)
Expand Down
5 changes: 3 additions & 2 deletions models/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ func CreateOrganization(org, owner *User) (err error) {
return err
}
org.UseCustomAvatar = true
org.MaxRepoCreation = -1
org.MaxPublicRepoCreation = -1
org.MaxPrivateRepoCreation = -1
org.NumTeams = 1
org.NumMembers = 1
org.Type = UserTypeOrganization
Expand Down Expand Up @@ -622,7 +623,7 @@ func removeOrgUser(sess *xorm.Session, orgID, userID int64) error {
if err != nil {
return fmt.Errorf("AccessibleReposEnv: %v", err)
}
repoIDs, err := env.RepoIDs(1, org.NumRepos)
repoIDs, err := env.RepoIDs(1, org.NumPublicRepos+org.NumPrivateRepos)
if err != nil {
return fmt.Errorf("GetUserRepositories [%d]: %v", userID, err)
}
Expand Down
23 changes: 19 additions & 4 deletions models/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -1010,7 +1010,7 @@ func (repo *Repository) CloneLink() (cl *CloneLink) {
// CheckCreateRepository check if could created a repository
func CheckCreateRepository(doer, u *User, name string) error {
if !doer.CanCreateRepo() {
return ErrReachLimitOfRepo{u.MaxRepoCreation}
return ErrReachLimitOfRepo{u.MaxCreationLimit()}
}

if err := IsUsableRepoName(name); err != nil {
Expand Down Expand Up @@ -1135,10 +1135,17 @@ func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error
return fmt.Errorf("updateUser: %v", err)
}

if _, err = ctx.e.Incr("num_repos").ID(u.ID).Update(new(User)); err != nil {
return fmt.Errorf("increment user total_repos: %v", err)
if !repo.IsPrivate {
if _, err = ctx.e.Incr("num_public_repos").ID(u.ID).Update(new(User)); err != nil {
return fmt.Errorf("increment user num_public_repos: %v", err)
}
u.NumPublicRepos++
} else {
if _, err = ctx.e.Incr("num_private_repos").ID(u.ID).Update(new(User)); err != nil {
return fmt.Errorf("increment user num_private_repos: %v", err)
}
u.NumPrivateRepos++
}
u.NumRepos++

// Give access to all members in teams with access to all repositories.
if u.IsOrganization() {
Expand Down Expand Up @@ -1415,6 +1422,14 @@ func GetRepositoriesByForkID(forkID int64) ([]*Repository, error) {
}

func updateRepository(e Engine, repo *Repository, visibilityChanged bool) (err error) {
if visibilityChanged {
if repo.IsPrivate && !repo.Owner.CanCreatePrivateRepo() {
return ErrReachLimitOfRepo{repo.Owner.MaxPrivateCreationLimit()}
} else if !repo.Owner.CanCreatePublicRepo() {
return ErrReachLimitOfRepo{repo.Owner.MaxPublicCreationLimit()}
}
}

repo.LowerName = strings.ToLower(repo.Name)

if utf8.RuneCountInString(repo.Description) > 255 {
Expand Down
79 changes: 60 additions & 19 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,9 @@ type User struct {

// Remember visibility choice for convenience, true for private
LastRepoVisibility bool
// Maximum repository creation limit, -1 means use global default
MaxRepoCreation int `xorm:"NOT NULL DEFAULT -1"`
// Maximum repository creation limits, -1 means use global default
MaxPublicRepoCreation int `xorm:"NOT NULL DEFAULT -1"`
MaxPrivateRepoCreation int `xorm:"NOT NULL DEFAULT -1"`

// Permissions
IsActive bool `xorm:"INDEX"` // Activate primary email
Expand All @@ -149,10 +150,11 @@ type User struct {
UseCustomAvatar bool

// Counters
NumFollowers int
NumFollowing int `xorm:"NOT NULL DEFAULT 0"`
NumStars int
NumRepos int
NumFollowers int
NumFollowing int `xorm:"NOT NULL DEFAULT 0"`
NumStars int
NumPublicRepos int
NumPrivateRepos int

// For organization
NumTeams int
Expand Down Expand Up @@ -184,8 +186,12 @@ func (u *User) ColorFormat(s fmt.State) {

// BeforeUpdate is invoked from XORM before updating this object.
func (u *User) BeforeUpdate() {
if u.MaxRepoCreation < -1 {
u.MaxRepoCreation = -1
if u.MaxPublicRepoCreation < -1 {
u.MaxPublicRepoCreation = -1
}

if u.MaxPrivateRepoCreation < -1 {
u.MaxPrivateRepoCreation = -1
}

// Organization does not need email
Expand Down Expand Up @@ -272,27 +278,61 @@ func (u *User) HasForkedRepo(repoID int64) bool {
return has
}

// MaxCreationLimit returns the number of repositories a user is allowed to create
// MaxCreationLimit returns the total number of repositories a user is allowed to create, public and private
func (u *User) MaxCreationLimit() int {
if u.MaxRepoCreation <= -1 {
return setting.Repository.MaxCreationLimit
return u.MaxPublicCreationLimit() + u.MaxPrivateCreationLimit()
}

// MaxPublicCreationLimit returns the number of public repositories a user is allowed to create
func (u *User) MaxPublicCreationLimit() int {
if u.MaxPublicRepoCreation <= -1 {
return setting.Repository.MaxPublicCreationLimit
}
return u.MaxPublicRepoCreation
}

// MaxPrivateCreationLimit returns the number of private repositories a user is allowed to create
func (u *User) MaxPrivateCreationLimit() int {
if u.MaxPrivateRepoCreation <= -1 {
return setting.Repository.MaxPrivateCreationLimit
}
return u.MaxRepoCreation
return u.MaxPrivateRepoCreation
}

// CanCreateRepo returns if user login can create a repository
// CanCreateRepo returns if user login can create any type of repository
// NOTE: functions calling this assume a failure due to repository count limit; if new checks are added, those functions should be revised
func (u *User) CanCreateRepo() bool {
return u.CanCreatePublicRepo() || u.CanCreatePrivateRepo()
}

// CanCreatePublicRepo returns if user login can create a public repository
// NOTE: functions calling this assume a failure due to repository count limit; if new checks are added, those functions should be revised
func (u *User) CanCreatePublicRepo() bool {
if u.IsAdmin {
return true
}
if u.MaxPublicRepoCreation <= -1 {
if setting.Repository.MaxPublicCreationLimit <= -1 {
return true
}
return u.NumPublicRepos < setting.Repository.MaxPublicCreationLimit
}
return u.NumPublicRepos < u.MaxPublicRepoCreation
}

// CanCreatePrivateRepo returns if user login can create a private repository
// NOTE: functions calling this assume a failure due to repository count limit; if new checks are added, those functions should be revised
func (u *User) CanCreatePrivateRepo() bool {
if u.IsAdmin {
return true
}
if u.MaxRepoCreation <= -1 {
if setting.Repository.MaxCreationLimit <= -1 {
if u.MaxPrivateRepoCreation <= -1 {
if setting.Repository.MaxPrivateCreationLimit <= -1 {
return true
}
return u.NumRepos < setting.Repository.MaxCreationLimit
return u.NumPrivateRepos < setting.Repository.MaxPrivateCreationLimit
}
return u.NumRepos < u.MaxRepoCreation
return u.NumPrivateRepos < u.MaxPrivateRepoCreation
}

// CanCreateOrganization returns true if user can create organisation.
Expand Down Expand Up @@ -754,7 +794,7 @@ func (u *User) GetOrganizations(opts *SearchOrganizationsOptions) error {

orgs := make([]*User, len(orgCounts))
for i, orgCount := range orgCounts {
orgCount.User.NumRepos = orgCount.OrgCount
orgCount.User.NumPublicRepos = orgCount.OrgCount
orgs[i] = &orgCount.User
}

Expand Down Expand Up @@ -1004,7 +1044,8 @@ func CreateUser(u *User) (err error) {
u.HashPassword(u.Passwd)
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
u.MaxRepoCreation = -1
u.MaxPublicRepoCreation = -1
u.MaxPrivateRepoCreation = -1
u.Theme = setting.UI.DefaultTheme

if _, err = sess.Insert(u); err != nil {
Expand Down
3 changes: 2 additions & 1 deletion modules/auth/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ type AdminEditUserForm struct {
Password string `binding:"MaxSize(255)"`
Website string `binding:"ValidUrl;MaxSize(255)"`
Location string `binding:"MaxSize(50)"`
MaxRepoCreation int
MaxPublicRepoCreation int
MaxPrivateRepoCreation int
Active bool
Admin bool
Restricted bool
Expand Down
3 changes: 2 additions & 1 deletion modules/auth/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ type UpdateOrgSettingForm struct {
Website string `binding:"ValidUrl;MaxSize(255)"`
Location string `binding:"MaxSize(50)"`
Visibility structs.VisibleType
MaxRepoCreation int
MaxPublicRepoCreation int
MaxPrivateRepoCreation int
RepoAdminChangeTeamAccess bool
}

Expand Down
13 changes: 10 additions & 3 deletions modules/repository/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ import (

// CreateRepository creates a repository for the user/organization.
func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *models.Repository, err error) {
if !doer.IsAdmin && !u.CanCreateRepo() {
return nil, models.ErrReachLimitOfRepo{
Limit: u.MaxRepoCreation,
if !doer.IsAdmin {
if opts.IsPrivate && !u.CanCreatePrivateRepo() {
return nil, models.ErrReachLimitOfRepo{
Limit: u.MaxPrivateRepoCreation,
}
}
if !opts.IsPrivate && !u.CanCreatePublicRepo() {
return nil, models.ErrReachLimitOfRepo{
Limit: u.MaxPublicRepoCreation,
}
}
}

Expand Down
9 changes: 6 additions & 3 deletions modules/setting/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ var (
AnsiCharset string
ForcePrivate bool
DefaultPrivate string
MaxCreationLimit int
MaxPublicCreationLimit int
MaxPrivateCreationLimit int
MirrorQueueLength int
PullRequestQueueLength int
PreferredLicenses []string
Expand Down Expand Up @@ -131,7 +132,8 @@ var (
AnsiCharset: "",
ForcePrivate: false,
DefaultPrivate: RepoCreatingLastUserVisibility,
MaxCreationLimit: -1,
MaxPublicCreationLimit: -1,
MaxPrivateCreationLimit: -1,
MirrorQueueLength: 1000,
PullRequestQueueLength: 1000,
PreferredLicenses: []string{"Apache License 2.0,MIT License"},
Expand Down Expand Up @@ -241,7 +243,8 @@ func newRepository() {
sec := Cfg.Section("repository")
Repository.DisableHTTPGit = sec.Key("DISABLE_HTTP_GIT").MustBool()
Repository.UseCompatSSHURI = sec.Key("USE_COMPAT_SSH_URI").MustBool()
Repository.MaxCreationLimit = sec.Key("MAX_CREATION_LIMIT").MustInt(-1)
Repository.MaxPublicCreationLimit = sec.Key("MAX_PUBLIC_CREATION_LIMIT").MustInt(-1)
Repository.MaxPrivateCreationLimit = sec.Key("MAX_PRIVATE_CREATION_LIMIT").MustInt(-1)
Repository.DefaultBranch = sec.Key("DEFAULT_BRANCH").MustString("master")
RepoRootPath = sec.Key("ROOT").MustString(path.Join(homeDir, "gitea-repositories"))
forcePathSeparator(RepoRootPath)
Expand Down
3 changes: 2 additions & 1 deletion modules/structs/admin_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ type EditUserOption struct {
Admin *bool `json:"admin"`
AllowGitHook *bool `json:"allow_git_hook"`
AllowImportLocal *bool `json:"allow_import_local"`
MaxRepoCreation *int `json:"max_repo_creation"`
MaxPublicRepoCreation *int `json:"max_public_repo_creation"`
MaxPrivateRepoCreation *int `json:"max_private_repo_creation"`
ProhibitLogin *bool `json:"prohibit_login"`
AllowCreateOrganization *bool `json:"allow_create_organization"`
}
9 changes: 6 additions & 3 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1999,7 +1999,8 @@ users.activated = Activated
users.admin = Admin
users.restricted = Restricted
users.2fa = 2FA
users.repos = Repos
users.publicrepos = Public Repos
users.privaterepos = Private Repos
users.created = Created
users.last_login = Last Sign-In
users.never_login = Never Signed-In
Expand All @@ -2012,8 +2013,10 @@ users.auth_login_name = Authentication Sign-In Name
users.password_helper = Leave the password empty to keep it unchanged.
users.update_profile_success = The user account has been updated.
users.edit_account = Edit User Account
users.max_repo_creation = Maximum Number of Repositories
users.max_repo_creation_desc = (Enter -1 to use the global default limit.)
users.max_public_repo_creation = Maximum Number of Public Repositories
users.max_public_repo_creation_desc = (Enter -1 to use the global default limit.)
users.max_private_repo_creation = Maximum Number of Private Repositories
users.max_private_repo_creation_desc = (Enter -1 to use the global default limit.)
Comment on lines +2016 to +2019
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose that means "this violates DRY" or some such? 😄

users.is_activated = User Account Is Activated
users.prohibit_login = Disable Sign-In
users.is_admin = Is Administrator
Expand Down
3 changes: 2 additions & 1 deletion routers/admin/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ func EditUserPost(ctx *context.Context, form auth.AdminEditUserForm) {
u.Email = form.Email
u.Website = form.Website
u.Location = form.Location
u.MaxRepoCreation = form.MaxRepoCreation
u.MaxPublicRepoCreation = form.MaxPublicRepoCreation
u.MaxPrivateRepoCreation = form.MaxPrivateRepoCreation
u.IsActive = form.Active
u.IsAdmin = form.Admin
u.IsRestricted = form.Restricted
Expand Down
7 changes: 5 additions & 2 deletions routers/api/v1/admin/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,11 @@ func EditUser(ctx *context.APIContext, form api.EditUserOption) {
if form.AllowImportLocal != nil {
u.AllowImportLocal = *form.AllowImportLocal
}
if form.MaxRepoCreation != nil {
u.MaxRepoCreation = *form.MaxRepoCreation
if form.MaxPublicRepoCreation != nil {
u.MaxPublicRepoCreation = *form.MaxPublicRepoCreation
}
if form.MaxPrivateRepoCreation != nil {
u.MaxPrivateRepoCreation = *form.MaxPrivateRepoCreation
}
if form.AllowCreateOrganization != nil {
u.AllowCreateOrganization = *form.AllowCreateOrganization
Expand Down
5 changes: 3 additions & 2 deletions routers/org/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ func SettingsPost(ctx *context.Context, form auth.UpdateOrgSettingForm) {
org.LowerName = strings.ToLower(form.Name)

if ctx.User.IsAdmin {
org.MaxRepoCreation = form.MaxRepoCreation
org.MaxPublicRepoCreation = form.MaxPublicRepoCreation
org.MaxPrivateRepoCreation = form.MaxPrivateRepoCreation
}

org.FullName = form.FullName
Expand All @@ -97,7 +98,7 @@ func SettingsPost(ctx *context.Context, form auth.UpdateOrgSettingForm) {

// update forks visibility
if visibilityChanged {
if err := org.GetRepositories(models.ListOptions{Page: 1, PageSize: org.NumRepos}); err != nil {
if err := org.GetRepositories(models.ListOptions{Page: 1, PageSize: org.NumPublicRepos + org.NumPrivateRepos}); err != nil {
ctx.ServerError("GetRepositories", err)
return
}
Expand Down
2 changes: 1 addition & 1 deletion routers/repo/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func handleMigrateError(ctx *context.Context, owner *models.User, err error, nam
case migrations.IsTwoFactorAuthError(err):
ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form)
case models.IsErrReachLimitOfRepo(err):
ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form)
ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", err.(models.ErrReachLimitOfRepo).Limit), tpl, form)
case models.IsErrRepoAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
Expand Down
Loading