Enable/disable owner and repo projects independently ()

Part of  

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>
pull/29570/head^2
Denys Konovalov committed by GitHub
parent 8553b4600e
commit fe6792dff3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -520,6 +520,7 @@
id: 75 id: 75
repo_id: 1 repo_id: 1
type: 8 type: 8
config: "{\"ProjectsMode\":\"all\"}"
created_unix: 946684810 created_unix: 946684810
- -
@ -650,12 +651,6 @@
type: 2 type: 2
created_unix: 946684810 created_unix: 946684810
-
id: 98
repo_id: 1
type: 8
created_unix: 946684810
- -
id: 99 id: 99
repo_id: 1 repo_id: 1

@ -411,6 +411,11 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit
Type: tp, Type: tp,
Config: new(ActionsConfig), Config: new(ActionsConfig),
} }
} else if tp == unit.TypeProjects {
return &RepoUnit{
Type: tp,
Config: new(ProjectsConfig),
}
} }
return &RepoUnit{ return &RepoUnit{

@ -202,6 +202,53 @@ func (cfg *ActionsConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg) 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. // BeforeSet is invoked from XORM before setting the value of a field of this object.
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
switch colName { switch colName {
@ -217,7 +264,9 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
r.Config = new(IssuesConfig) r.Config = new(IssuesConfig)
case unit.TypeActions: case unit.TypeActions:
r.Config = new(ActionsConfig) 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 fallthrough
default: default:
r.Config = new(UnitConfig) r.Config = new(UnitConfig)
@ -265,6 +314,11 @@ func (r *RepoUnit) ActionsConfig() *ActionsConfig {
return r.Config.(*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) { func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
var tmpUnits []*RepoUnit var tmpUnits []*RepoUnit
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil { if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {

@ -93,6 +93,12 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
AllowRebaseUpdate: true, 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 { } else {
units = append(units, repo_model.RepoUnit{ units = append(units, repo_model.RepoUnit{
RepoID: repo.ID, RepoID: repo.ID,

@ -90,6 +90,7 @@ type Repository struct {
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"` ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
HasPullRequests bool `json:"has_pull_requests"` HasPullRequests bool `json:"has_pull_requests"`
HasProjects bool `json:"has_projects"` HasProjects bool `json:"has_projects"`
ProjectsMode string `json:"projects_mode"`
HasReleases bool `json:"has_releases"` HasReleases bool `json:"has_releases"`
HasPackages bool `json:"has_packages"` HasPackages bool `json:"has_packages"`
HasActions bool `json:"has_actions"` HasActions bool `json:"has_actions"`
@ -180,6 +181,8 @@ type EditRepoOption struct {
HasPullRequests *bool `json:"has_pull_requests,omitempty"` HasPullRequests *bool `json:"has_pull_requests,omitempty"`
// either `true` to enable project unit, or `false` to disable them. // either `true` to enable project unit, or `false` to disable them.
HasProjects *bool `json:"has_projects,omitempty"` 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. // either `true` to enable releases unit, or `false` to disable them.
HasReleases *bool `json:"has_releases,omitempty"` HasReleases *bool `json:"has_releases,omitempty"`
// either `true` to enable packages unit, or `false` to disable them. // either `true` to enable packages unit, or `false` to disable them.

@ -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.pulls.default_allow_edits_from_maintainers = Allow edits from maintainers by default
settings.releases_desc = Enable Repository Releases settings.releases_desc = Enable Repository Releases
settings.packages_desc = Enable Repository Packages Registry 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.actions_desc = Enable Repository Actions
settings.admin_settings = Administrator Settings settings.admin_settings = Administrator Settings
settings.admin_enable_health_check = Enable Repository Health Checks (git fsck) settings.admin_enable_health_check = Enable Repository Health Checks (git fsck)

@ -944,13 +944,33 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
} }
} }
if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() { currHasProjects := repo.UnitEnabled(ctx, unit_model.TypeProjects)
if *opts.HasProjects { 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{ units = append(units, repo_model.RepoUnit{
RepoID: repo.ID, RepoID: repo.ID,
Type: unit_model.TypeProjects, Type: unit_model.TypeProjects,
Config: config,
}) })
} else { } else if !newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
} }
} }

@ -587,8 +587,15 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
if repo.Owner.IsOrganization() { if repo.Owner.IsOrganization() {
repoOwnerType = project_model.TypeOrganization repoOwnerType = project_model.TypeOrganization
} }
projectsUnit := repo.MustGetUnit(ctx, unit.TypeProjects)
var openProjects []*project_model.Project
var closedProjects []*project_model.Project
var err error var err error
projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
openProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
ListOptions: db.ListOptionsAll, ListOptions: db.ListOptionsAll,
RepoID: repo.ID, RepoID: repo.ID,
IsClosed: optional.Some(false), IsClosed: optional.Some(false),
@ -598,30 +605,31 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
ctx.ServerError("GetProjects", err) ctx.ServerError("GetProjects", err)
return return
} }
projects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
ListOptions: db.ListOptionsAll, ListOptions: db.ListOptionsAll,
OwnerID: repo.OwnerID, RepoID: repo.ID,
IsClosed: optional.Some(false), IsClosed: optional.Some(true),
Type: repoOwnerType, Type: project_model.TypeRepository,
}) })
if err != nil { if err != nil {
ctx.ServerError("GetProjects", err) ctx.ServerError("GetProjects", err)
return return
} }
}
ctx.Data["OpenProjects"] = append(projects, projects2...) if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeOwner) {
openProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
projects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
ListOptions: db.ListOptionsAll, ListOptions: db.ListOptionsAll,
RepoID: repo.ID, OwnerID: repo.OwnerID,
IsClosed: optional.Some(true), IsClosed: optional.Some(false),
Type: project_model.TypeRepository, Type: repoOwnerType,
}) })
if err != nil { if err != nil {
ctx.ServerError("GetProjects", err) ctx.ServerError("GetProjects", err)
return return
} }
projects2, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{ openProjects = append(openProjects, openProjects2...)
closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
ListOptions: db.ListOptionsAll, ListOptions: db.ListOptionsAll,
OwnerID: repo.OwnerID, OwnerID: repo.OwnerID,
IsClosed: optional.Some(true), IsClosed: optional.Some(true),
@ -631,8 +639,11 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
ctx.ServerError("GetProjects", err) ctx.ServerError("GetProjects", err)
return return
} }
closedProjects = append(closedProjects, closedProjects2...)
}
ctx.Data["ClosedProjects"] = append(projects, projects2...) ctx.Data["OpenProjects"] = openProjects
ctx.Data["ClosedProjects"] = closedProjects
} }
// repoReviewerSelection items to bee shown // repoReviewerSelection items to bee shown

