]> source.dussan.org Git - gitea.git/commitdiff
Enable/disable owner and repo projects independently (#28805)
authorDenys Konovalov <kontakt@denyskon.de>
Mon, 4 Mar 2024 02:56:52 +0000 (03:56 +0100)
committerGitHub <noreply@github.com>
Mon, 4 Mar 2024 02:56:52 +0000 (02:56 +0000)
Part of #23318

Add menu in repo settings to allow for repo admin to decide not just if
projects are enabled or disabled per repo, but also which kind of
projects (repo-level/owner-level) are enabled. If repo projects
disabled, don't show the projects tab.

![grafik](https://github.com/go-gitea/gitea/assets/47871822/b9b43fb4-824b-47f9-b8e2-12004313647c)

---------

Co-authored-by: delvh <dev.lh@web.de>
16 files changed:
models/fixtures/repo_unit.yml
models/repo/repo.go
models/repo/repo_unit.go
modules/repository/create.go
modules/structs/repo.go
options/locale/locale_en-US.ini
routers/api/v1/repo/repo.go
routers/web/repo/issue.go
routers/web/repo/projects.go
routers/web/repo/setting/setting.go
routers/web/web.go
services/convert/repository.go
services/forms/repo_form.go
templates/repo/header.tmpl
templates/repo/settings/options.tmpl
templates/swagger/v1_json.tmpl

index 4b266749905d09c54d723d505967ce8a727872b0..6714294e2b66e537b0f1612be160a961bad53708 100644 (file)
   id: 75
   repo_id: 1
   type: 8
+  config: "{\"ProjectsMode\":\"all\"}"
   created_unix: 946684810
 
 -
   type: 2
   created_unix: 946684810
 
--
-  id: 98
-  repo_id: 1
-  type: 8
-  created_unix: 946684810
-
 -
   id: 99
   repo_id: 1
index 5ce3ecb58aebc0377214e76c426b13458279519d..ad2e21b66b658d350a9adc07dea2f96a6b708724 100644 (file)
@@ -411,6 +411,11 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit
                        Type:   tp,
                        Config: new(ActionsConfig),
                }
+       } else if tp == unit.TypeProjects {
+               return &RepoUnit{
+                       Type:   tp,
+                       Config: new(ProjectsConfig),
+               }
        }
 
        return &RepoUnit{
index 31a2a2e2482281e7f582a8c1b06fb8324a075791..6b9dde7fafb3309efc08b5741853a29c051ec4a0 100644 (file)
@@ -202,6 +202,53 @@ func (cfg *ActionsConfig) ToDB() ([]byte, error) {
        return json.Marshal(cfg)
 }
 
+// ProjectsMode represents the projects enabled for a repository
+type ProjectsMode string
+
+const (
+       // ProjectsModeRepo allows only repo-level projects
+       ProjectsModeRepo ProjectsMode = "repo"
+       // ProjectsModeOwner allows only owner-level projects
+       ProjectsModeOwner ProjectsMode = "owner"
+       // ProjectsModeAll allows both kinds of projects
+       ProjectsModeAll ProjectsMode = "all"
+       // ProjectsModeNone doesn't allow projects
+       ProjectsModeNone ProjectsMode = "none"
+)
+
+// ProjectsConfig describes projects config
+type ProjectsConfig struct {
+       ProjectsMode ProjectsMode
+}
+
+// FromDB fills up a ProjectsConfig from serialized format.
+func (cfg *ProjectsConfig) FromDB(bs []byte) error {
+       return json.UnmarshalHandleDoubleEncode(bs, &cfg)
+}
+
+// ToDB exports a ProjectsConfig to a serialized format.
+func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
+       return json.Marshal(cfg)
+}
+
+func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
+       if cfg.ProjectsMode != "" {
+               return cfg.ProjectsMode
+       }
+
+       return ProjectsModeNone
+}
+
+func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
+       projectsMode := cfg.GetProjectsMode()
+
+       if m == ProjectsModeNone {
+               return true
+       }
+
+       return projectsMode == m || projectsMode == ProjectsModeAll
+}
+
 // BeforeSet is invoked from XORM before setting the value of a field of this object.
 func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
        switch colName {
@@ -217,7 +264,9 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
                        r.Config = new(IssuesConfig)
                case unit.TypeActions:
                        r.Config = new(ActionsConfig)
-               case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages:
+               case unit.TypeProjects:
+                       r.Config = new(ProjectsConfig)
+               case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypePackages:
                        fallthrough
                default:
                        r.Config = new(UnitConfig)
@@ -265,6 +314,11 @@ func (r *RepoUnit) ActionsConfig() *ActionsConfig {
        return r.Config.(*ActionsConfig)
 }
 
+// ProjectsConfig returns config for unit.ProjectsConfig
+func (r *RepoUnit) ProjectsConfig() *ProjectsConfig {
+       return r.Config.(*ProjectsConfig)
+}
+
 func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
        var tmpUnits []*RepoUnit
        if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {
index ca2150b972e06c9fd224c77dbb4f0e613900ce7f..f009c0880d71147c4cca2a9c91fa9bd534ae3781 100644 (file)
@@ -93,6 +93,12 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
                                        AllowRebaseUpdate: true,
                                },
                        })
