Skip to content

Commit fe6792d

Browse files
denyskondelvh
andauthored
Enable/disable owner and repo projects independently (#28805)
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 <[email protected]>
1 parent 8553b46 commit fe6792d

File tree

16 files changed

+212
-63
lines changed

16 files changed

+212
-63
lines changed

models/fixtures/repo_unit.yml

+1-6
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@
520520
id: 75
521521
repo_id: 1
522522
type: 8
523+
config: "{\"ProjectsMode\":\"all\"}"
523524
created_unix: 946684810
524525

525526
-
@@ -650,12 +651,6 @@
650651
type: 2
651652
created_unix: 946684810
652653

653-
-
654-
id: 98
655-
repo_id: 1
656-
type: 8
657-
created_unix: 946684810
658-
659654
-
660655
id: 99
661656
repo_id: 1

models/repo/repo.go

+5
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,11 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit
411411
Type: tp,
412412
Config: new(ActionsConfig),
413413
}
414+
} else if tp == unit.TypeProjects {
415+
return &RepoUnit{
416+
Type: tp,
417+
Config: new(ProjectsConfig),
418+
}
414419
}
415420

416421
return &RepoUnit{

models/repo/repo_unit.go

+55-1
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,53 @@ func (cfg *ActionsConfig) ToDB() ([]byte, error) {
202202
return json.Marshal(cfg)
203203
}
204204

205+
// ProjectsMode represents the projects enabled for a repository
206+
type ProjectsMode string
207+
208+
const (
209+
// ProjectsModeRepo allows only repo-level projects
210+
ProjectsModeRepo ProjectsMode = "repo"
211+
// ProjectsModeOwner allows only owner-level projects
212+
ProjectsModeOwner ProjectsMode = "owner"
213+
// ProjectsModeAll allows both kinds of projects
214+
ProjectsModeAll ProjectsMode = "all"
215+
// ProjectsModeNone doesn't allow projects
216+
ProjectsModeNone ProjectsMode = "none"
217+
)
218+
219+
// ProjectsConfig describes projects config
220+
type ProjectsConfig struct {
221+
ProjectsMode ProjectsMode
222+
}
223+
224+
// FromDB fills up a ProjectsConfig from serialized format.
225+
func (cfg *ProjectsConfig) FromDB(bs []byte) error {
226+
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
227+
}
228+
229+
// ToDB exports a ProjectsConfig to a serialized format.
230+
func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
231+
return json.Marshal(cfg)
232+
}
233+
234+
func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
235+
if cfg.ProjectsMode != "" {
236+
return cfg.ProjectsMode
237+
}
238+
239+
return ProjectsModeNone
240+
}
241+
242+
func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
243+
projectsMode := cfg.GetProjectsMode()
244+
245+
if m == ProjectsModeNone {
246+
return true
247+
}
248+
249+
return projectsMode == m || projectsMode == ProjectsModeAll
250+
}
251+
205252
// BeforeSet is invoked from XORM before setting the value of a field of this object.
206253
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
207254
switch colName {
@@ -217,7 +264,9 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
217264
r.Config = new(IssuesConfig)
218265
case unit.TypeActions:
219266
r.Config = new(ActionsConfig)
220-
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages:
267+
case unit.TypeProjects:
268+
r.Config = new(ProjectsConfig)
269+
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypePackages:
221270
fallthrough
222271
default:
223272
r.Config = new(UnitConfig)
@@ -265,6 +314,11 @@ func (r *RepoUnit) ActionsConfig() *ActionsConfig {
265314
return r.Config.(*ActionsConfig)
266315
}
267316

317+
// ProjectsConfig returns config for unit.ProjectsConfig
318+
func (r *RepoUnit) ProjectsConfig() *ProjectsConfig {
319+
return r.Config.(*ProjectsConfig)
320+
}
321+
268322
func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
269323
var tmpUnits []*RepoUnit
270324
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {

modules/repository/create.go

+6
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
9393
AllowRebaseUpdate: true,
9494
},
9595
})
96+
} else if tp == unit.TypeProjects {
97+
units = append(units, repo_model.RepoUnit{
98+
RepoID: repo.ID,
99+
Type: tp,
100+
Config: &repo_model.ProjectsConfig{ProjectsMode: repo_model.ProjectsModeAll},
101+
})
96102
} else {
97103
units = append(units, repo_model.RepoUnit{
98104
RepoID: repo.ID,

modules/structs/repo.go

+3
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ type Repository struct {
9090
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
9191
HasPullRequests bool `json:"has_pull_requests"`
9292
HasProjects bool `json:"has_projects"`
93+
ProjectsMode string `json:"projects_mode"`
9394
HasReleases bool `json:"has_releases"`
9495
HasPackages bool `json:"has_packages"`
9596
HasActions bool `json:"has_actions"`
@@ -180,6 +181,8 @@ type EditRepoOption struct {
180181
HasPullRequests *bool `json:"has_pull_requests,omitempty"`
181182
// either `true` to enable project unit, or `false` to disable them.
182183
HasProjects *bool `json:"has_projects,omitempty"`
184+
// `repo` to only allow repo-level projects, `owner` to only allow owner projects, `all` to allow both.
185+
ProjectsMode *string `json:"projects_mode,omitempty" binding:"In(repo,owner,all)"`
183186
// either `true` to enable releases unit, or `false` to disable them.
184187
HasReleases *bool `json:"has_releases,omitempty"`
185188
// either `true` to enable packages unit, or `false` to disable them.

options/locale/locale_en-US.ini

+5-1
Original file line numberDiff line numberDiff line change
@@ -2090,7 +2090,11 @@ settings.pulls.default_delete_branch_after_merge = Delete pull request branch af
20902090
settings.pulls.default_allow_edits_from_maintainers = Allow edits from maintainers by default
20912091
settings.releases_desc = Enable Repository Releases
20922092
settings.packages_desc = Enable Repository Packages Registry
2093-
settings.projects_desc = Enable Repository Projects
2093+
settings.projects_desc = Enable Projects
2094+
settings.projects_mode_desc = Projects Mode (which kinds of projects to show)
2095+
settings.projects_mode_repo = Repo projects only
2096+
settings.projects_mode_owner = Only user or org projects
2097+
settings.projects_mode_all = All projects
20942098
settings.actions_desc = Enable Repository Actions
20952099
settings.admin_settings = Administrator Settings
20962100
settings.admin_enable_health_check = Enable Repository Health Checks (git fsck)

routers/api/v1/repo/repo.go

+23-3
Original file line numberDiff line numberDiff line change
@@ -944,13 +944,33 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
944944
}
945945
}
946946

947-
if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() {
948-
if *opts.HasProjects {
947+
currHasProjects := repo.UnitEnabled(ctx, unit_model.TypeProjects)
948+
newHasProjects := currHasProjects
949+
if opts.HasProjects != nil {
950+
newHasProjects = *opts.HasProjects
951+
}
952+
if currHasProjects || newHasProjects {
953+
if newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
954+
unit, err := repo.GetUnit(ctx, unit_model.TypeProjects)
955+
var config *repo_model.ProjectsConfig
956+
if err != nil {
957+
config = &repo_model.ProjectsConfig{
958+
ProjectsMode: repo_model.ProjectsModeAll,
959+
}
960+
} else {
961+
config = unit.ProjectsConfig()
962+
}
963+
964+
if opts.ProjectsMode != nil {
965+
config.ProjectsMode = repo_model.ProjectsMode(*opts.ProjectsMode)
966+
}
967+
949968
units = append(units, repo_model.RepoUnit{
950969
RepoID: repo.ID,
951970
Type: unit_model.TypeProjects,
971+
Config: config,
952972
})
953-
} else {
973+
} else if !newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
954974
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
955975
}
956976
}

routers/web/repo/issue.go

+52-41
Original file line numberDiff line numberDiff line change
@@ -587,52 +587,63 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) {
587587
if repo.Owner.IsOrganization() {
588588
repoOwnerType = project_model.TypeOrganization
589589
}
590-
var err error
591-
projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
592-
ListOptions: db.ListOptionsAll,
593-
RepoID: repo.ID,
594-
IsClosed: optional.Some(false),
595-
Type: project_model.TypeRepository,
596-
})
597-
if err != nil {
598-
ctx.ServerError("GetProjects", err)
599-
return
600-
}
601-
projects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
602-
ListOptions: db.ListOptionsAll,
603-
OwnerID: repo.OwnerID,
604-
IsClosed: optional.Some(false),
605-
Type: repoOwnerType,
606-
})
607-
if err != nil {
608-
ctx.ServerError("GetProjects", err)
609-
return
610-
}
611590

