Check user/org repo limit instead of doer (#34147)

This PR tries to finally fix the bug mentioned in #30011 and #15504,
where the user repo limit is checked when creating a repo in an
organization.

Fix #30011

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
This commit is contained in:
DrMaxNix
2025-04-08 06:45:31 +00:00
committed by GitHub
parent a100ac3306
commit fd7c364ca6
15 changed files with 92 additions and 71 deletions

View File

@ -178,12 +178,6 @@ func (org *Organization) HomeLink() string {
return org.AsUser().HomeLink() return org.AsUser().HomeLink()
} }
// CanCreateRepo returns if user login can create a repository
// NOTE: functions calling this assume a failure due to repository count limit; if new checks are added, those functions should be revised
func (org *Organization) CanCreateRepo() bool {
return org.AsUser().CanCreateRepo()
}
// FindOrgMembersOpts represensts find org members conditions // FindOrgMembersOpts represensts find org members conditions
type FindOrgMembersOpts struct { type FindOrgMembersOpts struct {
db.ListOptions db.ListOptions

View File

@ -111,31 +111,31 @@ func (err ErrRepoFilesAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist return util.ErrAlreadyExist
} }
// CheckCreateRepository check if could created a repository // CheckCreateRepository check if doer could create a repository in new owner
func CheckCreateRepository(ctx context.Context, doer, u *user_model.User, name string, overwriteOrAdopt bool) error { func CheckCreateRepository(ctx context.Context, doer, owner *user_model.User, name string, overwriteOrAdopt bool) error {
if !doer.CanCreateRepo() { if !doer.CanCreateRepoIn(owner) {
return ErrReachLimitOfRepo{u.MaxRepoCreation} return ErrReachLimitOfRepo{owner.MaxRepoCreation}
} }
if err := IsUsableRepoName(name); err != nil { if err := IsUsableRepoName(name); err != nil {
return err return err
} }
has, err := IsRepositoryModelOrDirExist(ctx, u, name) has, err := IsRepositoryModelOrDirExist(ctx, owner, name)
if err != nil { if err != nil {
return fmt.Errorf("IsRepositoryExist: %w", err) return fmt.Errorf("IsRepositoryExist: %w", err)
} else if has { } else if has {
return ErrRepoAlreadyExist{u.Name, name} return ErrRepoAlreadyExist{owner.Name, name}
} }
repoPath := RepoPath(u.Name, name) repoPath := RepoPath(owner.Name, name)
isExist, err := util.IsExist(repoPath) isExist, err := util.IsExist(repoPath)
if err != nil { if err != nil {
log.Error("Unable to check if %s exists. Error: %v", repoPath, err) log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
return err return err
} }
if !overwriteOrAdopt && isExist { if !overwriteOrAdopt && isExist {
return ErrRepoFilesAlreadyExist{u.Name, name} return ErrRepoFilesAlreadyExist{owner.Name, name}
} }
return nil return nil
} }

View File

@ -247,19 +247,20 @@ func (u *User) MaxCreationLimit() int {
return u.MaxRepoCreation return u.MaxRepoCreation
} }
// CanCreateRepo returns if user login can create a repository // CanCreateRepoIn checks whether the doer(u) can create a repository in the owner
// NOTE: functions calling this assume a failure due to repository count limit; if new checks are added, those functions should be revised // NOTE: functions calling this assume a failure due to repository count limit; it ONLY checks the repo number LIMIT, if new checks are added, those functions should be revised
func (u *User) CanCreateRepo() bool { func (u *User) CanCreateRepoIn(owner *User) bool {
if u.IsAdmin { if u.IsAdmin {
return true return true
} }
if u.MaxRepoCreation <= -1 { const noLimit = -1
if setting.Repository.MaxCreationLimit <= -1 { if owner.MaxRepoCreation == noLimit {
if setting.Repository.MaxCreationLimit == noLimit {
return true return true
} }
return u.NumRepos < setting.Repository.MaxCreationLimit return owner.NumRepos < setting.Repository.MaxCreationLimit
} }
return u.NumRepos < u.MaxRepoCreation return owner.NumRepos < owner.MaxRepoCreation
} }
// CanCreateOrganization returns true if user can create organisation. // CanCreateOrganization returns true if user can create organisation.
@ -272,13 +273,12 @@ func (u *User) CanEditGitHook() bool {
return !setting.DisableGitHooks && (u.IsAdmin || u.AllowGitHook) return !setting.DisableGitHooks && (u.IsAdmin || u.AllowGitHook)
} }
// CanForkRepo returns if user login can fork a repository // CanForkRepoIn ONLY checks repository count limit
// It checks especially that the user can create repos, and potentially more func (u *User) CanForkRepoIn(owner *User) bool {
func (u *User) CanForkRepo() bool {
if setting.Repository.AllowForkWithoutMaximumLimit { if setting.Repository.AllowForkWithoutMaximumLimit {
return true return true
} }
return u.CanCreateRepo() return u.CanCreateRepoIn(owner)
} }
// CanImportLocal returns true if user can migrate repository by local path. // CanImportLocal returns true if user can migrate repository by local path.

