From 3c07d03c0388d3b86138572401281b51f2db9282 Mon Sep 17 00:00:00 2001
From: David Svantesson <davidsvantesson@gmail.com>
Date: Fri, 17 Jan 2020 08:34:37 +0100
Subject: [PATCH] Add setting to set default and global disabled repository
 units. (#8788)

* Add possibility to global disable repo units.

* Add Default Repo Unit app.ini setting.

* Hide units

* Hide disabled repo units

* Minor fixes

* Indicate disabled units in team settings.

Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: zeripath <art27@cantab.net>
---
 custom/conf/app.ini.sample           |   7 ++
 models/repo.go                       |  11 +-
 models/repo_unit.go                  |  13 ++-
 models/unit.go                       |  79 +++++++++++++-
 models/user.go                       |   3 +
 modules/setting/repository.go        |   4 +
 options/locale/locale_en-US.ini      |   2 +
 routers/api/v1/repo/repo.go          | 148 +++++++++++++--------------
 routers/repo/setting.go              | 125 +++++++++++-----------
 routers/routes/routes.go             |   5 +
 routers/user/home.go                 |  18 ++++
 templates/base/head_navbar.tmpl      |   6 ++
 templates/org/team/new.tmpl          |   6 +-
 templates/repo/settings/options.tmpl |  28 +++++
 14 files changed, 315 insertions(+), 140 deletions(-)

diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample
index 29e147add8..7e7dbbf5f3 100644
--- a/custom/conf/app.ini.sample
+++ b/custom/conf/app.ini.sample
@@ -42,6 +42,13 @@ DEFAULT_CLOSE_ISSUES_VIA_COMMITS_IN_ANY_BRANCH = false
 ; Allow users to push local repositories to Gitea and have them automatically created for a user or an org
 ENABLE_PUSH_CREATE_USER = false
 ENABLE_PUSH_CREATE_ORG = false
+; Comma separated list of globally disabled repo units. Allowed values: repo.issues, repo.ext_issues, repo.pulls, repo.wiki, repo.ext_wiki
+DISABLED_REPO_UNITS = 
+; Comma separated list of default repo units. Allowed values: repo.code, repo.releases, repo.issues, repo.pulls, repo.wiki.
+; Note: Code and Releases can currently not be deactivated. If you specify default repo units you should still list them for future compatibility.
+; External wiki and issue tracker can't be enabled by default as it requires additional settings.
+; Disabled repo units will not be added to new repositories regardless if it is in the default list.
+DEFAULT_REPO_UNITS = repo.code,repo.releases,repo.issues,repo.pulls,repo.wiki
 
 [repository.editor]
 ; List of file extensions for which lines should be wrapped in the CodeMirror editor
diff --git a/models/repo.go b/models/repo.go
index 2c9dafefc9..6c89dbcbbb 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -128,6 +128,7 @@ func loadRepoConfig() {
 // NewRepoContext creates a new repository context
 func NewRepoContext() {
 	loadRepoConfig()
+	loadUnitConfig()
 
 	RemoveAllWithNotice("Clean up repository temporary data", filepath.Join(setting.AppDataPath, "tmp"))
 }
@@ -393,6 +394,7 @@ func (repo *Repository) getUnits(e Engine) (err error) {
 	}
 
 	repo.Units, err = getUnitsByRepoID(e, repo.ID)
+	log.Trace("repo.Units: %-+v", repo.Units)
 	return err
 }
 
@@ -1442,14 +1444,19 @@ func UpdateRepositoryUpdatedTime(repoID int64, updateTime time.Time) error {
 }
 
 // UpdateRepositoryUnits updates a repository's units
-func UpdateRepositoryUnits(repo *Repository, units []RepoUnit) (err error) {
+func UpdateRepositoryUnits(repo *Repository, units []RepoUnit, deleteUnitTypes []UnitType) (err error) {
 	sess := x.NewSession()
 	defer sess.Close()
 	if err = sess.Begin(); err != nil {
 		return err
 	}
 
-	if _, err = sess.Where("repo_id = ?", repo.ID).Delete(new(RepoUnit)); err != nil {
+	// Delete existing settings of units before adding again
+	for _, u := range units {
+		deleteUnitTypes = append(deleteUnitTypes, u.Type)
+	}
+
+	if _, err = sess.Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(RepoUnit)); err != nil {
 		return err
 	}
 
diff --git a/models/repo_unit.go b/models/repo_unit.go
index a6162a65e5..ec680c395e 100644
--- a/models/repo_unit.go
+++ b/models/repo_unit.go
@@ -170,5 +170,16 @@ func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig {
 }
 
 func getUnitsByRepoID(e Engine, repoID int64) (units []*RepoUnit, err error) {
-	return units, e.Where("repo_id = ?", repoID).Find(&units)
+	var tmpUnits []*RepoUnit
+	if err := e.Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {
+		return nil, err
+	}
+
+	for _, u := range tmpUnits {
+		if !u.Type.UnitGlobalDisabled() {
+			units = append(units, u)
+		}
+	}
+
+	return units, nil
 }
diff --git a/models/unit.go b/models/unit.go
index 9f5c8d3cbb..bd2e6b13a6 100644
--- a/models/unit.go
+++ b/models/unit.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
 )
 
 // UnitType is Unit's Type
@@ -78,13 +79,89 @@ var (
 		UnitTypeWiki,
 	}
 
+	// NotAllowedDefaultRepoUnits contains units that can't be default
+	NotAllowedDefaultRepoUnits = []UnitType{
+		UnitTypeExternalWiki,
+		UnitTypeExternalTracker,
+	}
+
 	// MustRepoUnits contains the units could not be disabled currently
 	MustRepoUnits = []UnitType{
 		UnitTypeCode,
 		UnitTypeReleases,
 	}
+
+	// DisabledRepoUnits contains the units that have been globally disabled
+	DisabledRepoUnits = []UnitType{}
 )
 
+func loadUnitConfig() {
+	setDefaultRepoUnits := FindUnitTypes(setting.Repository.DefaultRepoUnits...)
+	// Default repo units set if setting is not empty
+	if len(setDefaultRepoUnits) > 0 {
+		// MustRepoUnits required as default
+		DefaultRepoUnits = make([]UnitType, len(MustRepoUnits))
+		copy(DefaultRepoUnits, MustRepoUnits)
+		for _, defaultU := range setDefaultRepoUnits {
+			if !defaultU.CanBeDefault() {
+				log.Warn("Not allowed as default unit: %s", defaultU.String())
+				continue
+			}
+			// MustRepoUnits already added
+			if defaultU.CanDisable() {
+				DefaultRepoUnits = append(DefaultRepoUnits, defaultU)
+			}
+		}
+	}
+
+	DisabledRepoUnits = FindUnitTypes(setting.Repository.DisabledRepoUnits...)
+	// Check that must units are not disabled
+	for i, disabledU := range DisabledRepoUnits {
+		if !disabledU.CanDisable() {
+			log.Warn("Not allowed to global disable unit %s", disabledU.String())
+			DisabledRepoUnits = append(DisabledRepoUnits[:i], DisabledRepoUnits[i+1:]...)
+		}
+	}
+	// Remove disabled units from default units
+	for _, disabledU := range DisabledRepoUnits {
+		for i, defaultU := range DefaultRepoUnits {
+			if defaultU == disabledU {
+				DefaultRepoUnits = append(DefaultRepoUnits[:i], DefaultRepoUnits[i+1:]...)
+			}
+		}
+	}
+}
+
+// UnitGlobalDisabled checks if unit type is global disabled
+func (u UnitType) UnitGlobalDisabled() bool {
+	for _, ud := range DisabledRepoUnits {
+		if u == ud {
+			return true
+		}
+	}
+	return false
+}
+
+// CanDisable checks if this unit type can be disabled.
+func (u *UnitType) CanDisable() bool {
+	for _, mu := range MustRepoUnits {
+		if *u == mu {
+			return false
+		}
+	}
+	return true
+}
+
+// CanBeDefault checks if the unit type can be a default repo unit
+func (u *UnitType) CanBeDefault() bool {
+	for _, nadU := range NotAllowedDefaultRepoUnits {
+		if *u == nadU {
+			return false
+		}
+	}
+	return true
+}
+
 // Unit is a section of one repository
 type Unit struct {
 	Type    UnitType
@@ -96,7 +173,7 @@ type Unit struct {
 
 // CanDisable returns if this unit could be disabled.
 func (u *Unit) CanDisable() bool {
-	return true
+	return u.Type.CanDisable()
 }
 
 // IsLessThan compares order of two units
diff --git a/models/user.go b/models/user.go
index d7129fb09a..4a4af3547a 100644
--- a/models/user.go
+++ b/models/user.go
@@ -622,6 +622,7 @@ func (u *User) GetRepositories(page, pageSize int) (err error) {
 }
 
 // GetRepositoryIDs returns repositories IDs where user owned and has unittypes
+// Caller shall check that units is not globally disabled
 func (u *User) GetRepositoryIDs(units ...UnitType) ([]int64, error) {
 	var ids []int64
 
@@ -636,6 +637,7 @@ func (u *User) GetRepositoryIDs(units ...UnitType) ([]int64, error) {
 }
 
 // GetOrgRepositoryIDs returns repositories IDs where user's team owned and has unittypes
+// Caller shall check that units is not globally disabled
 func (u *User) GetOrgRepositoryIDs(units ...UnitType) ([]int64, error) {
 	var ids []int64
 
@@ -656,6 +658,7 @@ func (u *User) GetOrgRepositoryIDs(units ...UnitType) ([]int64, error) {
 }
 
 // GetAccessRepoIDs returns all repositories IDs where user's or user is a team member organizations
+// Caller shall check that units is not globally disabled
 func (u *User) GetAccessRepoIDs(units ...UnitType) ([]int64, error) {
 	ids, err := u.GetRepositoryIDs(units...)
 	if err != nil {
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index 06797e891b..807b29b2d8 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -37,6 +37,8 @@ var (
 		DefaultCloseIssuesViaCommitsInAnyBranch bool
 		EnablePushCreateUser                    bool
 		EnablePushCreateOrg                     bool
+		DisabledRepoUnits                       []string
+		DefaultRepoUnits                        []string
 
 		// Repository editor settings
 		Editor struct {
@@ -98,6 +100,8 @@ var (
 		DefaultCloseIssuesViaCommitsInAnyBranch: false,
 		EnablePushCreateUser:                    false,
 		EnablePushCreateOrg:                     false,
+		DisabledRepoUnits:                       []string{},
+		DefaultRepoUnits:                        []string{},
 
 		// Repository editor settings
 		Editor: struct {
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 9a4f0535e8..0f3e9f4a3d 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -636,6 +636,7 @@ stargazers = Stargazers
 forks = Forks
 pick_reaction = Pick your reaction
 reactions_more = and %d more
+unit_disabled = The site administrator has disabled this repository section.
 
 template.items = Template Items
 template.git_content = Git Content (Default Branch)
@@ -1613,6 +1614,7 @@ team_desc_helper = Describe the purpose or role of the team.
 team_access_desc = Repository access
 team_permission_desc = Permission
 team_unit_desc = Allow Access to Repository Sections
+team_unit_disabled = (Disabled)
 
 form.name_reserved = The organization name '%s' is reserved.
 form.name_pattern_not_allowed = The pattern '%s' is not allowed in an organization name.
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 9ae0c4af4e..a13f6ebe0d 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -757,25 +757,10 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
 	repo := ctx.Repo.Repository
 
 	var units []models.RepoUnit
+	var deleteUnitTypes []models.UnitType
 
-	for _, tp := range models.MustRepoUnits {
-		units = append(units, models.RepoUnit{
-			RepoID: repo.ID,
-			Type:   tp,
-			Config: new(models.UnitConfig),
-		})
-	}
-
-	if opts.HasIssues == nil {
-		// If HasIssues setting not touched, rewrite existing repo unit
-		if unit, err := repo.GetUnit(models.UnitTypeIssues); err == nil {
-			units = append(units, *unit)
-		} else if unit, err := repo.GetUnit(models.UnitTypeExternalTracker); err == nil {
-			units = append(units, *unit)
-		}
-	} else if *opts.HasIssues {
-		if opts.ExternalTracker != nil {
-
+	if opts.HasIssues != nil {
+		if *opts.HasIssues && opts.ExternalTracker != nil && !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
 			// Check that values are valid
 			if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) {
 				err := fmt.Errorf("External tracker URL not valid")
@@ -797,7 +782,8 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
 					ExternalTrackerStyle:  opts.ExternalTracker.ExternalTrackerStyle,
 				},
 			})
-		} else {
+			deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
+		} else if *opts.HasIssues && opts.ExternalTracker == nil && !models.UnitTypeIssues.UnitGlobalDisabled() {
 			// Default to built-in tracker
 			var config *models.IssuesConfig
 
@@ -823,19 +809,19 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
 				Type:   models.UnitTypeIssues,
 				Config: config,
 			})
+			deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
+		} else if !*opts.HasIssues {
+			if !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
+				deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
+			}
+			if !models.UnitTypeIssues.UnitGlobalDisabled() {
+				deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
+			}
 		}
 	}
 
-	if opts.HasWiki == nil {
-		// If HasWiki setting not touched, rewrite existing repo unit
-		if unit, err := repo.GetUnit(models.UnitTypeWiki); err == nil {
-			units = append(units, *unit)
-		} else if unit, err := repo.GetUnit(models.UnitTypeExternalWiki); err == nil {
-			units = append(units, *unit)
-		}
-	} else if *opts.HasWiki {
-		if opts.ExternalWiki != nil {
-
+	if opts.HasWiki != nil {
+		if *opts.HasWiki && opts.ExternalWiki != nil && !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
 			// Check that values are valid
 			if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) {
 				err := fmt.Errorf("External wiki URL not valid")
@@ -850,64 +836,72 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
 					ExternalWikiURL: opts.ExternalWiki.ExternalWikiURL,
 				},
 			})
-		} else {
+			deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
+		} else if *opts.HasWiki && opts.ExternalWiki == nil && !models.UnitTypeWiki.UnitGlobalDisabled() {
 			config := &models.UnitConfig{}
 			units = append(units, models.RepoUnit{
 				RepoID: repo.ID,
 				Type:   models.UnitTypeWiki,
 				Config: config,
 			})
-		}
-	}
-
-	if opts.HasPullRequests == nil {
-		// If HasPullRequest setting not touched, rewrite existing repo unit
-		if unit, err := repo.GetUnit(models.UnitTypePullRequests); err == nil {
-			units = append(units, *unit)
-		}
-	} else if *opts.HasPullRequests {
-		// We do allow setting individual PR settings through the API, so
-		// we get the config settings and then set them
-		// if those settings were provided in the opts.
-		unit, err := repo.GetUnit(models.UnitTypePullRequests)
-		var config *models.PullRequestsConfig
-		if err != nil {
-			// Unit type doesn't exist so we make a new config file with default values
-			config = &models.PullRequestsConfig{
-				IgnoreWhitespaceConflicts: false,
-				AllowMerge:                true,
-				AllowRebase:               true,
-				AllowRebaseMerge:          true,
-				AllowSquash:               true,
+			deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
+		} else if !*opts.HasWiki {
+			if !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
+				deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
+			}
+			if !models.UnitTypeWiki.UnitGlobalDisabled() {
+				deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
 			}
-		} else {
-			config = unit.PullRequestsConfig()
 		}
-
-		if opts.IgnoreWhitespaceConflicts != nil {
-			config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts
-		}
-		if opts.AllowMerge != nil {
-			config.AllowMerge = *opts.AllowMerge
-		}
-		if opts.AllowRebase != nil {
-			config.AllowRebase = *opts.AllowRebase
-		}
-		if opts.AllowRebaseMerge != nil {
-			config.AllowRebaseMerge = *opts.AllowRebaseMerge
-		}
-		if opts.AllowSquash != nil {
-			config.AllowSquash = *opts.AllowSquash
-		}
-
-		units = append(units, models.RepoUnit{
-			RepoID: repo.ID,
-			Type:   models.UnitTypePullRequests,
-			Config: config,
-		})
 	}
 
-	if err := models.UpdateRepositoryUnits(repo, units); err != nil {
+	if opts.HasPullRequests != nil {
+		if *opts.HasPullRequests && !models.UnitTypePullRequests.UnitGlobalDisabled() {
+			// We do allow setting individual PR settings through the API, so
+			// we get the config settings and then set them
+			// if those settings were provided in the opts.
+			unit, err := repo.GetUnit(models.UnitTypePullRequests)
+			var config *models.PullRequestsConfig
+			if err != nil {
+				// Unit type doesn't exist so we make a new config file with default values
+				config = &models.PullRequestsConfig{
+					IgnoreWhitespaceConflicts: false,
+					AllowMerge:                true,
+					AllowRebase:               true,
+					AllowRebaseMerge:          true,
+					AllowSquash:               true,
+				}
+			} else {
+				config = unit.PullRequestsConfig()
+			}
+
+			if opts.IgnoreWhitespaceConflicts != nil {
+				config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts
+			}
+			if opts.AllowMerge != nil {
+				config.AllowMerge = *opts.AllowMerge
+			}
+			if opts.AllowRebase != nil {
+				config.AllowRebase = *opts.AllowRebase
+			}
+			if opts.AllowRebaseMerge != nil {
+				config.AllowRebaseMerge = *opts.AllowRebaseMerge
+			}
+			if opts.AllowSquash != nil {
+				config.AllowSquash = *opts.AllowSquash
+			}
+
+			units = append(units, models.RepoUnit{
+				RepoID: repo.ID,
+				Type:   models.UnitTypePullRequests,
+				Config: config,
+			})
+		} else if !*opts.HasPullRequests && !models.UnitTypePullRequests.UnitGlobalDisabled() {
+			deleteUnitTypes = append(deleteUnitTypes, models.UnitTypePullRequests)
+		}
+	}
+
+	if err := models.UpdateRepositoryUnits(repo, units, deleteUnitTypes); err != nil {
 		ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err)
 		return err
 	}
diff --git a/routers/repo/setting.go b/routers/repo/setting.go
index b2330f4ebc..6ad0b4a967 100644
--- a/routers/repo/setting.go
+++ b/routers/repo/setting.go
@@ -205,78 +205,85 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
 
 	case "advanced":
 		var units []models.RepoUnit
+		var deleteUnitTypes []models.UnitType
 
 		// 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
 
-		for _, tp := range models.MustRepoUnits {
+		if form.EnableWiki && form.EnableExternalWiki && !models.UnitTypeExternalWiki.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, models.RepoUnit{
 				RepoID: repo.ID,
-				Type:   tp,
+				Type:   models.UnitTypeExternalWiki,
+				Config: &models.ExternalWikiConfig{
+					ExternalWikiURL: form.ExternalWikiURL,
+				},
+			})
+			deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
+		} else if form.EnableWiki && !form.EnableExternalWiki && !models.UnitTypeWiki.UnitGlobalDisabled() {
+			units = append(units, models.RepoUnit{
+				RepoID: repo.ID,
+				Type:   models.UnitTypeWiki,
 				Config: new(models.UnitConfig),
 			})
-		}
-
-		if form.EnableWiki {
-			if form.EnableExternalWiki {
-				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, models.RepoUnit{
-					RepoID: repo.ID,
-					Type:   models.UnitTypeExternalWiki,
-					Config: &models.ExternalWikiConfig{
-						ExternalWikiURL: form.ExternalWikiURL,
-					},
-				})
-			} else {
-				units = append(units, models.RepoUnit{
-					RepoID: repo.ID,
-					Type:   models.UnitTypeWiki,
-					Config: new(models.UnitConfig),
-				})
+			deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
+		} else {
+			if !models.UnitTypeExternalWiki.UnitGlobalDisabled() {
+				deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalWiki)
+			}
+			if !models.UnitTypeWiki.UnitGlobalDisabled() {
+				deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeWiki)
 			}
 		}
 
-		if form.EnableIssues {
-			if form.EnableExternalTracker {
-				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, models.RepoUnit{
-					RepoID: repo.ID,
-					Type:   models.UnitTypeExternalTracker,
-					Config: &models.ExternalTrackerConfig{
-						ExternalTrackerURL:    form.ExternalTrackerURL,
-						ExternalTrackerFormat: form.TrackerURLFormat,
-						ExternalTrackerStyle:  form.TrackerIssueStyle,
-					},
-				})
-			} else {
-				units = append(units, models.RepoUnit{
-					RepoID: repo.ID,
-					Type:   models.UnitTypeIssues,
-					Config: &models.IssuesConfig{
-						EnableTimetracker:                form.EnableTimetracker,
-						AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
-						EnableDependencies:               form.EnableIssueDependencies,
-					},
-				})
+		if form.EnableIssues && form.EnableExternalTracker && !models.UnitTypeExternalTracker.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, models.RepoUnit{
+				RepoID: repo.ID,
+				Type:   models.UnitTypeExternalTracker,
+				Config: &models.ExternalTrackerConfig{
+					ExternalTrackerURL:    form.ExternalTrackerURL,
+					ExternalTrackerFormat: form.TrackerURLFormat,
+					ExternalTrackerStyle:  form.TrackerIssueStyle,
+				},
+			})
+			deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
+		} else if form.EnableIssues && !form.EnableExternalTracker && !models.UnitTypeIssues.UnitGlobalDisabled() {
+			units = append(units, models.RepoUnit{
+				RepoID: repo.ID,
+				Type:   models.UnitTypeIssues,
+				Config: &models.IssuesConfig{
+					EnableTimetracker:                form.EnableTimetracker,
+					AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
+					EnableDependencies:               form.EnableIssueDependencies,
+				},
+			})
+			deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
+		} else {
+			if !models.UnitTypeExternalTracker.UnitGlobalDisabled() {
+				deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeExternalTracker)
+			}
+			if !models.UnitTypeIssues.UnitGlobalDisabled() {
+				deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeIssues)
 			}
 		}
 