612-
ctx.Data["OpenProjects"] = append(projects, projects2...)
591+
projectsUnit := repo.MustGetUnit(ctx, unit.TypeProjects)
613592

614-
projects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
615-
ListOptions: db.ListOptionsAll,
616-
RepoID: repo.ID,
617-
IsClosed: optional.Some(true),
618-
Type: project_model.TypeRepository,
619-
})
620-
if err != nil {
621-
ctx.ServerError("GetProjects", err)
622-
return
593+
var openProjects []*project_model.Project
594+
var closedProjects []*project_model.Project
595+
var err error
596+
597+
if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
598+
openProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
599+
ListOptions: db.ListOptionsAll,
600+
RepoID: repo.ID,
601+
IsClosed: optional.Some(false),
602+
Type: project_model.TypeRepository,
603+
})
604+
if err != nil {
605+
ctx.ServerError("GetProjects", err)
606+
return
607+
}
608+
closedProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
609+
ListOptions: db.ListOptionsAll,
610+
RepoID: repo.ID,
611+
IsClosed: optional.Some(true),
612+
Type: project_model.TypeRepository,
613+
})
614+
if err != nil {
615+
ctx.ServerError("GetProjects", err)
616+
return
617+
}
623618
}
624-
projects2, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{
625-
ListOptions: db.ListOptionsAll,
626-
OwnerID: repo.OwnerID,
627-
IsClosed: optional.Some(true),
628-
Type: repoOwnerType,
629-
})
630-
if err != nil {
631-
ctx.ServerError("GetProjects", err)
632-
return
619+
620+
if projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeOwner) {
621+
openProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
622+
ListOptions: db.ListOptionsAll,
623+
OwnerID: repo.OwnerID,
624+
IsClosed: optional.Some(false),
625+
Type: repoOwnerType,
626+
})
627+
if err != nil {
628+
ctx.ServerError("GetProjects", err)
629+
return
630+
}
631+
openProjects = append(openProjects, openProjects2...)
632+
closedProjects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{
633+
ListOptions: db.ListOptionsAll,
634+
OwnerID: repo.OwnerID,
635+
IsClosed: optional.Some(true),
636+
Type: repoOwnerType,
637+
})
638+
if err != nil {
639+
ctx.ServerError("GetProjects", err)
640+
return
641+
}
642+
closedProjects = append(closedProjects, closedProjects2...)
633643
}
634644