@ -14,7 +14,7 @@ import (
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
project_model "code.gitea.io/gitea/models/project" 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/models/unit"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
@ -33,16 +33,17 @@ const (
tplProjectsView base.TplName = "repo/projects/view" tplProjectsView base.TplName = "repo/projects/view"
) )
// MustEnableProjects check if projects are enabled in settings // MustEnableRepoProjects check if repo projects are enabled in settings
func MustEnableProjects(ctx *context.Context) { func MustEnableRepoProjects(ctx *context.Context) {
if unit.TypeProjects.UnitGlobalDisabled() { if unit.TypeProjects.UnitGlobalDisabled() {
ctx.NotFound("EnableKanbanBoard", nil) ctx.NotFound("EnableKanbanBoard", nil)
return return
} }
if ctx.Repo.Repository != nil { if ctx.Repo.Repository != nil {
if !ctx.Repo.CanRead(unit.TypeProjects) { projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects)
ctx.NotFound("MustEnableProjects", nil) if !ctx.Repo.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
ctx.NotFound("MustEnableRepoProjects", nil)
return return
} }
} }
@ -325,10 +326,10 @@ func ViewProject(ctx *context.Context) {
} }
if project.CardType != project_model.CardTypeTextOnly { 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 _, issuesList := range issuesMap {
for _, issue := range issuesList { 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 issuesAttachmentMap[issue.ID] = issueAttachment
} }
} }