+               } else if tp == unit.TypeProjects {
+                       units = append(units, repo_model.RepoUnit{
+                               RepoID: repo.ID,
+                               Type:   tp,
+                               Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll},
+                       })
                } else {
                        units = append(units, repo_model.RepoUnit{
                                RepoID: repo.ID,
index 56d6158bd8fdf467da91a492ea30c2f4dff2a9a5..bc8eb0b756973bf01aae5668f0256b6753c78d1f 100644 (file)
@@ -90,6 +90,7 @@ type Repository struct {
        ExternalWiki                  *ExternalWiki    `json:"external_wiki,omitempty"`
        HasPullRequests               bool             `json:"has_pull_requests"`
        HasProjects                   bool             `json:"has_projects"`
+       ProjectsMode                  string           `json:"projects_mode"`
        HasReleases                   bool             `json:"has_releases"`
        HasPackages                   bool             `json:"has_packages"`
        HasActions                    bool             `json:"has_actions"`
@@ -180,6 +181,8 @@ type EditRepoOption struct {
        HasPullRequests *bool `json:"has_pull_requests,omitempty"`
        // either `true` to enable project unit, or `false` to disable them.
        HasProjects *bool `json:"has_projects,omitempty"`
+       // `repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.
+       ProjectsMode *string `json:"projects_mode,omitempty" binding:"In(repo,owner,all)"`
        // either `true` to enable releases unit, or `false` to disable them.
        HasReleases *bool `json:"has_releases,omitempty"`
        // either `true` to enable packages unit, or `false` to disable them.
index 8c4dae753b0ee9186527873db27e91aaf50ae87a..c8c8f2dfebcfdece610a02b464c66898ac1d186c 100644 (file)
@@ -2090,7 +2090,11 @@ settings.pulls.default_delete_branch_after_merge = Delete pull request branch af
 settings.pulls.default_allow_edits_from_maintainers = Allow edits from maintainers by default
 settings.releases_desc = Enable Repository Releases
 settings.packages_desc = Enable Repository Packages Registry
-settings.projects_desc = Enable Repository Projects
+settings.projects_desc = Enable Projects
+settings.projects_mode_desc = Projects Mode (which kinds of projects to show)
+settings.projects_mode_repo = Repo projects only
+settings.projects_mode_owner = Only user or org projects
+settings.projects_mode_all = All projects
 settings.actions_desc = Enable Repository Actions
 settings.admin_settings = Administrator Settings
 settings.admin_enable_health_check = Enable Repository Health Checks (git fsck)
index 6fde73a4e8f8ede468d0b85d1e255f42edfe693b..5f1af920414fbf84acdde2646959cc0fed1208b0 100644 (file)
@@ -944,13 +944,33 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
                }
        }
 
-       if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() {
-               if *opts.HasProjects {
+       currHasProjects := repo.UnitEnabled(ctx, unit_model.TypeProjects)
+       newHasProjects := currHasProjects
+       if opts.HasProjects != nil {
+               newHasProjects = *opts.HasProjects
+       }
+       if currHasProjects || newHasProjects {
+               if newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
+                       unit, err := repo.GetUnit(ctx, unit_model.TypeProjects)
+                       var config *repo_model.ProjectsConfig
+                       if err != nil {
+                               config = &repo_model.ProjectsConfig{
+                                       ProjectsMode: repo_model.ProjectsModeAll,
+                               }
+                       } else {
+                               config = unit.ProjectsConfig()
+                       }
+
+                       if opts.ProjectsMode != nil {
+                               config.ProjectsMode = repo_model.ProjectsMode(*opts.ProjectsMode)
+                       }
+
                        units = append(units, repo_model.RepoUnit{
                                RepoID: repo.ID,
                                Type:   unit_model.TypeProjects,
+                               Config: config,
                        })
-               } else {
+               } else if !newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
                        deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
                }
        }
index 1abd5e2ba5a3dc99d21d1731de3d981a076b4f32..b8c7f70aa6d34d8522bc5d206eef4e0a173344e8 100644 (file)
@@ -587,52 +587,63 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
        if repo.Owner.IsOrganization() {
                repoOwnerType = project_model.TypeOrganization
        }
-       var err error
-       projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
-               ListOptions: db.ListOptionsAll,
-               RepoID:      repo.ID,
-               IsClosed:    optional.Some(false),
-               Type:        project_model.TypeRepository,
-       })
-       if err != nil {
-               ctx.ServerError("GetProjects", err)
-               return
-       }
-       projects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
-               ListOptions: db.ListOptionsAll,
-               OwnerID:     repo.OwnerID,
-               IsClosed:    optional.Some(false),
-               Type:        repoOwnerType,
-       })
-       if err != nil {
-               ctx.ServerError("GetProjects", err)
-               return
-       }
 