635-
ctx.Data["ClosedProjects"] = append(projects, projects2...)
645+
ctx.Data["OpenProjects"] = openProjects
646+
ctx.Data["ClosedProjects"] = closedProjects
636647
}
637648

638649
// repoReviewerSelection items to bee shown

routers/web/repo/projects.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
issues_model "code.gitea.io/gitea/models/issues"
1515
"code.gitea.io/gitea/models/perm"
1616
project_model "code.gitea.io/gitea/models/project"
17-
attachment_model "code.gitea.io/gitea/models/repo"
17+
repo_model "code.gitea.io/gitea/models/repo"
1818
"code.gitea.io/gitea/models/unit"
1919
"code.gitea.io/gitea/modules/base"
2020
"code.gitea.io/gitea/modules/json"
@@ -33,16 +33,17 @@ const (
3333
tplProjectsView base.TplName = "repo/projects/view"
3434
)
3535

36-
// MustEnableProjects check if projects are enabled in settings
37-
func MustEnableProjects(ctx *context.Context) {
36+
// MustEnableRepoProjects check if repo projects are enabled in settings
37+
func MustEnableRepoProjects(ctx *context.Context) {
3838
if unit.TypeProjects.UnitGlobalDisabled() {
3939
ctx.NotFound("EnableKanbanBoard", nil)
4040
return
4141
}
4242

4343
if ctx.Repo.Repository != nil {
44-
if !ctx.Repo.CanRead(unit.TypeProjects) {
45-
ctx.NotFound("MustEnableProjects", nil)
44+
projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects)
45+
if !ctx.Repo.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
46+
ctx.NotFound("MustEnableRepoProjects", nil)
4647
return
4748
}
4849
}
@@ -325,10 +326,10 @@ func ViewProject(ctx *context.Context) {
325326
}
326327

327328
if project.CardType != project_model.CardTypeTextOnly {
328-
issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
329+
issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
329330
for _, issuesList := range issuesMap {
330331
for _, issue := range issuesList {
331-
if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
332+
if issueAttachment, err := repo_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
332333
issuesAttachmentMap[issue.ID] = issueAttachment
333334
}
334335
}

routers/web/repo/setting/setting.go

+3
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,9 @@ func SettingsPost(ctx *context.Context) {
533533
units = append(units, repo_model.RepoUnit{
534534
RepoID: repo.ID,
535535
Type: unit_model.TypeProjects,
536+
Config: &repo_model.ProjectsConfig{
537+
ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
538+
},
536539
})
537540
} else if !unit_model.TypeProjects.UnitGlobalDisabled() {
538541
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)

routers/web/web.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1344,7 +1344,7 @@ func registerRoutes(m *web.Route) {
13441344
})
13451345
})
13461346
}, reqRepoProjectsWriter, context.RepoMustNotBeArchived())
1347-
}, reqRepoProjectsReader, repo.MustEnableProjects)
1347+
}, reqRepoProjectsReader, repo.MustEnableRepoProjects)
13481348