View File

@ -19,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -616,3 +617,37 @@ func TestGetInactiveUsers(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Empty(t, users) assert.Empty(t, users)
} }
func TestCanCreateRepo(t *testing.T) {
defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)()
const noLimit = -1
doerNormal := &user_model.User{}
doerAdmin := &user_model.User{IsAdmin: true}
t.Run("NoGlobalLimit", func(t *testing.T) {
setting.Repository.MaxCreationLimit = noLimit
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
})
t.Run("GlobalLimit50", func(t *testing.T) {
setting.Repository.MaxCreationLimit = 50
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100}))
})
}

View File

@ -91,12 +91,17 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
ctx.Data["CanForkToUser"] = canForkToUser ctx.Data["CanForkToUser"] = canForkToUser
ctx.Data["Orgs"] = orgs ctx.Data["Orgs"] = orgs
// TODO: this message should only be shown for the "current doer" when it is selected, just like the "new repo" page.
// msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", ctx.Doer.MaxCreationLimit())
if canForkToUser { if canForkToUser {
ctx.Data["ContextUser"] = ctx.Doer ctx.Data["ContextUser"] = ctx.Doer
ctx.Data["CanForkRepoInNewOwner"] = true
} else if len(orgs) > 0 { } else if len(orgs) > 0 {
ctx.Data["ContextUser"] = orgs[0] ctx.Data["ContextUser"] = orgs[0]
ctx.Data["CanForkRepoInNewOwner"] = true
} else { } else {
ctx.Data["CanForkRepo"] = false ctx.Data["CanForkRepoInNewOwner"] = false
ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true) ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
return nil return nil
} }
@ -120,15 +125,6 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
// Fork render repository fork page // Fork render repository fork page
func Fork(ctx *context.Context) { func Fork(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_fork") ctx.Data["Title"] = ctx.Tr("new_fork")
if ctx.Doer.CanForkRepo() {
ctx.Data["CanForkRepo"] = true
} else {
maxCreationLimit := ctx.Doer.MaxCreationLimit()
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
ctx.Flash.Error(msg, true)
}
getForkRepository(ctx) getForkRepository(ctx)
if ctx.Written() { if ctx.Written() {
return return
@ -141,7 +137,6 @@ func Fork(ctx *context.Context) {
func ForkPost(ctx *context.Context) { func ForkPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateRepoForm) form := web.GetForm(ctx).(*forms.CreateRepoForm)
ctx.Data["Title"] = ctx.Tr("new_fork") ctx.Data["Title"] = ctx.Tr("new_fork")
ctx.Data["CanForkRepo"] = true
ctxUser := checkContextUser(ctx, form.UID) ctxUser := checkContextUser(ctx, form.UID)
if ctx.Written() { if ctx.Written() {

View File

@ -87,17 +87,13 @@ func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
return nil return nil
} }
if !ctx.Doer.IsAdmin { var orgsAvailable []*organization.Organization
orgsAvailable := []*organization.Organization{} for i := 0; i < len(orgs); i++ {
for i := 0; i < len(orgs); i++ { if ctx.Doer.CanCreateRepoIn(orgs[i].AsUser()) {
if orgs[i].CanCreateRepo() { orgsAvailable = append(orgsAvailable, orgs[i])
orgsAvailable = append(orgsAvailable, orgs[i])
}
} }
ctx.Data["Orgs"] = orgsAvailable
} else {
ctx.Data["Orgs"] = orgs
} }
ctx.Data["Orgs"] = orgsAvailable
// Not equal means current user is an organization. // Not equal means current user is an organization.
if uid == ctx.Doer.ID || uid == 0 { if uid == ctx.Doer.ID || uid == 0 {
@ -154,7 +150,7 @@ func createCommon(ctx *context.Context) {
ctx.Data["Licenses"] = repo_module.Licenses ctx.Data["Licenses"] = repo_module.Licenses
ctx.Data["Readmes"] = repo_module.Readmes ctx.Data["Readmes"] = repo_module.Readmes
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
ctx.Data["CanCreateRepoInDoer"] = ctx.Doer.CanCreateRepo() ctx.Data["CanCreateRepoInDoer"] = ctx.Doer.CanCreateRepoIn(ctx.Doer)
ctx.Data["MaxCreationLimitOfDoer"] = ctx.Doer.MaxCreationLimit() ctx.Data["MaxCreationLimitOfDoer"] = ctx.Doer.MaxCreationLimit()
ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats
ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat

View File

@ -59,6 +59,7 @@ func SettingsCtxData(ctx *context.Context) {
ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner)
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath()) signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
ctx.Data["SigningKeyAvailable"] = len(signing) > 0 ctx.Data["SigningKeyAvailable"] = len(signing) > 0
@ -786,7 +787,7 @@ func handleSettingsPostConvertFork(ctx *context.Context) {
return return
} }
if !ctx.Repo.Owner.CanCreateRepo() { if !ctx.Doer.CanForkRepoIn(ctx.Repo.Owner) {
maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit() maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit()
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
ctx.Flash.Error(msg) ctx.Flash.Error(msg)

View File

@ -40,17 +40,17 @@ func deleteFailedAdoptRepository(repoID int64) error {
} }
// AdoptRepository adopts pre-existing repository files for the user/organization. // AdoptRepository adopts pre-existing repository files for the user/organization.
func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { func AdoptRepository(ctx context.Context, doer, owner *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
if !doer.IsAdmin && !u.CanCreateRepo() { if !doer.CanCreateRepoIn(owner) {
return nil, repo_model.ErrReachLimitOfRepo{ return nil, repo_model.ErrReachLimitOfRepo{
Limit: u.MaxRepoCreation, Limit: owner.MaxRepoCreation,
} }
} }
repo := &repo_model.Repository{ repo := &repo_model.Repository{
OwnerID: u.ID, OwnerID: owner.ID,
Owner: u, Owner: owner,
OwnerName: u.Name, OwnerName: owner.Name,
Name: opts.Name, Name: opts.Name,
LowerName: strings.ToLower(opts.Name), LowerName: strings.ToLower(opts.Name),
Description: opts.Description, Description: opts.Description,
@ -65,7 +65,7 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
// 1 - create the repository database operations first // 1 - create the repository database operations first
err := db.WithTx(ctx, func(ctx context.Context) error { err := db.WithTx(ctx, func(ctx context.Context) error {
return createRepositoryInDB(ctx, doer, u, repo, false) return createRepositoryInDB(ctx, doer, owner, repo, false)
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -104,7 +104,7 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err) return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
} }
notify_service.AdoptRepository(ctx, doer, u, repo) notify_service.AdoptRepository(ctx, doer, owner, repo)
return repo, nil return repo, nil
} }

View File

@ -204,10 +204,10 @@ func initRepository(ctx context.Context, u *user_model.User, repo *repo_model.Re
} }
// CreateRepositoryDirectly creates a repository for the user/organization. // CreateRepositoryDirectly creates a repository for the user/organization.
func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
if !doer.IsAdmin && !u.CanCreateRepo() { if !doer.CanCreateRepoIn(owner) {
return nil, repo_model.ErrReachLimitOfRepo{ return nil, repo_model.ErrReachLimitOfRepo{
Limit: u.MaxRepoCreation, Limit: owner.MaxRepoCreation,
} }
} }
@ -227,9 +227,9 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt
} }
repo := &repo_model.Repository{ repo := &repo_model.Repository{
OwnerID: u.ID, OwnerID: owner.ID,
Owner: u, Owner: owner,
OwnerName: u.Name, OwnerName: owner.Name,
Name: opts.Name, Name: opts.Name,
LowerName: strings.ToLower(opts.Name), LowerName: strings.ToLower(opts.Name),
Description: opts.Description, Description: opts.Description,
@ -252,7 +252,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt
// 1 - create the repository database operations first // 1 - create the repository database operations first
err := db.WithTx(ctx, func(ctx context.Context) error { err := db.WithTx(ctx, func(ctx context.Context) error {
return createRepositoryInDB(ctx, doer, u, repo, false) return createRepositoryInDB(ctx, doer, owner, repo, false)
}) })
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -65,7 +65,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
} }
// Fork is prohibited, if user has reached maximum limit of repositories // Fork is prohibited, if user has reached maximum limit of repositories
if !owner.CanForkRepo() { if !doer.CanForkRepoIn(owner) {
return nil, repo_model.ErrReachLimitOfRepo{ return nil, repo_model.ErrReachLimitOfRepo{
Limit: owner.MaxRepoCreation, Limit: owner.MaxRepoCreation,
} }

View File

@ -68,7 +68,7 @@ func GenerateProtectedBranch(ctx context.Context, templateRepo, generateRepo *re
// GenerateRepository generates a repository from a template // GenerateRepository generates a repository from a template
func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) { func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) {
if !doer.IsAdmin && !owner.CanCreateRepo() { if !doer.CanCreateRepoIn(owner) {
return nil, repo_model.ErrReachLimitOfRepo{ return nil, repo_model.ErrReachLimitOfRepo{
Limit: owner.MaxRepoCreation, Limit: owner.MaxRepoCreation,
} }

View File

@ -61,7 +61,7 @@ func AcceptTransferOwnership(ctx context.Context, repo *repo_model.Repository, d
return err return err
} }
if !doer.IsAdmin && !repoTransfer.Recipient.CanCreateRepo() { if !doer.CanCreateRepoIn(repoTransfer.Recipient) {
limit := util.Iif(repoTransfer.Recipient.MaxRepoCreation >= 0, repoTransfer.Recipient.MaxRepoCreation, setting.Repository.MaxCreationLimit) limit := util.Iif(repoTransfer.Recipient.MaxRepoCreation >= 0, repoTransfer.Recipient.MaxRepoCreation, setting.Repository.MaxCreationLimit)
return LimitReachedError{Limit: limit} return LimitReachedError{Limit: limit}
} }
@ -416,7 +416,7 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
return err return err
} }
if !doer.IsAdmin && !newOwner.CanCreateRepo() { if !doer.CanForkRepoIn(newOwner) {
limit := util.Iif(newOwner.MaxRepoCreation >= 0, newOwner.MaxRepoCreation, setting.Repository.MaxCreationLimit) limit := util.Iif(newOwner.MaxRepoCreation >= 0, newOwner.MaxRepoCreation, setting.Repository.MaxCreationLimit)
return LimitReachedError{Limit: limit} return LimitReachedError{Limit: limit}
} }

View File

@ -144,7 +144,7 @@ func TestRepositoryTransferRejection(t *testing.T) {
require.NotNil(t, transfer) require.NotNil(t, transfer)
require.NoError(t, transfer.LoadRecipient(db.DefaultContext)) require.NoError(t, transfer.LoadRecipient(db.DefaultContext))
require.True(t, transfer.Recipient.CanCreateRepo()) // admin is not subject to limits require.True(t, doerAdmin.CanCreateRepoIn(transfer.Recipient)) // admin is not subject to limits
// Administrator should not be affected by the limits so transfer should be successful // Administrator should not be affected by the limits so transfer should be successful
assert.NoError(t, AcceptTransferOwnership(db.DefaultContext, repo, doerAdmin)) assert.NoError(t, AcceptTransferOwnership(db.DefaultContext, repo, doerAdmin))
@ -158,7 +158,7 @@ func TestRepositoryTransferRejection(t *testing.T) {
require.NotNil(t, transfer) require.NotNil(t, transfer)
require.NoError(t, transfer.LoadRecipient(db.DefaultContext)) require.NoError(t, transfer.LoadRecipient(db.DefaultContext))
require.False(t, transfer.Recipient.CanCreateRepo()) // regular user is subject to limits require.False(t, doer.CanCreateRepoIn(transfer.Recipient)) // regular user is subject to limits
// Cannot accept because of the limit // Cannot accept because of the limit
err = AcceptTransferOwnership(db.DefaultContext, repo, doer) err = AcceptTransferOwnership(db.DefaultContext, repo, doer)

View File

@ -75,7 +75,7 @@
<div class="inline field"> <div class="inline field">
<label></label> <label></label>
<button class="ui primary button{{if not .CanForkRepo}} disabled{{end}}"> <button class="ui primary button{{if not .CanForkRepoInNewOwner}} disabled{{end}}">
{{ctx.Locale.Tr "repo.fork_repo"}} {{ctx.Locale.Tr "repo.fork_repo"}}
</button> </button>
</div> </div>

View File

@ -802,7 +802,7 @@
</div> </div>
</div> </div>
{{end}} {{end}}
{{if and .Repository.IsFork .Repository.Owner.CanCreateRepo}} {{if .CanConvertFork}}
<div class="flex-item"> <div class="flex-item">
<div class="flex-item-main"> <div class="flex-item-main">
<div class="flex-item-title">{{ctx.Locale.Tr "repo.settings.convert_fork"}}</div> <div class="flex-item-title">{{ctx.Locale.Tr "repo.settings.convert_fork"}}</div>
@ -916,7 +916,7 @@
</div> </div>
</div> </div>
{{end}} {{end}}
{{if and .Repository.IsFork .Repository.Owner.CanCreateRepo}} {{if .CanConvertFork}}
<div class="ui small modal" id="convert-fork-repo-modal"> <div class="ui small modal" id="convert-fork-repo-modal">
<div class="header"> <div class="header">
{{ctx.Locale.Tr "repo.settings.convert_fork"}} {{ctx.Locale.Tr "repo.settings.convert_fork"}}