-       ctx.Data["OpenProjects"] = append(projects, projects2...)
+       projectsUnit := repo.MustGetUnit(ctx, unit.TypeProjects)
 
-       projects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
-               ListOptions: db.ListOptionsAll,
-               RepoID:      repo.ID,
-               IsClosed:    optional.Some(true),
-               Type:        project_model.TypeRepository,
-       })
-       if err != nil {
-               ctx.ServerError("GetProjects", err)
-               return
+       var openProjects []*project_model.Project
+       var closedProjects []*project_model.Project
+       var err error
+
+       if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
+               openProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
+                       ListOptions: db.ListOptionsAll,
+                       RepoID:      repo.ID,
+                       IsClosed:    optional.Some(false),
+                       Type:        project_model.TypeRepository,
+               })
+               if err != nil {
+                       ctx.ServerError("GetProjects", err)
+                       return
+               }
+               closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
+                       ListOptions: db.ListOptionsAll,
+                       RepoID:      repo.ID,
+                       IsClosed:    optional.Some(true),
+                       Type:        project_model.TypeRepository,
+               })
+               if err != nil {
+                       ctx.ServerError("GetProjects", err)
+                       return
+               }
        }
-       projects2, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
-               ListOptions: db.ListOptionsAll,
-               OwnerID:     repo.OwnerID,
-               IsClosed:    optional.Some(true),
-               Type:        repoOwnerType,
-       })
-       if err != nil {
-               ctx.ServerError("GetProjects", err)
-               return
+
+       if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeOwner) {
+               openProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
+                       ListOptions: db.ListOptionsAll,
+                       OwnerID:     repo.OwnerID,
+                       IsClosed:    optional.Some(false),
+                       Type:        repoOwnerType,
+               })
+               if err != nil {
+                       ctx.ServerError("GetProjects", err)
+                       return
+               }
+               openProjects = append(openProjects, openProjects2...)
+               closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
+                       ListOptions: db.ListOptionsAll,
+                       OwnerID:     repo.OwnerID,
+                       IsClosed:    optional.Some(true),
+                       Type:        repoOwnerType,
+               })
+               if err != nil {
+                       ctx.ServerError("GetProjects", err)
+                       return
+               }
+               closedProjects = append(closedProjects, closedProjects2...)
        }
 
-       ctx.Data["ClosedProjects"] = append(projects, projects2...)
+       ctx.Data["OpenProjects"] = openProjects
+       ctx.Data["ClosedProjects"] = closedProjects
 }
 
 // repoReviewerSelection items to bee shown