@ -533,6 +533,9 @@ func SettingsPost(ctx *context.Context) {
units = append(units, repo_model.RepoUnit{ units = append(units, repo_model.RepoUnit{
RepoID: repo.ID, RepoID: repo.ID,
Type: unit_model.TypeProjects, Type: unit_model.TypeProjects,
Config: &repo_model.ProjectsConfig{
ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
},
}) })
} else if !unit_model.TypeProjects.UnitGlobalDisabled() { } else if !unit_model.TypeProjects.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)

@ -1344,7 +1344,7 @@ func registerRoutes(m *web.Route) {
}) })
}) })
}, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) }, reqRepoProjectsWriter, context.RepoMustNotBeArchived())
}, reqRepoProjectsReader, repo.MustEnableProjects) }, reqRepoProjectsReader, repo.MustEnableRepoProjects)
m.Group("/actions", func() { m.Group("/actions", func() {
m.Get("", actions.List) m.Get("", actions.List)

@ -113,8 +113,11 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
} }
hasProjects := false 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 hasProjects = true
config := unit.ProjectsConfig()
projectsMode = config.ProjectsMode
} }
hasReleases := false hasReleases := false
@ -211,6 +214,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
InternalTracker: internalTracker, InternalTracker: internalTracker,
HasWiki: hasWiki, HasWiki: hasWiki,
HasProjects: hasProjects, HasProjects: hasProjects,
ProjectsMode: string(projectsMode),
HasReleases: hasReleases, HasReleases: hasReleases,
HasPackages: hasPackages, HasPackages: hasPackages,
HasActions: hasActions, HasActions: hasActions,

@ -142,6 +142,7 @@ type RepoSettingForm struct {
ExternalTrackerRegexpPattern string ExternalTrackerRegexpPattern string
EnableCloseIssuesViaCommitInAnyBranch bool EnableCloseIssuesViaCommitInAnyBranch bool
EnableProjects bool EnableProjects bool
ProjectsMode string
EnableReleases bool EnableReleases bool
EnablePackages bool EnablePackages bool
EnablePulls bool EnablePulls bool

@ -174,7 +174,8 @@
</a> </a>
{{end}} {{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"> <a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}} {{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}}
{{if .Repository.NumOpenProjects}} {{if .Repository.NumOpenProjects}}

@ -446,13 +446,45 @@
{{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}} {{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}}
{{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}} {{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}}
{{$projectsUnit := .Repository.MustGetUnit $.Context $.UnitTypeProjects}}
<div class="inline field"> <div class="inline field">
<label>{{ctx.Locale.Tr "repo.project_board"}}</label> <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}}> <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> <label>{{ctx.Locale.Tr "repo.settings.projects_desc"}}</label>
</div> </div>
</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}} {{$isReleasesEnabled := .Repository.UnitEnabled $.Context $.UnitTypeReleases}}
{{$isReleasesGlobalDisabled := .UnitTypeReleases.UnitGlobalDisabled}} {{$isReleasesGlobalDisabled := .UnitTypeReleases.UnitGlobalDisabled}}

@ -19570,6 +19570,11 @@
"type": "boolean", "type": "boolean",
"x-go-name": "Private" "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": { "template": {
"description": "either `true` to make this repository a template or `false` to make it a normal repository", "description": "either `true` to make this repository a template or `false` to make it a normal repository",
"type": "boolean", "type": "boolean",
@ -22491,6 +22496,10 @@
"type": "boolean", "type": "boolean",
"x-go-name": "Private" "x-go-name": "Private"
}, },
"projects_mode": {
"type": "string",
"x-go-name": "ProjectsMode"
},
"release_counter": { "release_counter": {
"type": "integer", "type": "integer",
"format": "int64", "format": "int64",

Loading…
Cancel
Save