13491349
m.Group("/actions", func() {
13501350
m.Get("", actions.List)

services/convert/repository.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,11 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
113113
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
114114
}
115115
hasProjects := false
116-
if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
116+
projectsMode := repo_model.ProjectsModeAll
117+
if unit, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
117118
hasProjects = true
119+
config := unit.ProjectsConfig()
120+
projectsMode = config.ProjectsMode
118121
}
119122

120123
hasReleases := false
@@ -211,6 +214,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
211214
InternalTracker: internalTracker,
212215
HasWiki: hasWiki,
213216
HasProjects: hasProjects,
217+
ProjectsMode: string(projectsMode),
214218
HasReleases: hasReleases,
215219
HasPackages: hasPackages,
216220
HasActions: hasActions,

services/forms/repo_form.go

+1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ type RepoSettingForm struct {
142142
ExternalTrackerRegexpPattern string
143143
EnableCloseIssuesViaCommitInAnyBranch bool
144144
EnableProjects bool
145+
ProjectsMode string
145146
EnableReleases bool
146147
EnablePackages bool
147148
EnablePulls bool

templates/repo/header.tmpl

+2-1
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@
174174
</a>
175175
{{end}}
176176

177-
{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects)}}
177+
{{$projectsUnit := .Repository.MustGetUnit $.Context $.UnitTypeProjects}}
178+
{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead $.UnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}}
178179
<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
179180
{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}}
180181
{{if .Repository.NumOpenProjects}}

0 commit comments

Comments
 (0)