index 4c171defbd4ffbc80bf18cfd79a60d954a9bc1f7..86909b5fd0f5ad3190755c63432c054d1d885e39 100644 (file)
@@ -14,7 +14,7 @@ import (
        issues_model "code.gitea.io/gitea/models/issues"
        "code.gitea.io/gitea/models/perm"
        project_model "code.gitea.io/gitea/models/project"
-       attachment_model "code.gitea.io/gitea/models/repo"
+       repo_model "code.gitea.io/gitea/models/repo"
        "code.gitea.io/gitea/models/unit"
        "code.gitea.io/gitea/modules/base"
        "code.gitea.io/gitea/modules/json"
@@ -33,16 +33,17 @@ const (
        tplProjectsView base.TplName = "repo/projects/view"
 )
 
-// MustEnableProjects check if projects are enabled in settings
-func MustEnableProjects(ctx *context.Context) {
+// MustEnableRepoProjects check if repo projects are enabled in settings
+func MustEnableRepoProjects(ctx *context.Context) {
        if unit.TypeProjects.UnitGlobalDisabled() {
                ctx.NotFound("EnableKanbanBoard", nil)
                return
        }
 
        if ctx.Repo.Repository != nil {
-               if !ctx.Repo.CanRead(unit.TypeProjects) {
-                       ctx.NotFound("MustEnableProjects", nil)
+               projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects)
+               if !ctx.Repo.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
+                       ctx.NotFound("MustEnableRepoProjects", nil)
                        return
                }
        }
@@ -325,10 +326,10 @@ func ViewProject(ctx *context.Context) {
        }
 
        if project.CardType != project_model.CardTypeTextOnly {
-               issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
+               issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
                for _, issuesList := range issuesMap {
                        for _, issue := range issuesList {
-                               if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
+                               if issueAttachment, err := repo_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
                                        issuesAttachmentMap[issue.ID] = issueAttachment
                                }
                        }
index 0f649acba312d381e454a960898c66353682f0b3..3af0ddb5789d0b402dbd739f87795675e14d0fb2 100644 (file)
@@ -533,6 +533,9 @@ func SettingsPost(ctx *context.Context) {
                        units = append(units, repo_model.RepoUnit{
                                RepoID: repo.ID,
                                Type:   unit_model.TypeProjects,
+                               Config: &repo_model.ProjectsConfig{
+                                       ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
+                               },
                        })
                } else if !unit_model.TypeProjects.UnitGlobalDisabled() {
                        deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
index 9de652fba53d9b16466fb38c788dc443c6a0e01a..14d31b3a9088f03a9cb023c250d9ac6910a030fb 100644 (file)
@@ -1344,7 +1344,7 @@ func registerRoutes(m *web.Route) {
                                        })
                                })
                        }, reqRepoProjectsWriter, context.RepoMustNotBeArchived())
-               }, reqRepoProjectsReader, repo.MustEnableProjects)
+               }, reqRepoProjectsReader, repo.MustEnableRepoProjects)
 
                m.Group("/actions", func() {
                        m.Get("", actions.List)
index 9184bc05c7a9b3d44222a7ff0048a9ac2d92d7e4..39efd304a96adcce2b1f23c1323fe664ebc84d88 100644 (file)
@@ -113,8 +113,11 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
                defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
        }
        hasProjects := false
-       if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
+       projectsMode := repo_model.ProjectsModeAll
+       if unit, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
                hasProjects = true
+               config := unit.ProjectsConfig()
+               projectsMode = config.ProjectsMode
        }
 
        hasReleases := false
@@ -211,6 +214,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
                InternalTracker:               internalTracker,
                HasWiki:                       hasWiki,
                HasProjects:                   hasProjects,
+               ProjectsMode:                  string(projectsMode),
                HasReleases:                   hasReleases,
                HasPackages:                   hasPackages,
                HasActions:                    hasActions,
