diff --git a/models/unit/unit.go b/models/unit/unit.go index b216712d37..e37adf995e 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -108,6 +108,10 @@ var ( // DisabledRepoUnits contains the units that have been globally disabled DisabledRepoUnits = []Type{} + + // AllowedRepoUnitGroups contains the units that have been globally enabled, + // with mutually exclusive units grouped together. + AllowedRepoUnitGroups = [][]Type{} ) // Get valid set of default repository units from settings @@ -162,6 +166,45 @@ func LoadUnitConfig() error { if len(DefaultForkRepoUnits) == 0 { return errors.New("no default fork repository units found") } + + // Collect the allowed repo unit groups. Mutually exclusive units are + // grouped together. + AllowedRepoUnitGroups = [][]Type{} + for _, unit := range []Type{ + TypeCode, + TypePullRequests, + TypeProjects, + TypePackages, + TypeActions, + } { + // If unit is globally disabled, ignore it. + if unit.UnitGlobalDisabled() { + continue + } + + // If it is allowed, add it to the group list. + AllowedRepoUnitGroups = append(AllowedRepoUnitGroups, []Type{unit}) + } + + addMutuallyExclusiveGroup := func(unit1, unit2 Type) { + var list []Type + + if !unit1.UnitGlobalDisabled() { + list = append(list, unit1) + } + + if !unit2.UnitGlobalDisabled() { + list = append(list, unit2) + } + + if len(list) > 0 { + AllowedRepoUnitGroups = append(AllowedRepoUnitGroups, list) + } + } + + addMutuallyExclusiveGroup(TypeIssues, TypeExternalTracker) + addMutuallyExclusiveGroup(TypeWiki, TypeExternalWiki) + return nil } diff --git a/modules/context/repo.go b/modules/context/repo.go index b48f6ded26..727c18cad6 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -81,6 +81,31 @@ func (r *Repository) CanCreateBranch() bool { return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch() } +// AllUnitsEnabled returns true if all units are enabled for the repo. +func (r *Repository) AllUnitsEnabled(ctx context.Context) bool { + hasAnyUnitEnabled := func(unitGroup []unit_model.Type) bool { + // Loop over the group of units + for _, unit := range unitGroup { + // If *any* of them is enabled, return true. + if r.Repository.UnitEnabled(ctx, unit) { + return true + } + } + + // If none are enabled, return false. + return false + } + + for _, unitGroup := range unit_model.AllowedRepoUnitGroups { + // If any disabled unit is found, return false immediately. + if !hasAnyUnitEnabled(unitGroup) { + return false + } + } + + return true +} + // RepoMustNotBeArchived checks if a repo is archived func RepoMustNotBeArchived() func(ctx *Context) { return func(ctx *Context) { @@ -1053,6 +1078,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context ctx.Data["IsViewTag"] = ctx.Repo.IsViewTag ctx.Data["IsViewCommit"] = ctx.Repo.IsViewCommit ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() + ctx.Data["AllUnitsEnabled"] = ctx.Repo.AllUnitsEnabled(ctx) ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() if err != nil { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 95ce92e882..9c8b3fc541 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2066,6 +2066,10 @@ settings.mirror_settings.push_mirror.remote_url = Git Remote Repository URL settings.mirror_settings.push_mirror.add = Add Push Mirror settings.mirror_settings.push_mirror.edit_sync_time = Edit mirror sync interval +settings.units.units = Repository Units +settings.units.overview = Overview +settings.units.add_more = Add more... + settings.sync_mirror = Synchronize Now settings.pull_mirror_sync_in_progress = Pulling changes from the remote %s at the moment. settings.push_mirror_sync_in_progress = Pushing changes to the remote %s at the moment. diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 552507e57c..8a429c359c 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -41,6 +41,7 @@ import ( const ( tplSettingsOptions base.TplName = "repo/settings/options" + tplSettingsUnits base.TplName = "repo/settings/units" tplCollaboration base.TplName = "repo/settings/collaboration" tplBranches base.TplName = "repo/settings/branches" tplGithooks base.TplName = "repo/settings/githooks" @@ -89,6 +90,201 @@ func SettingsCtxData(ctx *context.Context) { ctx.Data["PushMirrors"] = pushMirrors } +// Units show a repositorys unit settings page +func Units(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.settings.units.units") + ctx.Data["PageIsRepoSettingsUnits"] = true + + ctx.HTML(http.StatusOK, tplSettingsUnits) +} + +func UnitsPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RepoUnitSettingForm) + + repo := ctx.Repo.Repository + + var repoChanged bool + var units []repo_model.RepoUnit + var deleteUnitTypes []unit_model.Type + + // This section doesn't require repo_name/RepoName to be set in the form, don't show it + // as an error on the UI for this action + ctx.Data["Err_RepoName"] = nil + + if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch { + repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch + repoChanged = true + } + + if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeCode, + }) + } else if !unit_model.TypeCode.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode) + } + + if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { + if !validation.IsValidExternalURL(form.ExternalWikiURL) { + ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error")) + ctx.Redirect(repo.Link() + "/settings/units") + return + } + + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeExternalWiki, + Config: &repo_model.ExternalWikiConfig{ + ExternalWikiURL: form.ExternalWikiURL, + }, + }) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) + } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { + var wikiPermissions repo_model.UnitAccessMode + if form.GloballyWriteableWiki { + wikiPermissions = repo_model.UnitAccessModeWrite + } else { + wikiPermissions = repo_model.UnitAccessModeRead + } + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeWiki, + Config: new(repo_model.UnitConfig), + DefaultPermissions: wikiPermissions, + }) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) + } else { + if !unit_model.TypeExternalWiki.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) + } + if !unit_model.TypeWiki.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) + } + } + + if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { + if !validation.IsValidExternalURL(form.ExternalTrackerURL) { + ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error")) + ctx.Redirect(repo.Link() + "/settings/units") + return + } + if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) { + ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error")) + ctx.Redirect(repo.Link() + "/settings/units") + return + } + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeExternalTracker, + Config: &repo_model.ExternalTrackerConfig{ + ExternalTrackerURL: form.ExternalTrackerURL, + ExternalTrackerFormat: form.TrackerURLFormat, + ExternalTrackerStyle: form.TrackerIssueStyle, + ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern, + }, + }) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) + } else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeIssues, + Config: &repo_model.IssuesConfig{ + EnableTimetracker: form.EnableTimetracker, + AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime, + EnableDependencies: form.EnableIssueDependencies, + }, + }) + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) + } else { + if !unit_model.TypeExternalTracker.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) + } + if !unit_model.TypeIssues.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) + } + } + + if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeProjects, + }) + } else if !unit_model.TypeProjects.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) + } + + if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeReleases, + }) + } else if !unit_model.TypeReleases.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases) + } + + if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypePackages, + }) + } else if !unit_model.TypePackages.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages) + } + + if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeActions, + }) + } else if !unit_model.TypeActions.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions) + } + + if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypePullRequests, + Config: &repo_model.PullRequestsConfig{ + IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace, + AllowMerge: form.PullsAllowMerge, + AllowRebase: form.PullsAllowRebase, + AllowRebaseMerge: form.PullsAllowRebaseMerge, + AllowSquash: form.PullsAllowSquash, + AllowManualMerge: form.PullsAllowManualMerge, + AutodetectManualMerge: form.EnableAutodetectManualMerge, + AllowRebaseUpdate: form.PullsAllowRebaseUpdate, + DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge, + DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle), + DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit, + }, + }) + } else if !unit_model.TypePullRequests.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests) + } + + if len(units) == 0 { + ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/units") + return + } + + if err := repo_model.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { + ctx.ServerError("UpdateRepositoryUnits", err) + return + } + if repoChanged { + if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { + ctx.ServerError("UpdateRepository", err) + return + } + } + log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings/units") +} + // Settings show a repository's settings page func Settings(ctx *context.Context) { ctx.HTML(http.StatusOK, tplSettingsOptions) @@ -435,188 +631,6 @@ func SettingsPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) ctx.Redirect(repo.Link() + "/settings") - case "advanced": - var repoChanged bool - var units []repo_model.RepoUnit - var deleteUnitTypes []unit_model.Type - - // This section doesn't require repo_name/RepoName to be set in the form, don't show it - // as an error on the UI for this action - ctx.Data["Err_RepoName"] = nil - - if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch { - repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch - repoChanged = true - } - - if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeCode, - }) - } else if !unit_model.TypeCode.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode) - } - - if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { - if !validation.IsValidExternalURL(form.ExternalWikiURL) { - ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error")) - ctx.Redirect(repo.Link() + "/settings") - return - } - - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeExternalWiki, - Config: &repo_model.ExternalWikiConfig{ - ExternalWikiURL: form.ExternalWikiURL, - }, - }) - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) - } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() { - var wikiPermissions repo_model.UnitAccessMode - if form.GloballyWriteableWiki { - wikiPermissions = repo_model.UnitAccessModeWrite - } else { - wikiPermissions = repo_model.UnitAccessModeRead - } - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeWiki, - Config: new(repo_model.UnitConfig), - DefaultPermissions: wikiPermissions, - }) - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) - } else { - if !unit_model.TypeExternalWiki.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) - } - if !unit_model.TypeWiki.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) - } - } - - if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { - if !validation.IsValidExternalURL(form.ExternalTrackerURL) { - ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error")) - ctx.Redirect(repo.Link() + "/settings") - return - } - if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) { - ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error")) - ctx.Redirect(repo.Link() + "/settings") - return - } - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeExternalTracker, - Config: &repo_model.ExternalTrackerConfig{ - ExternalTrackerURL: form.ExternalTrackerURL, - ExternalTrackerFormat: form.TrackerURLFormat, - ExternalTrackerStyle: form.TrackerIssueStyle, - ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern, - }, - }) - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) - } else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeIssues, - Config: &repo_model.IssuesConfig{ - EnableTimetracker: form.EnableTimetracker, - AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime, - EnableDependencies: form.EnableIssueDependencies, - }, - }) - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) - } else { - if !unit_model.TypeExternalTracker.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) - } - if !unit_model.TypeIssues.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) - } - } - - if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeProjects, - }) - } else if !unit_model.TypeProjects.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) - } - - if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeReleases, - }) - } else if !unit_model.TypeReleases.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases) - } - - if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypePackages, - }) - } else if !unit_model.TypePackages.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages) - } - - if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypeActions, - }) - } else if !unit_model.TypeActions.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions) - } - - if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() { - units = append(units, repo_model.RepoUnit{ - RepoID: repo.ID, - Type: unit_model.TypePullRequests, - Config: &repo_model.PullRequestsConfig{ - IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace, - AllowMerge: form.PullsAllowMerge, - AllowRebase: form.PullsAllowRebase, - AllowRebaseMerge: form.PullsAllowRebaseMerge, - AllowSquash: form.PullsAllowSquash, - AllowManualMerge: form.PullsAllowManualMerge, - AutodetectManualMerge: form.EnableAutodetectManualMerge, - AllowRebaseUpdate: form.PullsAllowRebaseUpdate, - DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge, - DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle), - DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit, - }, - }) - } else if !unit_model.TypePullRequests.UnitGlobalDisabled() { - deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests) - } - - if len(units) == 0 { - ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") - return - } - - if err := repo_model.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { - ctx.ServerError("UpdateRepositoryUnits", err) - return - } - if repoChanged { - if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { - ctx.ServerError("UpdateRepository", err) - return - } - } - log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) - - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) - ctx.Redirect(ctx.Repo.RepoLink + "/settings") - case "signing": changed := false trustModel := repo_model.ToTrustModel(form.TrustModel) diff --git a/routers/web/web.go b/routers/web/web.go index 1a83b86fa1..23980d522d 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1034,6 +1034,8 @@ func registerRoutes(m *web.Route) { m.Combo("").Get(repo_setting.Settings). Post(web.Bind(forms.RepoSettingForm{}), repo_setting.SettingsPost) }, repo_setting.SettingsCtxData) + m.Combo("/units").Get(repo_setting.Units). + Post(web.Bind(forms.RepoUnitSettingForm{}), repo_setting.UnitsPost) m.Post("/avatar", web.Bind(forms.AvatarForm{}), repo_setting.SettingsAvatar) m.Post("/avatar/delete", repo_setting.SettingsDeleteAvatar) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 7cc07532ef..9527916ae0 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -130,6 +130,24 @@ type RepoSettingForm struct { EnablePrune bool // Advanced settings + IsArchived bool + + // Signing Settings + TrustModel string + + // Admin settings + EnableHealthCheck bool + RequestReindexType string +} + +// Validate validates the fields +func (f *RepoSettingForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetValidateContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} + +// RepoUnitSettingForm form for changing repository unit settings +type RepoUnitSettingForm struct { EnableCode bool EnableWiki bool GloballyWriteableWiki bool @@ -161,18 +179,10 @@ type RepoSettingForm struct { EnableTimetracker bool AllowOnlyContributorsToTrackTime bool EnableIssueDependencies bool - IsArchived bool - - // Signing Settings - TrustModel string - - // Admin settings - EnableHealthCheck bool - RequestReindexType string } // Validate validates the fields -func (f *RepoSettingForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { +func (f *RepoUnitSettingForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { ctx := context.GetValidateContext(req) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 3e29f1f29e..6fe0b39b52 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -219,6 +219,11 @@ {{end}} {{if .Permission.IsAdmin}} + {{if not .AllUnitsEnabled}} + + {{svg "octicon-diff-added"}} {{ctx.Locale.Tr "repo.settings.units.add_more"}} + + {{end}} {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl index 3bef0fa4c1..62f81a901e 100644 --- a/templates/repo/settings/navbar.tmpl +++ b/templates/repo/settings/navbar.tmpl @@ -4,6 +4,23 @@ {{ctx.Locale.Tr "repo.settings.options"}} +
+ {{ctx.Locale.Tr "repo.settings.units.units"}} + +
{{ctx.Locale.Tr "repo.settings.collaboration"}} diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 191ac53967..6cfef31060 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -299,325 +299,6 @@ {{end}} -

- {{ctx.Locale.Tr "repo.settings.advanced_settings"}} -

-
-
- {{.CsrfTokenHtml}} - - - {{$isCodeEnabled := .Repository.UnitEnabled $.Context $.UnitTypeCode}} - {{$isCodeGlobalDisabled := .UnitTypeCode.UnitGlobalDisabled}} -
- -
- - -
-
- - {{$isWikiEnabled := or (.Repository.UnitEnabled $.Context $.UnitTypeWiki) (.Repository.UnitEnabled $.Context $.UnitTypeExternalWiki)}} - {{$isWikiGlobalDisabled := .UnitTypeWiki.UnitGlobalDisabled}} - {{$isExternalWikiGlobalDisabled := .UnitTypeExternalWiki.UnitGlobalDisabled}} - {{$isBothWikiGlobalDisabled := and $isWikiGlobalDisabled $isExternalWikiGlobalDisabled}} -
- -
- - -
-
-
-
-
- - -
-
- {{if (not .Repository.IsPrivate)}} -
-
-
- - -
-
-
- {{end}} -
-
- - -
-
-
- - -

{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}

-
-
- -
- - {{$isIssuesEnabled := or (.Repository.UnitEnabled $.Context $.UnitTypeIssues) (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}} - {{$isIssuesGlobalDisabled := .UnitTypeIssues.UnitGlobalDisabled}} - {{$isExternalTrackerGlobalDisabled := .UnitTypeExternalTracker.UnitGlobalDisabled}} - {{$isIssuesAndExternalGlobalDisabled := and $isIssuesGlobalDisabled $isExternalTrackerGlobalDisabled}} -
- -
- - -
-
-
-
-
- - -
-
-
- {{if .Repository.CanEnableTimetracker}} -
-
- - -
-
-
-
- - -
-
- {{end}} -
-
- - -
-
-
- - -
-
-
-
- - -
-
-
-
- - -

{{ctx.Locale.Tr "repo.settings.external_tracker_url_desc"}}

-
-
- - -

{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc" | Str2html}}

-
-
- -
-
- {{$externalTracker := (.Repository.MustGetUnit $.Context $.UnitTypeExternalTracker)}} - {{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}} - - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -

{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc" | Str2html}}

-
-
-
- -
- - {{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}} - {{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}} -
- -
- - -
-
- - {{$isReleasesEnabled := .Repository.UnitEnabled $.Context $.UnitTypeReleases}} - {{$isReleasesGlobalDisabled := .UnitTypeReleases.UnitGlobalDisabled}} -
- -
- - -
-
- - {{$isPackagesEnabled := .Repository.UnitEnabled $.Context $.UnitTypePackages}} - {{$isPackagesGlobalDisabled := .UnitTypePackages.UnitGlobalDisabled}} -
- -
- - -
-
- - {{if .EnableActions}} - {{$isActionsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeActions}} - {{$isActionsGlobalDisabled := .UnitTypeActions.UnitGlobalDisabled}} -
- -
- - -
-
- {{end}} - - {{if not .IsMirror}} -
- {{$pullRequestEnabled := .Repository.UnitEnabled $.Context $.UnitTypePullRequests}} - {{$pullRequestGlobalDisabled := .UnitTypePullRequests.UnitGlobalDisabled}} - {{$prUnit := .Repository.MustGetUnit $.Context $.UnitTypePullRequests}} -
- -
- - -
-
-
-
-

- {{ctx.Locale.Tr "repo.settings.merge_style_desc"}} -

-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
- -
-

- {{ctx.Locale.Tr "repo.settings.default_merge_style_desc"}} -

- -
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
- {{end}} - -
-
- -
-
-
-

{{ctx.Locale.Tr "repo.settings.signing_settings"}}

diff --git a/templates/repo/settings/units.tmpl b/templates/repo/settings/units.tmpl new file mode 100644 index 0000000000..66ed035964 --- /dev/null +++ b/templates/repo/settings/units.tmpl @@ -0,0 +1,13 @@ +{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings options")}} +
+
+ {{.CsrfTokenHtml}} + {{template "repo/settings/units/overview" .}} + {{template "repo/settings/units/issues" .}} + {{if not .IsMirror}} + {{template "repo/settings/units/pulls" .}} + {{end}} + {{template "repo/settings/units/wiki" .}} +
+
+{{template "repo/settings/layout_footer" .}} diff --git a/templates/repo/settings/units/issues.tmpl b/templates/repo/settings/units/issues.tmpl new file mode 100644 index 0000000000..a09b8edb2e --- /dev/null +++ b/templates/repo/settings/units/issues.tmpl @@ -0,0 +1,102 @@ +

+ {{ctx.Locale.Tr "repo.issues"}} +

+
+ {{$isIssuesEnabled := or (.Repository.UnitEnabled $.Context $.UnitTypeIssues) (.Repository.UnitEnabled $.Context $.UnitTypeExternalTracker)}} + {{$isIssuesGlobalDisabled := .UnitTypeIssues.UnitGlobalDisabled}} + {{$isExternalTrackerGlobalDisabled := .UnitTypeExternalTracker.UnitGlobalDisabled}} + {{$isIssuesAndExternalGlobalDisabled := and $isIssuesGlobalDisabled $isExternalTrackerGlobalDisabled}} +
+ +
+ + +
+
+
+
+
+ + +
+
+
+ {{if .Repository.CanEnableTimetracker}} +
+
+ + +
+
+
+
+ + +
+
+ {{end}} +
+
+ + +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +

{{ctx.Locale.Tr "repo.settings.external_tracker_url_desc"}}

+
+
+ + +

{{ctx.Locale.Tr "repo.settings.tracker_url_format_desc" | Str2html}}

+
+
+ +
+
+ {{$externalTracker := (.Repository.MustGetUnit $.Context $.UnitTypeExternalTracker)}} + {{$externalTrackerStyle := $externalTracker.ExternalTrackerConfig.ExternalTrackerStyle}} + + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +

{{ctx.Locale.Tr "repo.settings.tracker_issue_style.regexp_pattern_desc" | Str2html}}

+
+
+
+ +
+ +
+ +
+
diff --git a/templates/repo/settings/units/overview.tmpl b/templates/repo/settings/units/overview.tmpl new file mode 100644 index 0000000000..816b45ce1d --- /dev/null +++ b/templates/repo/settings/units/overview.tmpl @@ -0,0 +1,62 @@ +

+ {{ctx.Locale.Tr "repo.settings.units.overview"}} +

+
+ {{$isCodeEnabled := .Repository.UnitEnabled $.Context $.UnitTypeCode}} + {{$isCodeGlobalDisabled := .UnitTypeCode.UnitGlobalDisabled}} +
+ +
+ + +
+
+ + {{$isProjectsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeProjects}} + {{$isProjectsGlobalDisabled := .UnitTypeProjects.UnitGlobalDisabled}} +
+ +
+ + +
+
+ + {{$isReleasesEnabled := .Repository.UnitEnabled $.Context $.UnitTypeReleases}} + {{$isReleasesGlobalDisabled := .UnitTypeReleases.UnitGlobalDisabled}} +
+ +
+ + +
+
+ + {{$isPackagesEnabled := .Repository.UnitEnabled $.Context $.UnitTypePackages}} + {{$isPackagesGlobalDisabled := .UnitTypePackages.UnitGlobalDisabled}} +
+ +
+ + +
+
+ + {{if .EnableActions}} + {{$isActionsEnabled := .Repository.UnitEnabled $.Context $.UnitTypeActions}} + {{$isActionsGlobalDisabled := .UnitTypeActions.UnitGlobalDisabled}} +
+ +
+ + +
+
+ {{end}} + +
+ +
+ +
+
diff --git a/templates/repo/settings/units/pulls.tmpl b/templates/repo/settings/units/pulls.tmpl new file mode 100644 index 0000000000..e735fe974c --- /dev/null +++ b/templates/repo/settings/units/pulls.tmpl @@ -0,0 +1,121 @@ +

+ {{ctx.Locale.Tr "repo.pulls"}} +

+
+ {{$pullRequestEnabled := .Repository.UnitEnabled $.Context $.UnitTypePullRequests}} + {{$pullRequestGlobalDisabled := .UnitTypePullRequests.UnitGlobalDisabled}} + {{$prUnit := .Repository.MustGetUnit $.Context $.UnitTypePullRequests}} +
+ +
+ + +
+
+
+
+

+ {{ctx.Locale.Tr "repo.settings.merge_style_desc"}} +

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+

+ {{ctx.Locale.Tr "repo.settings.default_merge_style_desc"}} +

+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+ +
+
diff --git a/templates/repo/settings/units/wiki.tmpl b/templates/repo/settings/units/wiki.tmpl new file mode 100644 index 0000000000..c3be39f1cc --- /dev/null +++ b/templates/repo/settings/units/wiki.tmpl @@ -0,0 +1,51 @@ +

+ {{ctx.Locale.Tr "repo.wiki"}} +

+
+ {{$isWikiEnabled := or (.Repository.UnitEnabled $.Context $.UnitTypeWiki) (.Repository.UnitEnabled $.Context $.UnitTypeExternalWiki)}} + {{$isWikiGlobalDisabled := .UnitTypeWiki.UnitGlobalDisabled}} + {{$isExternalWikiGlobalDisabled := .UnitTypeExternalWiki.UnitGlobalDisabled}} + {{$isBothWikiGlobalDisabled := and $isWikiGlobalDisabled $isExternalWikiGlobalDisabled}} +
+ +
+ + +
+
+
+
+
+ + +
+
+ {{if (not .Repository.IsPrivate)}} +
+
+
+ + +
+
+
+ {{end}} +
+
+ + +
+
+
+ + +

{{ctx.Locale.Tr "repo.settings.external_wiki_url_desc"}}

+
+
+ +
+ +
+ +
+
diff --git a/tests/integration/repo_settings_test.go b/tests/integration/repo_settings_test.go new file mode 100644 index 0000000000..16e0bb3d7d --- /dev/null +++ b/tests/integration/repo_settings_test.go @@ -0,0 +1,130 @@ +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" + repo_service "code.gitea.io/gitea/services/repository" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestRepoSettingsUnits(t *testing.T) { + defer tests.PrepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, Name: "repo1"}) + session := loginUser(t, user.Name) + + req := NewRequest(t, "GET", fmt.Sprintf("%s/settings/units", repo.Link())) + session.MakeRequest(t, req, http.StatusOK) +} + +func TestRepoAddMoreUnits(t *testing.T) { + defer tests.PrepareTestEnv(t)() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) + session := loginUser(t, user.Name) + + // Make sure there are no disabled repos in the settings! + setting.Repository.DisabledRepoUnits = []string{} + unit_model.LoadUnitConfig() + + // Create a known-good repo, with all units enabled. + repo, _, f := CreateDeclarativeRepo(t, user, "", []unit_model.Type{ + unit_model.TypeCode, + unit_model.TypePullRequests, + unit_model.TypeProjects, + unit_model.TypePackages, + unit_model.TypeActions, + unit_model.TypeIssues, + unit_model.TypeWiki, + }, nil, nil) + defer f() + + assertAddMore := func(t *testing.T, present bool) { + t.Helper() + + req := NewRequest(t, "GET", repo.Link()) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, fmt.Sprintf("a[href='%s/settings/units']", repo.Link()), present) + } + + t.Run("no add more with all units enabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + assertAddMore(t, false) + }) + + t.Run("add more if units can be enabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer func() { + repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{ + RepoID: repo.ID, + Type: unit_model.TypePackages, + }}, nil) + }() + + // Disable the Packages unit + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, nil, []unit_model.Type{unit_model.TypePackages}) + assert.NoError(t, err) + + assertAddMore(t, true) + }) + + t.Run("no add more if unit is globally disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer func() { + repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{ + RepoID: repo.ID, + Type: unit_model.TypePackages, + }}, nil) + setting.Repository.DisabledRepoUnits = []string{} + unit_model.LoadUnitConfig() + }() + + // Disable the Packages unit globally + setting.Repository.DisabledRepoUnits = []string{"repo.packages"} + unit_model.LoadUnitConfig() + + // Disable the Packages unit + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, nil, []unit_model.Type{unit_model.TypePackages}) + assert.NoError(t, err) + + // The "Add more" link appears no more + assertAddMore(t, false) + }) + + t.Run("issues & ext tracker globally disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer func() { + repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{{ + RepoID: repo.ID, + Type: unit_model.TypeIssues, + }}, nil) + setting.Repository.DisabledRepoUnits = []string{} + unit_model.LoadUnitConfig() + }() + + // Disable both Issues and ExternalTracker units globally + setting.Repository.DisabledRepoUnits = []string{"repo.issues", "repo.ext_issues"} + unit_model.LoadUnitConfig() + + // Disable the Issues unit + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, nil, []unit_model.Type{unit_model.TypeIssues}) + assert.NoError(t, err) + + // The "Add more" link appears no more + assertAddMore(t, false) + }) +}