-		if form.EnablePulls {
+		if form.EnablePulls && !models.UnitTypePullRequests.UnitGlobalDisabled() {
 			units = append(units, models.RepoUnit{
 				RepoID: repo.ID,
 				Type:   models.UnitTypePullRequests,
@@ -288,9 +295,11 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
 					AllowSquash:               form.PullsAllowSquash,
 				},
 			})
+		} else if !models.UnitTypePullRequests.UnitGlobalDisabled() {
+			deleteUnitTypes = append(deleteUnitTypes, models.UnitTypePullRequests)
 		}
 
-		if err := models.UpdateRepositoryUnits(repo, units); err != nil {
+		if err := models.UpdateRepositoryUnits(repo, units, deleteUnitTypes); err != nil {
 			ctx.ServerError("UpdateRepositoryUnits", err)
 			return
 		}
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index 7e81f55de6..74bddc79e5 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -261,6 +261,11 @@ func RegisterRoutes(m *macaron.Macaron) {
 	}
 
 	m.Use(user.GetNotificationCount)
+	m.Use(func(ctx *context.Context) {
+		ctx.Data["UnitWikiGlobalDisabled"] = models.UnitTypeWiki.UnitGlobalDisabled()
+		ctx.Data["UnitIssuesGlobalDisabled"] = models.UnitTypeIssues.UnitGlobalDisabled()
+		ctx.Data["UnitPullsGlobalDisabled"] = models.UnitTypePullRequests.UnitGlobalDisabled()
+	})
 
 	// FIXME: not all routes need go through same middlewares.
 	// Especially some AJAX requests, we can reduce middleware number to improve performance.
diff --git a/routers/user/home.go b/routers/user/home.go
index 822452f1ca..0d78b17dad 100644
--- a/routers/user/home.go
+++ b/routers/user/home.go
@@ -158,6 +158,12 @@ func Dashboard(ctx *context.Context) {
 
 // Milestones render the user milestones page
 func Milestones(ctx *context.Context) {
+	if models.UnitTypeIssues.UnitGlobalDisabled() && models.UnitTypePullRequests.UnitGlobalDisabled() {
+		log.Debug("Milestones overview page not available as both issues and pull requests are globally disabled")
+		ctx.Status(404)
+		return
+	}
+
 	ctx.Data["Title"] = ctx.Tr("milestones")
 	ctx.Data["PageIsMilestonesDashboard"] = true
 
@@ -335,10 +341,22 @@ func Issues(ctx *context.Context) {
 	isPullList := ctx.Params(":type") == "pulls"
 	unitType := models.UnitTypeIssues
 	if isPullList {
+		if models.UnitTypePullRequests.UnitGlobalDisabled() {
+			log.Debug("Pull request overview page not available as it is globally disabled.")
+			ctx.Status(404)
+			return
+		}
+
 		ctx.Data["Title"] = ctx.Tr("pull_requests")
 		ctx.Data["PageIsPulls"] = true
 		unitType = models.UnitTypePullRequests
 	} else {
+		if models.UnitTypeIssues.UnitGlobalDisabled() {
+			log.Debug("Issues overview page not available as it is globally disabled.")
+			ctx.Status(404)
+			return
+		}
+
 		ctx.Data["Title"] = ctx.Tr("issues")
 		ctx.Data["PageIsIssues"] = true
 	}
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index a09b4b832e..5f1b9405d9 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -10,9 +10,15 @@
 
 	{{if .IsSigned}}
 		<a class="item {{if .PageIsDashboard}}active{{end}}" href="{{AppSubUrl}}/">{{.i18n.Tr "dashboard"}}</a>
+		{{if not .UnitIssuesGlobalDisabled}}
 		<a class="item {{if .PageIsIssues}}active{{end}}" href="{{AppSubUrl}}/issues">{{.i18n.Tr "issues"}}</a>
+		{{end}}
+		{{if not .UnitPullsGlobalDisabled}}
 		<a class="item {{if .PageIsPulls}}active{{end}}" href="{{AppSubUrl}}/pulls">{{.i18n.Tr "pull_requests"}}</a>
+		{{end}}
+		{{if not (and .UnitIssuesGlobalDisabled .UnitPullsGlobalDisabled)}}
 		{{if .ShowMilestonesDashboardPage}}<a class="item {{if .PageIsMilestonesDashboard}}active{{end}}" href="{{AppSubUrl}}/milestones">{{.i18n.Tr "milestones"}}</a>{{end}}
+		{{end}}
 		<a class="item {{if .PageIsExplore}}active{{end}}" href="{{AppSubUrl}}/explore/repos">{{.i18n.Tr "explore"}}</a>
 	{{else if .IsLandingPageHome}}
 		<a class="item {{if .PageIsHome}}active{{end}}" href="{{AppSubUrl}}/">{{.i18n.Tr "home"}}</a>
diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl
index c38fa4d940..228f86824a 100644
--- a/templates/org/team/new.tmpl
+++ b/templates/org/team/new.tmpl
@@ -81,10 +81,14 @@
 							<label>{{.i18n.Tr "org.team_unit_desc"}}</label>
 							<br>
 							{{range $t, $unit := $.Units}}
+							{{if $unit.Type.UnitGlobalDisabled}}
+							<div class="field poping up" data-content="{{$.i18n.Tr "repo.unit_disabled"}}">
+							{{else}}
 							<div class="field">
+							{{end}}
 								<div class="ui toggle checkbox">
 									<input type="checkbox" class="hidden" name="units" value="{{$unit.Type.Value}}"{{if or (eq $.Team.ID 0) ($.Team.UnitEnabled $unit.Type)}} checked{{end}}>
-									<label>{{$.i18n.Tr $unit.NameKey}}</label>
+									<label>{{$.i18n.Tr $unit.NameKey}}{{if $unit.Type.UnitGlobalDisabled}} {{$.i18n.Tr "org.team_unit_disabled"}}{{end}}</label>
 									<span class="help">{{$.i18n.Tr $unit.DescKey}}</span>
 								</div>
 							</div>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 6f96ff7f47..c674fcf7f9 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -144,20 +144,32 @@
 				{{$isWikiEnabled := or (.Repository.UnitEnabled $.UnitTypeWiki) (.Repository.UnitEnabled $.UnitTypeExternalWiki)}}
 				<div class="inline field">
 					<label>{{.i18n.Tr "repo.wiki"}}</label>
+					{{if and (.UnitTypeWiki.UnitGlobalDisabled) (.UnitTypeExternalWiki.UnitGlobalDisabled)}}
+					<div class="ui checkbox poping up disabled" data-content="{{.i18n.Tr "repo.unit_disabled"}}">
+					{{else}}
 					<div class="ui checkbox">
+					{{end}}
 						<input class="enable-system" name="enable_wiki" type="checkbox" data-target="#wiki_box" {{if $isWikiEnabled}}checked{{end}}>
 						<label>{{.i18n.Tr "repo.settings.wiki_desc"}}</label>
 					</div>
 				</div>
 				<div class="field {{if not $isWikiEnabled}}disabled{{end}}" id="wiki_box">
 					<div class="field">
+						{{if .UnitTypeWiki.UnitGlobalDisabled}}
+						<div class="ui radio checkbox poping up disabled" data-content="{{.i18n.Tr "repo.unit_disabled"}}">
+						{{else}}
 						<div class="ui radio checkbox">
+						{{end}}
 							<input class="hidden enable-system-radio" tabindex="0" name="enable_external_wiki" type="radio" value="false" data-target="#external_wiki_box" {{if not (.Repository.UnitEnabled $.UnitTypeExternalWiki)}}checked{{end}}/>
 							<label>{{.i18n.Tr "repo.settings.use_internal_wiki"}}</label>
 						</div>
 					</div>
 					<div class="field">
+						{{if .UnitTypeExternalWiki.UnitGlobalDisabled}}
+						<div class="ui radio checkbox poping up disabled" data-content="{{.i18n.Tr "repo.unit_disabled"}}">
+						{{else}}
 						<div class="ui radio checkbox">
+						{{end}}
 							<input class="hidden enable-system-radio" tabindex="0" name="enable_external_wiki" type="radio" value="true" data-target="#external_wiki_box" {{if .Repository.UnitEnabled $.UnitTypeExternalWiki}}checked{{end}}/>
 							<label>{{.i18n.Tr "repo.settings.use_external_wiki"}}</label>
 						</div>
@@ -174,14 +186,22 @@
 				{{$isIssuesEnabled := or (.Repository.UnitEnabled $.UnitTypeIssues) (.Repository.UnitEnabled $.UnitTypeExternalTracker)}}
 				<div class="inline field">
 					<label>{{.i18n.Tr "repo.issues"}}</label>
+					{{if and (.UnitTypeIssues.UnitGlobalDisabled) (.UnitTypeExternalTracker.UnitGlobalDisabled)}}
+					<div class="ui checkbox poping up disabled" data-content="{{.i18n.Tr "repo.unit_disabled"}}">
+					{{else}}
 					<div class="ui checkbox">
+					{{end}}
 						<input class="enable-system" name="enable_issues" type="checkbox" data-target="#issue_box" {{if $isIssuesEnabled}}checked{{end}}>
 						<label>{{.i18n.Tr "repo.settings.issues_desc"}}</label>
 					</div>
 				</div>
 				<div class="field {{if not $isIssuesEnabled}}disabled{{end}}" id="issue_box">
 					<div class="field">
+						{{if .UnitTypeIssues.UnitGlobalDisabled}}
+						<div class="ui radio checkbox poping up disabled" data-content="{{.i18n.Tr "repo.unit_disabled"}}">
+						{{else}}
 						<div class="ui radio checkbox">
+						{{end}}
 							<input class="hidden enable-system-radio" tabindex="0" name="enable_external_tracker" type="radio" value="false" data-context="#internal_issue_box" data-target="#external_issue_box" {{if not (.Repository.UnitEnabled $.UnitTypeExternalTracker)}}checked{{end}}/>
 							<label>{{.i18n.Tr "repo.settings.use_internal_issue_tracker"}}</label>
 						</div>
@@ -209,7 +229,11 @@
 							</div>
 					</div>
 					<div class="field">
+						{{if .UnitTypeExternalTracker.UnitGlobalDisabled}}
+						<div class="ui radio checkbox poping up disabled" data-content="{{.i18n.Tr "repo.unit_disabled"}}">
+						{{else}}
 						<div class="ui radio checkbox">
+						{{end}}
 							<input class="hidden enable-system-radio" tabindex="0" name="enable_external_tracker" type="radio" value="true" data-context="#internal_issue_box" data-target="#external_issue_box" {{if .Repository.UnitEnabled $.UnitTypeExternalTracker}}checked{{end}}/>
 							<label>{{.i18n.Tr "repo.settings.use_external_issue_tracker"}}</label>
 						</div>
@@ -251,7 +275,11 @@
 					{{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}}
 					<div class="inline field">
 						<label>{{.i18n.Tr "repo.pulls"}}</label>
+						{{if .UnitTypePullRequests.UnitGlobalDisabled}}
+						<div class="ui checkbox poping up disabled" data-content="{{.i18n.Tr "repo.unit_disabled"}}">
+						{{else}}
 						<div class="ui checkbox">
+						{{end}}
 							<input class="enable-system" name="enable_pulls" type="checkbox" data-target="#pull_box" {{if $pullRequestEnabled}}checked{{end}}>
 							<label>{{.i18n.Tr "repo.settings.pulls_desc"}}</label>
 						</div>