index e40bcf4eea8e316efda79fc259c6f557ed185f13..8c3e458d2ff6ba9c6a875ff4d003d39ca4d70f1e 100644 (file)
@@ -142,6 +142,7 @@ type RepoSettingForm struct {
        ExternalTrackerRegexpPattern          string
        EnableCloseIssuesViaCommitInAnyBranch bool
        EnableProjects                        bool
+       ProjectsMode                          string
        EnableReleases                        bool
        EnablePackages                        bool
        EnablePulls                           bool
index ee46af423624760f538f68b0f094fbc5e4b4d1fc..b692c851ee383de6acdc58ee8b91c2d93f932fa7 100644 (file)
                                        </a>
                                {{end}}
 
-                               {{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects)}}
+                               {{$projectsUnit := .Repository.MustGetUnit $.Context $.UnitTypeProjects}}
+                               {{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}}
                                        <a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
                                                {{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}}
                                                {{if .Repository.NumOpenProjects}}
index 6d01b227ff167ae048db2213af6fb25190e48cd1..5a85192a43793e463d9278e8f664844aeeac28ef 100644 (file)
 
                                {{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}}
                                {{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}}
+                               {{$projectsUnit := .Repository.MustGetUnit $.Context $.UnitTypeProjects}}
                                <div class="inline field">
                                        <label>{{ctx.Locale.Tr "repo.project_board"}}</label>
                                        <div class="ui checkbox{{if $isProjectsGlobalDisabled}} disabled{{end}}"{{if $isProjectsGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
-                                               <input class="enable-system" name="enable_projects" type="checkbox" {{if $isProjectsEnabled}}checked{{end}}>
+                                               <input class="enable-system" name="enable_projects" type="checkbox" data-target="#projects_box" {{if $isProjectsEnabled}}checked{{end}}>
                                                <label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
                                        </div>
                                </div>
+                               <div class="field {{if not $isProjectsEnabled}} disabled{{end}} gt-pl-4" id="projects_box">
+                                       <p>
+                                               {{ctx.Locale.Tr "repo.settings.projects_mode_desc"}}
+                                       </p>
+                                       <div class="ui dropdown selection">
+                                               <select name="projects_mode">
+                                                       <option value="repo" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.ProjectsMode "repo")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</option>
+                                                       <option value="owner" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.ProjectsMode "owner")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</option>
+                                                       <option value="all" {{if or (not $isProjectsEnabled) (eq $projectsUnit.ProjectsConfig.ProjectsMode "all")}}selected{{end}}>{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</option>
+                                               </select>
+                                               {{svg "octicon-triangle-down" 14 "dropdown icon"}}
+                                               <div class="default text">
+                                                       {{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "repo")}}
+                                                               {{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}
+                                                       {{end}}
+                                                       {{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "owner")}}
+                                                               {{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}
+                                                       {{end}}
+                                                       {{if (eq $projectsUnit.ProjectsConfig.ProjectsMode "all")}}
+                                                               {{ctx.Locale.Tr "repo.settings.projects_mode_all"}}
+                                                       {{end}}
+                                               </div>
+                                               <div class="menu">
+                                                       <div class="item" data-value="repo">{{ctx.Locale.Tr "repo.settings.projects_mode_repo"}}</div>
+                                                       <div class="item" data-value="owner">{{ctx.Locale.Tr "repo.settings.projects_mode_owner"}}</div>
+                                                       <div class="item" data-value="all">{{ctx.Locale.Tr "repo.settings.projects_mode_all"}}</div>
+                                               </div>
+                                       </div>
+                               </div>
+
+                               <div class="divider"></div>
 
                                {{$isReleasesEnabled := .Repository.UnitEnabled $.Context $.UnitTypeReleases}}
                                {{$isReleasesGlobalDisabled := .UnitTypeReleases.UnitGlobalDisabled}}
index fa7cd60eb31ad0b2eed8f993a7b7e26feefdf599..9aba84a0232720642509936cd6f78535c44e2328 100644 (file)
           "type": "boolean",
           "x-go-name": "Private"
         },
+        "projects_mode": {
+          "description": "`repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.",
+          "type": "string",
+          "x-go-name": "ProjectsMode"
+        },
         "template": {
           "description": "either `true` to make this repository a template or `false` to make it a normal repository",
           "type": "boolean",
           "type": "boolean",
           "x-go-name": "Private"
         },
+        "projects_mode": {
+          "type": "string",
+          "x-go-name": "ProjectsMode"
+        },
         "release_counter": {
           "type": "integer",
           "format": "int64",