Repository: go-gitea/gitea
Pull request #33257: Add anonymous access support for private repositories (backend)
Author: login: wxiaoguang
Labels: name: lgtm/done
description: This PR has enough approvals to get merged. There are no important open reservations anymore.
name: skip-changelog
description: This PR is irrelevant for the (next) changelog, for example bug fixes for unreleased features.
Created: 2025-01-14T05:21:13Z | Merged: 2025-03-28T14:42:30Z

Follow #33127

For https://github.com/go-gitea/gitea/issues/8649 and https://github.com/go-gitea/gitea/issues/639

This PR add backend logic and test for "anonymous access", it shares the same logic as "everyone access", so not too much change.

By the way, split `SettingsPost` into small functions to make it easier to make frontend-related changes in the future.

Next PR will add frontend support for "anonymous access"

https://github.com/go-gitea/gitea/pull/33257/files?diff=unified&w=1


Diff:
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 572738013f802..6e631f98c7640 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -378,6 +378,7 @@ func prepareMigrationTasks() []*migration {
 		newMigration(315, "Add Ephemeral to ActionRunner", v1_24.AddEphemeralToActionRunner),
 		newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables),
 		newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
+		newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
 	}
 	return preparedMigrations
 }
diff --git a/models/migrations/v1_24/v318.go b/models/migrations/v1_24/v318.go
new file mode 100644
index 0000000000000..83fb0061d3dc6
--- /dev/null
+++ b/models/migrations/v1_24/v318.go
@@ -0,0 +1,17 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_24 //nolint
+
+import (
+	"code.gitea.io/gitea/models/perm"
+
+	"xorm.io/xorm"
+)
+
+func AddRepoUnitAnonymousAccessMode(x *xorm.Engine) error {
+	type RepoUnit struct { //revive:disable-line:exported
+		AnonymousAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
+	}
+	return x.Sync(&RepoUnit{})
+}
diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go
index 5e7ecb31eabbd..20ede7fee2af5 100644
--- a/models/perm/access/repo_permission.go
+++ b/models/perm/access/repo_permission.go
@@ -25,7 +25,8 @@ type Permission struct {
 	units     []*repo_model.RepoUnit
 	unitsMode map[unit.Type]perm_model.AccessMode
 
-	everyoneAccessMode map[unit.Type]perm_model.AccessMode
+	everyoneAccessMode  map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for every signed-in user
+	anonymousAccessMode map[unit.Type]perm_model.AccessMode // the unit's minimal access mode for anonymous (non-signed-in) user
 }
 
 // IsOwner returns true if current user is the owner of repository.
@@ -39,7 +40,7 @@ func (p *Permission) IsAdmin() bool {
 }
 
 // HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
-// It doesn't count the "everyone access mode".
+// It doesn't count the "public(anonymous/everyone) access mode".
 func (p *Permission) HasAnyUnitAccess() bool {
 	for _, v := range p.unitsMode {
 		if v >= perm_model.AccessModeRead {
@@ -49,7 +50,12 @@ func (p *Permission) HasAnyUnitAccess() bool {
 	return p.AccessMode >= perm_model.AccessModeRead
 }
 
-func (p *Permission) HasAnyUnitAccessOrEveryoneAccess() bool {
+func (p *Permission) HasAnyUnitAccessOrPublicAccess() bool {
+	for _, v := range p.anonymousAccessMode {
+		if v >= perm_model.AccessModeRead {
+			return true
+		}
+	}
 	for _, v := range p.everyoneAccessMode {
 		if v >= perm_model.AccessModeRead {
 			return true
@@ -73,14 +79,16 @@ func (p *Permission) GetFirstUnitRepoID() int64 {
 }
 
 // UnitAccessMode returns current user access mode to the specify unit of the repository
-// It also considers "everyone access mode"
+// It also considers "public (anonymous/everyone) access mode"
 func (p *Permission) UnitAccessMode(unitType unit.Type) perm_model.AccessMode {
 	// if the units map contains the access mode, use it, but admin/owner mode could override it
 	if m, ok := p.unitsMode[unitType]; ok {
 		return util.Iif(p.AccessMode >= perm_model.AccessModeAdmin, p.AccessMode, m)
 	}
 	// if the units map does not contain the access mode, return the default access mode if the unit exists
-	unitDefaultAccessMode := max(p.AccessMode, p.everyoneAccessMode[unitType])
+	unitDefaultAccessMode := p.AccessMode
+	unitDefaultAccessMode = max(unitDefaultAccessMode, p.anonymousAccessMode[unitType])
+	unitDefaultAccessMode = max(unitDefaultAccessMode, p.everyoneAccessMode[unitType])
 	hasUnit := slices.ContainsFunc(p.units, func(u *repo_model.RepoUnit) bool { return u.Type == unitType })
 	return util.Iif(hasUnit, unitDefaultAccessMode, perm_model.AccessModeNone)
 }
@@ -171,27 +179,38 @@ func (p *Permission) LogString() string {
 		format += "\n\tunitsMode[%-v]: %-v"
 		args = append(args, key.LogString(), value.LogString())
 	}
+	format += "\n\tanonymousAccessMode: %-v"
+	args = append(args, p.anonymousAccessMode)
 	format += "\n\teveryoneAccessMode: %-v"
 	args = append(args, p.everyoneAccessMode)
 	format += "\n\t]>"
 	return fmt.Sprintf(format, args...)
 }
 
+func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) {
+	if accessMode >= perm_model.AccessModeRead && accessMode > (*modeMap)[unitType] {
+		if *modeMap == nil {
+			*modeMap = make(map[unit.Type]perm_model.AccessMode)
+		}
+		(*modeMap)[unitType] = accessMode
+	}
+}
+
 func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
+	// apply public (anonymous) access permissions
+	for _, u := range perm.units {
+		applyPublicAccessPermission(u.Type, u.AnonymousAccessMode, &perm.anonymousAccessMode)
+	}
+
 	if user == nil || user.ID <= 0 {
 		// for anonymous access, it could be:
 		// AccessMode is None or Read, units has repo units, unitModes is nil
 		return
 	}
 
-	// apply everyone access permissions
+	// apply public (everyone) access permissions
 	for _, u := range perm.units {
-		if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] {
-			if perm.everyoneAccessMode == nil {
-				perm.everyoneAccessMode = make(map[unit.Type]perm_model.AccessMode)
-			}
-			perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode
-		}
+		applyPublicAccessPermission(u.Type, u.EveryoneAccessMode, &perm.everyoneAccessMode)
 	}
 
 	if perm.unitsMode == nil {
@@ -209,6 +228,11 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
 				break
 			}
 		}
+		for t := range perm.anonymousAccessMode {
+			if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
+				break
+			}
+		}
 		for t := range perm.everyoneAccessMode {
 			if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
 				break
diff --git a/models/perm/access/repo_permission_test.go b/models/perm/access/repo_permission_test.go
index 9862da06731f7..024f4400b3d66 100644
--- a/models/perm/access/repo_permission_test.go
+++ b/models/perm/access/repo_permission_test.go
@@ -22,14 +22,21 @@ func TestHasAnyUnitAccess(t *testing.T) {
 		units: []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
 	}
 	assert.False(t, perm.HasAnyUnitAccess())
-	assert.False(t, perm.HasAnyUnitAccessOrEveryoneAccess())
+	assert.False(t, perm.HasAnyUnitAccessOrPublicAccess())
 
 	perm = Permission{
 		units:              []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
 		everyoneAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead},
 	}
 	assert.False(t, perm.HasAnyUnitAccess())
-	assert.True(t, perm.HasAnyUnitAccessOrEveryoneAccess())
+	assert.True(t, perm.HasAnyUnitAccessOrPublicAccess())
+
+	perm = Permission{
+		units:               []*repo_model.RepoUnit{{Type: unit.TypeWiki}},
+		anonymousAccessMode: map[unit.Type]perm_model.AccessMode{unit.TypeIssues: perm_model.AccessModeRead},
+	}
+	assert.False(t, perm.HasAnyUnitAccess())
+	assert.True(t, perm.HasAnyUnitAccessOrPublicAccess())
 
 	perm = Permission{
 		AccessMode: perm_model.AccessModeRead,
@@ -43,7 +50,7 @@ func TestHasAnyUnitAccess(t *testing.T) {
 	assert.True(t, perm.HasAnyUnitAccess())
 }
 
-func TestApplyEveryoneRepoPermission(t *testing.T) {
+func TestApplyPublicAccessRepoPermission(t *testing.T) {
 	perm := Permission{
 		AccessMode: perm_model.AccessModeNone,
 		units: []*repo_model.RepoUnit{
@@ -53,6 +60,15 @@ func TestApplyEveryoneRepoPermission(t *testing.T) {
 	finalProcessRepoUnitPermission(nil, &perm)
 	assert.False(t, perm.CanRead(unit.TypeWiki))
 
+	perm = Permission{
+		AccessMode: perm_model.AccessModeNone,
+		units: []*repo_model.RepoUnit{
+			{Type: unit.TypeWiki, AnonymousAccessMode: perm_model.AccessModeRead},
+		},
+	}
+	finalProcessRepoUnitPermission(nil, &perm)
+	assert.True(t, perm.CanRead(unit.TypeWiki))
+
 	perm = Permission{
 		AccessMode: perm_model.AccessModeNone,
 		units: []*repo_model.RepoUnit{
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index cb52c2c9e2058..93b0dcab3111f 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -42,12 +42,13 @@ func (err ErrUnitTypeNotExist) Unwrap() error {
 
 // RepoUnit describes all units of a repository
 type RepoUnit struct { //revive:disable-line:exported
-	ID                 int64
-	RepoID             int64              `xorm:"INDEX(s)"`
-	Type               unit.Type          `xorm:"INDEX(s)"`
-	Config             convert.Conversion `xorm:"TEXT"`
-	CreatedUnix        timeutil.TimeStamp `xorm:"INDEX CREATED"`
-	EveryoneAccessMode perm.AccessMode    `xorm:"NOT NULL DEFAULT 0"`
+	ID                  int64
+	RepoID              int64              `xorm:"INDEX(s)"`
+	Type                unit.Type          `xorm:"INDEX(s)"`
+	Config              convert.Conversion `xorm:"TEXT"`
+	CreatedUnix         timeutil.TimeStamp `xorm:"INDEX CREATED"`
+	AnonymousAccessMode perm.AccessMode    `xorm:"NOT NULL DEFAULT 0"`
+	EveryoneAccessMode  perm.AccessMode    `xorm:"NOT NULL DEFAULT 0"`
 }
 
 func init() {
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index ac7eb768fa232..e8da443b67fa9 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -105,8 +105,6 @@ func Settings(ctx *context.Context) {
 
 // SettingsPost response for changes of a repository
 func SettingsPost(ctx *context.Context) {
-	form := web.GetForm(ctx).(*forms.RepoSettingForm)
-
 	ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
 	ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
 	ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
@@ -119,867 +117,965 @@ func SettingsPost(ctx *context.Context) {
 	ctx.Data["SigningSettings"] = setting.Repository.Signing
 	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 
-	repo := ctx.Repo.Repository
-
 	switch ctx.FormString("action") {
 	case "update":
-		if ctx.HasError() {
-			ctx.HTML(http.StatusOK, tplSettingsOptions)
-			return
-		}
+		handleSettingsPostUpdate(ctx)
+	case "mirror":
+		handleSettingsPostMirror(ctx)
+	case "mirror-sync":
+		handleSettingsPostMirrorSync(ctx)
+	case "push-mirror-sync":
+		handleSettingsPostPushMirrorSync(ctx)
+	case "push-mirror-update":
+		handleSettingsPostPushMirrorUpdate(ctx)
+	case "push-mirror-remove":
+		handleSettingsPostPushMirrorRemove(ctx)
+	case "push-mirror-add":
+		handleSettingsPostPushMirrorAdd(ctx)
+	case "advanced":
+		handleSettingsPostAdvanced(ctx)
+	case "signing":
+		handleSettingsPostSigning(ctx)
+	case "admin":
+		handleSettingsPostAdmin(ctx)
+	case "admin_index":
+		handleSettingsPostAdminIndex(ctx)
+	case "convert":
+		handleSettingsPostConvert(ctx)
+	case "convert_fork":
+		handleSettingsPostConvertFork(ctx)
+	case "transfer":
+		handleSettingsPostTransfer(ctx)
+	case "cancel_transfer":
+		handleSettingsPostCancelTransfer(ctx)
+	case "delete":
+		handleSettingsPostDelete(ctx)
+	case "delete-wiki":
+		handleSettingsPostDeleteWiki(ctx)
+	case "archive":
+		handleSettingsPostArchive(ctx)
+	case "unarchive":
+		handleSettingsPostUnarchive(ctx)
+	case "visibility":
+		handleSettingsPostVisibility(ctx)
+	default:
+		ctx.NotFound(nil)
+	}
+}
 
-		newRepoName := form.RepoName
-		// Check if repository name has been changed.
-		if repo.LowerName != strings.ToLower(newRepoName) {
-			// Close the GitRepo if open
-			if ctx.Repo.GitRepo != nil {
-				ctx.Repo.GitRepo.Close()
-				ctx.Repo.GitRepo = nil
-			}
-			if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil {
+func handleSettingsPostUpdate(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
+	if ctx.HasError() {
+		ctx.HTML(http.StatusOK, tplSettingsOptions)
+		return
+	}
+
+	newRepoName := form.RepoName
+	// Check if repository name has been changed.
+	if repo.LowerName != strings.ToLower(newRepoName) {
+		// Close the GitRepo if open
+		if ctx.Repo.GitRepo != nil {
+			ctx.Repo.GitRepo.Close()
+			ctx.Repo.GitRepo = nil
+		}
+		if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil {
+			ctx.Data["Err_RepoName"] = true
+			switch {
+			case repo_model.IsErrRepoAlreadyExist(err):
+				ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
+			case db.IsErrNameReserved(err):
+				ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
+			case repo_model.IsErrRepoFilesAlreadyExist(err):
 				ctx.Data["Err_RepoName"] = true
 				switch {
-				case repo_model.IsErrRepoAlreadyExist(err):
-					ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
-				case db.IsErrNameReserved(err):
-					ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
-				case repo_model.IsErrRepoFilesAlreadyExist(err):
-					ctx.Data["Err_RepoName"] = true
-					switch {
-					case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
-						ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form)
-					case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
-						ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
-					case setting.Repository.AllowDeleteOfUnadoptedRepositories:
-						ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
-					default:
-						ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
-					}
-				case db.IsErrNamePatternNotAllowed(err):
-					ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
+				case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
+					ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form)
+				case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
+					ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
+				case setting.Repository.AllowDeleteOfUnadoptedRepositories:
+					ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
 				default:
-					ctx.ServerError("ChangeRepositoryName", err)
+					ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
 				}
-				return
+			case db.IsErrNamePatternNotAllowed(err):
+				ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
+			default:
+				ctx.ServerError("ChangeRepositoryName", err)
 			}
-
-			log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
-		}
-		// In case it's just a case change.
-		repo.Name = newRepoName
-		repo.LowerName = strings.ToLower(newRepoName)
-		repo.Description = form.Description
-		repo.Website = form.Website
-		repo.IsTemplate = form.Template
-
-		// Visibility of forked repository is forced sync with base repository.
-		if repo.IsFork {
-			form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
-		}
-
-		if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
-			ctx.ServerError("UpdateRepository", err)
 			return
 		}
-		log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
 
-		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-		ctx.Redirect(repo.Link() + "/settings")
+		log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
+	}
+	// In case it's just a case change.
+	repo.Name = newRepoName
+	repo.LowerName = strings.ToLower(newRepoName)
+	repo.Description = form.Description
+	repo.Website = form.Website
+	repo.IsTemplate = form.Template
+
+	// Visibility of forked repository is forced sync with base repository.
+	if repo.IsFork {
+		form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
+	}
 
-	case "mirror":
-		if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
-			ctx.NotFound(nil)
-			return
-		}
+	if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
+		ctx.ServerError("UpdateRepository", err)
+		return
+	}
+	log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
 
-		pullMirror, err := repo_model.GetMirrorByRepoID(ctx, ctx.Repo.Repository.ID)
-		if err == repo_model.ErrMirrorNotExist {
-			ctx.NotFound(nil)
-			return
-		}
-		if err != nil {
-			ctx.ServerError("GetMirrorByRepoID", err)
-			return
-		}
-		// 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
-
-		interval, err := time.ParseDuration(form.Interval)
-		if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
-			ctx.Data["Err_Interval"] = true
-			ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
-			return
-		}
+	ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+	ctx.Redirect(repo.Link() + "/settings")
+}
 
-		pullMirror.EnablePrune = form.EnablePrune
-		pullMirror.Interval = interval
-		pullMirror.ScheduleNextUpdate()
-		if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
-			ctx.ServerError("UpdateMirror", err)
-			return
-		}
+func handleSettingsPostMirror(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
+	if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
+		ctx.NotFound(nil)
+		return
+	}
 
-		u, err := git.GetRemoteURL(ctx, ctx.Repo.Repository.RepoPath(), pullMirror.GetRemoteName())
-		if err != nil {
-			ctx.Data["Err_MirrorAddress"] = true
-			handleSettingRemoteAddrError(ctx, err, form)
-			return
-		}
-		if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() {
-			form.MirrorPassword, _ = u.User.Password()
-		}
+	pullMirror, err := repo_model.GetMirrorByRepoID(ctx, ctx.Repo.Repository.ID)
+	if err == repo_model.ErrMirrorNotExist {
+		ctx.NotFound(nil)
+		return
+	}
+	if err != nil {
+		ctx.ServerError("GetMirrorByRepoID", err)
+		return
+	}
+	// 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
+
+	interval, err := time.ParseDuration(form.Interval)
+	if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
+		ctx.Data["Err_Interval"] = true
+		ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
+		return
+	}
 
-		address, err := git.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
-		if err == nil {
-			err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
-		}
-		if err != nil {
-			ctx.Data["Err_MirrorAddress"] = true
-			handleSettingRemoteAddrError(ctx, err, form)
-			return
-		}
+	pullMirror.EnablePrune = form.EnablePrune
+	pullMirror.Interval = interval
+	pullMirror.ScheduleNextUpdate()
+	if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
+		ctx.ServerError("UpdateMirror", err)
+		return
+	}
 
-		if err := mirror_service.UpdateAddress(ctx, pullMirror, address); err != nil {
-			ctx.ServerError("UpdateAddress", err)
-			return
-		}
+	u, err := git.GetRemoteURL(ctx, ctx.Repo.Repository.RepoPath(), pullMirror.GetRemoteName())
+	if err != nil {
+		ctx.Data["Err_MirrorAddress"] = true
+		handleSettingRemoteAddrError(ctx, err, form)
+		return
+	}
+	if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() {
+		form.MirrorPassword, _ = u.User.Password()
+	}
 
-		remoteAddress, err := util.SanitizeURL(form.MirrorAddress)
-		if err != nil {
-			ctx.Data["Err_MirrorAddress"] = true
-			handleSettingRemoteAddrError(ctx, err, form)
-			return
-		}
-		pullMirror.RemoteAddress = remoteAddress
+	address, err := git.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
+	if err == nil {
+		err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
+	}
+	if err != nil {
+		ctx.Data["Err_MirrorAddress"] = true
+		handleSettingRemoteAddrError(ctx, err, form)
+		return
+	}
 
-		form.LFS = form.LFS && setting.LFS.StartServer
+	if err := mirror_service.UpdateAddress(ctx, pullMirror, address); err != nil {
+		ctx.ServerError("UpdateAddress", err)
+		return
+	}
 
-		if len(form.LFSEndpoint) > 0 {
-			ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
-			if ep == nil {
-				ctx.Data["Err_LFSEndpoint"] = true
-				ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
-				return
-			}
-			err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
-			if err != nil {
-				ctx.Data["Err_LFSEndpoint"] = true
-				handleSettingRemoteAddrError(ctx, err, form)
-				return
-			}
-		}
+	remoteAddress, err := util.SanitizeURL(form.MirrorAddress)
+	if err != nil {
+		ctx.Data["Err_MirrorAddress"] = true
+		handleSettingRemoteAddrError(ctx, err, form)
+		return
+	}
+	pullMirror.RemoteAddress = remoteAddress
+
+	form.LFS = form.LFS && setting.LFS.StartServer
 
-		pullMirror.LFS = form.LFS
-		pullMirror.LFSEndpoint = form.LFSEndpoint
-		if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
-			ctx.ServerError("UpdateMirror", err)
+	if len(form.LFSEndpoint) > 0 {
+		ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
+		if ep == nil {
+			ctx.Data["Err_LFSEndpoint"] = true
+			ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
 			return
 		}
-
-		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-		ctx.Redirect(repo.Link() + "/settings")
-
-	case "mirror-sync":
-		if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
-			ctx.NotFound(nil)
+		err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
+		if err != nil {
+			ctx.Data["Err_LFSEndpoint"] = true
+			handleSettingRemoteAddrError(ctx, err, form)
 			return
 		}
+	}
 
-		mirror_service.AddPullMirrorToQueue(repo.ID)
+	pullMirror.LFS = form.LFS
+	pullMirror.LFSEndpoint = form.LFSEndpoint
+	if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
+		ctx.ServerError("UpdateMirror", err)
+		return
+	}
 
-		ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
-		ctx.Redirect(repo.Link() + "/settings")
+	ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+	ctx.Redirect(repo.Link() + "/settings")
+}
 
-	case "push-mirror-sync":
-		if !setting.Mirror.Enabled {
-			ctx.NotFound(nil)
-			return
-		}
+func handleSettingsPostMirrorSync(ctx *context.Context) {
+	repo := ctx.Repo.Repository
+	if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
+		ctx.NotFound(nil)
+		return
+	}
 
-		m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
-		if m == nil {
-			ctx.NotFound(nil)
-			return
-		}
+	mirror_service.AddPullMirrorToQueue(repo.ID)
 
-		mirror_service.AddPushMirrorToQueue(m.ID)
+	ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
+	ctx.Redirect(repo.Link() + "/settings")
+}
 
-		ctx.Flash.Info(ctx.Tr("repo.settings.push_mirror_sync_in_progress", m.RemoteAddress))
-		ctx.Redirect(repo.Link() + "/settings")
+func handleSettingsPostPushMirrorSync(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
 
-	case "push-mirror-update":
-		if !setting.Mirror.Enabled || repo.IsArchived {
-			ctx.NotFound(nil)
-			return
-		}
+	if !setting.Mirror.Enabled {
+		ctx.NotFound(nil)
+		return
+	}
+
+	m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
+	if m == nil {
+		ctx.NotFound(nil)
+		return
+	}
 
-		// 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
+	mirror_service.AddPushMirrorToQueue(m.ID)
 
-		interval, err := time.ParseDuration(form.PushMirrorInterval)
-		if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
-			ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{})
-			return
-		}
+	ctx.Flash.Info(ctx.Tr("repo.settings.push_mirror_sync_in_progress", m.RemoteAddress))
+	ctx.Redirect(repo.Link() + "/settings")
+}
 
-		m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
-		if m == nil {
-			ctx.NotFound(nil)
-			return
-		}
+func handleSettingsPostPushMirrorUpdate(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
 
-		m.Interval = interval
-		if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil {
-			ctx.ServerError("UpdatePushMirrorInterval", err)
-			return
-		}
-		// Background why we are adding it to Queue
-		// If we observed its implementation in the context of `push-mirror-sync` where it
-		// is evident that pushing to the queue is necessary for updates.
-		// So, there are updates within the given interval, it is necessary to update the queue accordingly.
-		if !ctx.FormBool("push_mirror_defer_sync") {
-			// push_mirror_defer_sync is mainly for testing purpose, we do not really want to sync the push mirror immediately
-			mirror_service.AddPushMirrorToQueue(m.ID)
-		}
-		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-		ctx.Redirect(repo.Link() + "/settings")
+	if !setting.Mirror.Enabled || repo.IsArchived {
+		ctx.NotFound(nil)
+		return
+	}
 
-	case "push-mirror-remove":
-		if !setting.Mirror.Enabled || repo.IsArchived {
-			ctx.NotFound(nil)
-			return
-		}
+	// 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
 
-		// 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
+	interval, err := time.ParseDuration(form.PushMirrorInterval)
+	if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
+		ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{})
+		return
+	}
 
-		m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
-		if m == nil {
-			ctx.NotFound(nil)
-			return
-		}
+	m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
+	if m == nil {
+		ctx.NotFound(nil)
+		return
+	}
 
-		if err := mirror_service.RemovePushMirrorRemote(ctx, m); err != nil {
-			ctx.ServerError("RemovePushMirrorRemote", err)
-			return
-		}
+	m.Interval = interval
+	if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil {
+		ctx.ServerError("UpdatePushMirrorInterval", err)
+		return
+	}
+	// Background why we are adding it to Queue
+	// If we observed its implementation in the context of `push-mirror-sync` where it
+	// is evident that pushing to the queue is necessary for updates.
+	// So, there are updates within the given interval, it is necessary to update the queue accordingly.
+	if !ctx.FormBool("push_mirror_defer_sync") {
+		// push_mirror_defer_sync is mainly for testing purpose, we do not really want to sync the push mirror immediately
+		mirror_service.AddPushMirrorToQueue(m.ID)
+	}
+	ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+	ctx.Redirect(repo.Link() + "/settings")
+}
 
-		if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
-			ctx.ServerError("DeletePushMirrorByID", err)
-			return
-		}
+func handleSettingsPostPushMirrorRemove(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
 
-		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-		ctx.Redirect(repo.Link() + "/settings")
+	if !setting.Mirror.Enabled || repo.IsArchived {
+		ctx.NotFound(nil)
+		return
+	}
 
-	case "push-mirror-add":
-		if setting.Mirror.DisableNewPush || repo.IsArchived {
-			ctx.NotFound(nil)
-			return
-		}
+	// 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
 
-		// 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
+	m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
+	if m == nil {
+		ctx.NotFound(nil)
+		return
+	}
 
-		interval, err := time.ParseDuration(form.PushMirrorInterval)
-		if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
-			ctx.Data["Err_PushMirrorInterval"] = true
-			ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
-			return
-		}
+	if err := mirror_service.RemovePushMirrorRemote(ctx, m); err != nil {
+		ctx.ServerError("RemovePushMirrorRemote", err)
+		return
+	}
 
-		address, err := git.ParseRemoteAddr(form.PushMirrorAddress, form.PushMirrorUsername, form.PushMirrorPassword)
-		if err == nil {
-			err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
-		}
-		if err != nil {
-			ctx.Data["Err_PushMirrorAddress"] = true
-			handleSettingRemoteAddrError(ctx, err, form)
-			return
-		}
+	if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
+		ctx.ServerError("DeletePushMirrorByID", err)
+		return
+	}
 
-		remoteSuffix, err := util.CryptoRandomString(10)
-		if err != nil {
-			ctx.ServerError("RandomString", err)
-			return
-		}
+	ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+	ctx.Redirect(repo.Link() + "/settings")
+}
 
-		remoteAddress, err := util.SanitizeURL(form.PushMirrorAddress)
-		if err != nil {
-			ctx.Data["Err_PushMirrorAddress"] = true
-			handleSettingRemoteAddrError(ctx, err, form)
-			return
-		}
+func handleSettingsPostPushMirrorAdd(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
 
-		m := &repo_model.PushMirror{
-			RepoID:        repo.ID,
-			Repo:          repo,
-			RemoteName:    fmt.Sprintf("remote_mirror_%s", remoteSuffix),
-			SyncOnCommit:  form.PushMirrorSyncOnCommit,
-			Interval:      interval,
-			RemoteAddress: remoteAddress,
-		}
-		if err := db.Insert(ctx, m); err != nil {
-			ctx.ServerError("InsertPushMirror", err)
-			return
-		}
+	if setting.Mirror.DisableNewPush || repo.IsArchived {
+		ctx.NotFound(nil)
+		return
+	}
 
-		if err := mirror_service.AddPushMirrorRemote(ctx, m, address); err != nil {
-			if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
-				log.Error("DeletePushMirrors %v", err)
-			}
-			ctx.ServerError("AddPushMirrorRemote", err)
-			return
-		}
+	// 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
 
-		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-		ctx.Redirect(repo.Link() + "/settings")
+	interval, err := time.ParseDuration(form.PushMirrorInterval)
+	if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
+		ctx.Data["Err_PushMirrorInterval"] = true
+		ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
+		return
+	}
 
-	case "advanced":
-		var repoChanged bool
-		var units []repo_model.RepoUnit
-		var deleteUnitTypes []unit_model.Type
+	address, err := git.ParseRemoteAddr(form.PushMirrorAddress, form.PushMirrorUsername, form.PushMirrorPassword)
+	if err == nil {
+		err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
+	}
+	if err != nil {
+		ctx.Data["Err_PushMirrorAddress"] = true
+		handleSettingRemoteAddrError(ctx, err, form)
+		return
+	}
 
-		// 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
+	remoteSuffix, err := util.CryptoRandomString(10)
+	if err != nil {
+		ctx.ServerError("RandomString", err)
+		return
+	}
 
-		if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
-			repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
-			repoChanged = true
-		}
+	remoteAddress, err := util.SanitizeURL(form.PushMirrorAddress)
+	if err != nil {
+		ctx.Data["Err_PushMirrorAddress"] = true
+		handleSettingRemoteAddrError(ctx, err, form)
+		return
+	}
+
+	m := &repo_model.PushMirror{
+		RepoID:        repo.ID,
+		Repo:          repo,
+		RemoteName:    fmt.Sprintf("remote_mirror_%s", remoteSuffix),
+		SyncOnCommit:  form.PushMirrorSyncOnCommit,
+		Interval:      interval,
+		RemoteAddress: remoteAddress,
+	}
+	if err := db.Insert(ctx, m); err != nil {
+		ctx.ServerError("InsertPushMirror", err)
+		return
+	}
 
-		if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() {
-			units = append(units, repo_model.RepoUnit{
-				RepoID:             repo.ID,
-				Type:               unit_model.TypeCode,
-				EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultCodeEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
-			})
-		} else if !unit_model.TypeCode.UnitGlobalDisabled() {
-			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
+	if err := mirror_service.AddPushMirrorRemote(ctx, m, address); err != nil {
+		if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
+			log.Error("DeletePushMirrors %v", err)
 		}
+		ctx.ServerError("AddPushMirrorRemote", err)
+		return
+	}
 
-		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
-			}
+	ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+	ctx.Redirect(repo.Link() + "/settings")
+}
 
-			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() {
-			units = append(units, repo_model.RepoUnit{
-				RepoID:             repo.ID,
-				Type:               unit_model.TypeWiki,
-				Config:             new(repo_model.UnitConfig),
-				EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultWikiEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite),
-			})
-			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)
-			}
-		}
+func handleSettingsPostAdvanced(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
+	var repoChanged bool
+	var units []repo_model.RepoUnit
+	var deleteUnitTypes []unit_model.Type
 
-		if form.DefaultWikiBranch != "" {
-			if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil {
-				log.Error("ChangeDefaultWikiBranch failed, err: %v", err)
-				ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch"))
-			}
-		}
+	// 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 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,
-				},
-				EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultIssuesEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
-			})
-			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 repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
+		repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
+		repoChanged = true
+	}
 
-		if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
-			units = append(units, repo_model.RepoUnit{
-				RepoID: repo.ID,
-				Type:   unit_model.TypeProjects,
-				Config: &repo_model.ProjectsConfig{
-					ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
-				},
-			})
-		} else if !unit_model.TypeProjects.UnitGlobalDisabled() {
-			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
-		}
+	if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() {
+		units = append(units, repo_model.RepoUnit{
+			RepoID:             repo.ID,
+			Type:               unit_model.TypeCode,
+			EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultCodeEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
+		})
+	} else if !unit_model.TypeCode.UnitGlobalDisabled() {
+		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
+	}
 
-		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.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
 		}
 
-		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)
+		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() {
+		units = append(units, repo_model.RepoUnit{
+			RepoID:             repo.ID,
+			Type:               unit_model.TypeWiki,
+			Config:             new(repo_model.UnitConfig),
+			EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultWikiEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite),
+		})
+		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
+	} else {
+		if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
+			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
 		}
-
-		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 !unit_model.TypeWiki.UnitGlobalDisabled() {
+			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
 		}
+	}
 
-		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,
-					AllowFastForwardOnly:          form.PullsAllowFastForwardOnly,
-					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 form.DefaultWikiBranch != "" {
+		if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil {
+			log.Error("ChangeDefaultWikiBranch failed, err: %v", err)
+			ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch"))
 		}
+	}
 
-		if len(units) == 0 {
-			ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit"))
-			ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+	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 err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil {
-			ctx.ServerError("UpdateRepositoryUnits", err)
+		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
 		}
-		if repoChanged {
-			if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
-				ctx.ServerError("UpdateRepository", err)
-				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,
+			},
+			EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultIssuesEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
+		})
+		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)
 		}
-		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")
+	if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
+		units = append(units, repo_model.RepoUnit{
+			RepoID: repo.ID,
+			Type:   unit_model.TypeProjects,
+			Config: &repo_model.ProjectsConfig{
+				ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
+			},
+		})
+	} else if !unit_model.TypeProjects.UnitGlobalDisabled() {
+		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
+	}
 
-	case "signing":
-		changed := false
-		trustModel := repo_model.ToTrustModel(form.TrustModel)
-		if trustModel != repo.TrustModel {
-			repo.TrustModel = trustModel
-			changed = true
-		}
+	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 changed {
-			if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
-				ctx.ServerError("UpdateRepository", err)
-				return
-			}
-		}
-		log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+	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)
+	}
 
-		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+	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)
+	}
 
-	case "admin":
-		if !ctx.Doer.IsAdmin {
-			ctx.HTTPError(http.StatusForbidden)
-			return
-		}
+	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,
+				AllowFastForwardOnly:          form.PullsAllowFastForwardOnly,
+				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 repo.IsFsckEnabled != form.EnableHealthCheck {
-			repo.IsFsckEnabled = form.EnableHealthCheck
-		}
+	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_service.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)
 
-		log.Trace("Repository admin 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")
+}
 
-		ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
-		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+func handleSettingsPostSigning(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
+	changed := false
+	trustModel := repo_model.ToTrustModel(form.TrustModel)
+	if trustModel != repo.TrustModel {
+		repo.TrustModel = trustModel
+		changed = true
+	}
 
-	case "admin_index":
-		if !ctx.Doer.IsAdmin {
-			ctx.HTTPError(http.StatusForbidden)
+	if changed {
+		if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
+			ctx.ServerError("UpdateRepository", err)
 			return
 		}
+	}
+	log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
 
-		switch form.RequestReindexType {
-		case "stats":
-			if err := stats.UpdateRepoIndexer(ctx.Repo.Repository); err != nil {
-				ctx.ServerError("UpdateStatsRepondexer", err)
-				return
-			}
-		case "code":
-			if !setting.Indexer.RepoIndexerEnabled {
-				ctx.HTTPError(http.StatusForbidden)
-				return
-			}
-			code.UpdateRepoIndexer(ctx.Repo.Repository)
-		default:
-			ctx.NotFound(nil)
-			return
-		}
+	ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+	ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
 
-		log.Trace("Repository reindex for %s requested: %s/%s", form.RequestReindexType, ctx.Repo.Owner.Name, repo.Name)
+func handleSettingsPostAdmin(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
+	if !ctx.Doer.IsAdmin {
+		ctx.HTTPError(http.StatusForbidden)
+		return
+	}
 
-		ctx.Flash.Success(ctx.Tr("repo.settings.reindex_requested"))
-		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+	if repo.IsFsckEnabled != form.EnableHealthCheck {
+		repo.IsFsckEnabled = form.EnableHealthCheck
+	}
 
-	case "convert":
-		if !ctx.Repo.IsOwner() {
-			ctx.HTTPError(http.StatusNotFound)
-			return
-		}
-		if repo.Name != form.RepoName {
-			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
-			return
-		}
+	if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
+		ctx.ServerError("UpdateRepository", err)
+		return
+	}
 
-		if !repo.IsMirror {
-			ctx.HTTPError(http.StatusNotFound)
-			return
-		}
-		repo.IsMirror = false
+	log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
 
-		if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil {
-			ctx.ServerError("CleanUpMigrateInfo", err)
-			return
-		} else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil {
-			ctx.ServerError("DeleteMirrorByRepoID", err)
-			return
-		}
-		log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
-		ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
-		ctx.Redirect(repo.Link())
+	ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+	ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
 
-	case "convert_fork":
-		if !ctx.Repo.IsOwner() {
-			ctx.HTTPError(http.StatusNotFound)
-			return
-		}
-		if err := repo.LoadOwner(ctx); err != nil {
-			ctx.ServerError("Convert Fork", err)
+func handleSettingsPostAdminIndex(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
+	if !ctx.Doer.IsAdmin {
+		ctx.HTTPError(http.StatusForbidden)
+		return
+	}
+
+	switch form.RequestReindexType {
+	case "stats":
+		if err := stats.UpdateRepoIndexer(ctx.Repo.Repository); err != nil {
+			ctx.ServerError("UpdateStatsRepondexer", err)
 			return
 		}
-		if repo.Name != form.RepoName {
-			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+	case "code":
+		if !setting.Indexer.RepoIndexerEnabled {
+			ctx.HTTPError(http.StatusForbidden)
 			return
 		}
+		code.UpdateRepoIndexer(ctx.Repo.Repository)
+	default:
+		ctx.NotFound(nil)
+		return
+	}
 
-		if !repo.IsFork {
-			ctx.HTTPError(http.StatusNotFound)
-			return
-		}
+	log.Trace("Repository reindex for %s requested: %s/%s", form.RequestReindexType, ctx.Repo.Owner.Name, repo.Name)
 
-		if !ctx.Repo.Owner.CanCreateRepo() {
-			maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit()
-			msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
-			ctx.Flash.Error(msg)
-			ctx.Redirect(repo.Link() + "/settings")
-			return
-		}
+	ctx.Flash.Success(ctx.Tr("repo.settings.reindex_requested"))
+	ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
 
-		if err := repo_service.ConvertForkToNormalRepository(ctx, repo); err != nil {
-			log.Error("Unable to convert repository %-v from fork. Error: %v", repo, err)
-			ctx.ServerError("Convert Fork", err)
-			return
-		}
+func handleSettingsPostConvert(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
+	if !ctx.Repo.IsOwner() {
+		ctx.HTTPError(http.StatusNotFound)
+		return
+	}
+	if repo.Name != form.RepoName {
+		ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+		return
+	}
 
-		log.Trace("Repository converted from fork to regular: %s", repo.FullName())
-		ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
-		ctx.Redirect(repo.Link())
+	if !repo.IsMirror {
+		ctx.HTTPError(http.StatusNotFound)
+		return
+	}
+	repo.IsMirror = false
 
-	case "transfer":
-		if !ctx.Repo.IsOwner() {
-			ctx.HTTPError(http.StatusNotFound)
-			return
-		}
-		if repo.Name != form.RepoName {
-			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
-			return
-		}
+	if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil {
+		ctx.ServerError("CleanUpMigrateInfo", err)
+		return
+	} else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil {
+		ctx.ServerError("DeleteMirrorByRepoID", err)
+		return
+	}
+	log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
+	ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
+	ctx.Redirect(repo.Link())
+}
 
-		newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name"))
-		if err != nil {
-			if user_model.IsErrUserNotExist(err) {
-				ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
-				return
-			}
-			ctx.ServerError("IsUserExist", err)
-			return
-		}
+func handleSettingsPostConvertFork(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
+	if !ctx.Repo.IsOwner() {
+		ctx.HTTPError(http.StatusNotFound)
+		return
+	}
+	if err := repo.LoadOwner(ctx); err != nil {
+		ctx.ServerError("Convert Fork", err)
+		return
+	}
+	if repo.Name != form.RepoName {
+		ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+		return
+	}
 
-		if newOwner.Type == user_model.UserTypeOrganization {
-			if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) {
-				// The user shouldn't know about this organization
-				ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
-				return
-			}
-		}
+	if !repo.IsFork {
+		ctx.HTTPError(http.StatusNotFound)
+		return
+	}
 
-		// Close the GitRepo if open
-		if ctx.Repo.GitRepo != nil {
-			ctx.Repo.GitRepo.Close()
-			ctx.Repo.GitRepo = nil
-		}
+	if !ctx.Repo.Owner.CanCreateRepo() {
+		maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit()
+		msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+		ctx.Flash.Error(msg)
+		ctx.Redirect(repo.Link() + "/settings")
+		return
+	}
 
-		oldFullname := repo.FullName()
-		if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
-			if repo_model.IsErrRepoAlreadyExist(err) {
-				ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
-			} else if repo_model.IsErrRepoTransferInProgress(err) {
-				ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
-			} else if errors.Is(err, user_model.ErrBlockedUser) {
-				ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
-			} else {
-				ctx.ServerError("TransferOwnership", err)
-			}
+	if err := repo_service.ConvertForkToNormalRepository(ctx, repo); err != nil {
+		log.Error("Unable to convert repository %-v from fork. Error: %v", repo, err)
+		ctx.ServerError("Convert Fork", err)
+		return
+	}
 
-			return
-		}
+	log.Trace("Repository converted from fork to regular: %s", repo.FullName())
+	ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
+	ctx.Redirect(repo.Link())
+}
 
-		if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
-			log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
-			ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
-		} else {
-			log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
-			ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed"))
-		}
-		ctx.Redirect(repo.Link() + "/settings")
+func handleSettingsPostTransfer(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
+	if !ctx.Repo.IsOwner() {
+		ctx.HTTPError(http.StatusNotFound)
+		return
+	}
+	if repo.Name != form.RepoName {
+		ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+		return
+	}
 
-	case "cancel_transfer":
-		if !ctx.Repo.IsOwner() {
-			ctx.HTTPError(http.StatusNotFound)
+	newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name"))
+	if err != nil {
+		if user_model.IsErrUserNotExist(err) {
+			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
 			return
 		}
+		ctx.ServerError("IsUserExist", err)
+		return
+	}
 
-		repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
-		if err != nil {
-			if repo_model.IsErrNoPendingTransfer(err) {
-				ctx.Flash.Error("repo.settings.transfer_abort_invalid")
-				ctx.Redirect(repo.Link() + "/settings")
-			} else {
-				ctx.ServerError("GetPendingRepositoryTransfer", err)
-			}
+	if newOwner.Type == user_model.UserTypeOrganization {
+		if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) {
+			// The user shouldn't know about this organization
+			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
 			return
 		}
+	}
 
-		if err := repo_service.CancelRepositoryTransfer(ctx, repoTransfer, ctx.Doer); err != nil {
-			ctx.ServerError("CancelRepositoryTransfer", err)
-			return
+	// Close the GitRepo if open
+	if ctx.Repo.GitRepo != nil {
+		ctx.Repo.GitRepo.Close()
+		ctx.Repo.GitRepo = nil
+	}
+
+	oldFullname := repo.FullName()
+	if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
+		if repo_model.IsErrRepoAlreadyExist(err) {
+			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
+		} else if repo_model.IsErrRepoTransferInProgress(err) {
+			ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
+		} else if errors.Is(err, user_model.ErrBlockedUser) {
+			ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
+		} else {
+			ctx.ServerError("TransferOwnership", err)
 		}
 
-		log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
-		ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
-		ctx.Redirect(repo.Link() + "/settings")
+		return
+	}
 
-	case "delete":
-		if !ctx.Repo.IsOwner() {
-			ctx.HTTPError(http.StatusNotFound)
-			return
-		}
-		if repo.Name != form.RepoName {
-			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
-			return
-		}
+	if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
+		log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
+		ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
+	} else {
+		log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
+		ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed"))
+	}
+	ctx.Redirect(repo.Link() + "/settings")
+}
 
-		// Close the gitrepository before doing this.
-		if ctx.Repo.GitRepo != nil {
-			ctx.Repo.GitRepo.Close()
-		}
+func handleSettingsPostCancelTransfer(ctx *context.Context) {
+	repo := ctx.Repo.Repository
+	if !ctx.Repo.IsOwner() {
+		ctx.HTTPError(http.StatusNotFound)
+		return
+	}
 
-		if err := repo_service.DeleteRepository(ctx, ctx.Doer, ctx.Repo.Repository, true); err != nil {
-			ctx.ServerError("DeleteRepository", err)
-			return
+	repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
+	if err != nil {
+		if repo_model.IsErrNoPendingTransfer(err) {
+			ctx.Flash.Error("repo.settings.transfer_abort_invalid")
+			ctx.Redirect(repo.Link() + "/settings")
+		} else {
+			ctx.ServerError("GetPendingRepositoryTransfer", err)
 		}
-		log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+		return
+	}
 
-		ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
-		ctx.Redirect(ctx.Repo.Owner.DashboardLink())
+	if err := repo_service.CancelRepositoryTransfer(ctx, repoTransfer, ctx.Doer); err != nil {
+		ctx.ServerError("CancelRepositoryTransfer", err)
+		return
+	}
 
-	case "delete-wiki":
-		if !ctx.Repo.IsOwner() {
-			ctx.HTTPError(http.StatusNotFound)
-			return
-		}
-		if repo.Name != form.RepoName {
-			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
-			return
-		}
+	log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
+	ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
+	ctx.Redirect(repo.Link() + "/settings")
+}
 
-		err := wiki_service.DeleteWiki(ctx, repo)
-		if err != nil {
-			log.Error("Delete Wiki: %v", err.Error())
-		}
-		log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+func handleSettingsPostDelete(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
+	if !ctx.Repo.IsOwner() {
+		ctx.HTTPError(http.StatusNotFound)
+		return
+	}
+	if repo.Name != form.RepoName {
+		ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+		return
+	}
 
-		ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
-		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+	// Close the gitrepository before doing this.
+	if ctx.Repo.GitRepo != nil {
+		ctx.Repo.GitRepo.Close()
+	}
 
-	case "archive":
-		if !ctx.Repo.IsOwner() {
-			ctx.HTTPError(http.StatusForbidden)
-			return
-		}
+	if err := repo_service.DeleteRepository(ctx, ctx.Doer, ctx.Repo.Repository, true); err != nil {
+		ctx.ServerError("DeleteRepository", err)
+		return
+	}
+	log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
 
-		if repo.IsMirror {
-			ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
-			ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-			return
-		}
+	ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
+	ctx.Redirect(ctx.Repo.Owner.DashboardLink())
+}
 
-		if err := repo_model.SetArchiveRepoState(ctx, repo, true); err != nil {
-			log.Error("Tried to archive a repo: %s", err)
-			ctx.Flash.Error(ctx.Tr("repo.settings.archive.error"))
-			ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-			return
-		}
+func handleSettingsPostDeleteWiki(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
+	if !ctx.Repo.IsOwner() {
+		ctx.HTTPError(http.StatusNotFound)
+		return
+	}
+	if repo.Name != form.RepoName {
+		ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+		return
+	}
 
-		if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil {
-			log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
-		}
+	err := wiki_service.DeleteWiki(ctx, repo)
+	if err != nil {
+		log.Error("Delete Wiki: %v", err.Error())
+	}
+	log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+
+	ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
+	ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
 
-		// update issue indexer
-		issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
+func handleSettingsPostArchive(ctx *context.Context) {
+	repo := ctx.Repo.Repository
+	if !ctx.Repo.IsOwner() {
+		ctx.HTTPError(http.StatusForbidden)
+		return
+	}
 
-		ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
+	if repo.IsMirror {
+		ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
+		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+		return
+	}
 
-		log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+	if err := repo_model.SetArchiveRepoState(ctx, repo, true); err != nil {
+		log.Error("Tried to archive a repo: %s", err)
+		ctx.Flash.Error(ctx.Tr("repo.settings.archive.error"))
 		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+		return
+	}
 
-	case "unarchive":
-		if !ctx.Repo.IsOwner() {
-			ctx.HTTPError(http.StatusForbidden)
-			return
-		}
+	if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil {
+		log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+	}
 
-		if err := repo_model.SetArchiveRepoState(ctx, repo, false); err != nil {
-			log.Error("Tried to unarchive a repo: %s", err)
-			ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error"))
-			ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-			return
-		}
+	// update issue indexer
+	issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
 
-		if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
-			if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
-				log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
-			}
-		}
+	ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
 
-		// update issue indexer
-		issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
+	log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+	ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
 
-		ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
+func handleSettingsPostUnarchive(ctx *context.Context) {
+	repo := ctx.Repo.Repository
+	if !ctx.Repo.IsOwner() {
+		ctx.HTTPError(http.StatusForbidden)
+		return
+	}
 
-		log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+	if err := repo_model.SetArchiveRepoState(ctx, repo, false); err != nil {
+		log.Error("Tried to unarchive a repo: %s", err)
+		ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error"))
 		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+		return
+	}
 
-	case "visibility":
-		if repo.IsFork {
-			ctx.Flash.Error(ctx.Tr("repo.settings.visibility.fork_error"))
-			ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-			return
+	if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
+		if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
+			log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
 		}
+	}
 
-		var err error
+	// update issue indexer
+	issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
 
-		// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
-		if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin {
-			ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form)
-			return
-		}
+	ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
 
-		if repo.IsPrivate {
-			err = repo_service.MakeRepoPublic(ctx, repo)
-		} else {
-			err = repo_service.MakeRepoPrivate(ctx, repo)
-		}
+	log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+	ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
 
-		if err != nil {
-			log.Error("Tried to change the visibility of the repo: %s", err)
-			ctx.Flash.Error(ctx.Tr("repo.settings.visibility.error"))
-			ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-			return
-		}
+func handleSettingsPostVisibility(ctx *context.Context) {
+	form := web.GetForm(ctx).(*forms.RepoSettingForm)
+	repo := ctx.Repo.Repository
+	if repo.IsFork {
+		ctx.Flash.Error(ctx.Tr("repo.settings.visibility.fork_error"))
+		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+		return
+	}
 
-		ctx.Flash.Success(ctx.Tr("repo.settings.visibility.success"))
+	var err error
 
-		log.Trace("Repository visibility changed: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+	// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
+	if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin {
+		ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form)
+		return
+	}
 
-	default:
-		ctx.NotFound(nil)
+	if repo.IsPrivate {
+		err = repo_service.MakeRepoPublic(ctx, repo)
+	} else {
+		err = repo_service.MakeRepoPrivate(ctx, repo)
+	}
+
+	if err != nil {
+		log.Error("Tried to change the visibility of the repo: %s", err)
+		ctx.Flash.Error(ctx.Tr("repo.settings.visibility.error"))
+		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+		return
 	}
+
+	ctx.Flash.Success(ctx.Tr("repo.settings.visibility.success"))
+
+	log.Trace("Repository visibility changed: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+	ctx.Redirect(ctx.Repo.RepoLink + "/settings")
 }
 
 func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {
diff --git a/services/context/repo.go b/services/context/repo.go
index a083c557e661e..7d0b44c42f68c 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -346,7 +346,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
 		return
 	}
 
-	if !ctx.Repo.Permission.HasAnyUnitAccessOrEveryoneAccess() && !canWriteAsMaintainer(ctx) {
+	if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() && !canWriteAsMaintainer(ctx) {
 		if ctx.FormString("go-get") == "1" {
 			EarlyResponseForGoGetMeta(ctx)
 			return


Reviews:
user: login: lunny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T05:46:25Z
user: login: lunny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T05:48:13Z
user: login: lunny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T05:49:35Z
user: login: wxiaoguang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T05:50:44Z
user: login: lunny
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T05:51:09Z
user: login: wxiaoguang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T05:51:19Z
user: login: wxiaoguang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T05:51:43Z
user: login: wxiaoguang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T05:54:44Z
user: login: wxiaoguang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T05:55:53Z
user: login: lunny
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T07:15:11Z
user: login: hiifong
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T12:49:29Z
user: login: hiifong
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T13:08:40Z
user: login: wxiaoguang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T13:59:18Z
user: login: wxiaoguang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T13:59:55Z
user: login: hiifong
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T14:16:22Z

Review comments:
diff_hunk: @@ -25,7 +25,8 @@ type Permission struct {
 	units     []*repo_model.RepoUnit
 	unitsMode map[unit.Type]perm_model.AccessMode
 
-	everyoneAccessMode map[unit.Type]perm_model.AccessMode
+	everyoneAccessMode  map[unit.Type]perm_model.AccessMode
+	anonymousAccessMode map[unit.Type]perm_model.AccessMode
path: models/perm/access/repo_permission.go
user: login: lunny
body: Could we have some comments here about `everyoneAccessMode` and `anonymousAccessMode`?
created_at: 2025-03-28T05:46:25Z
author_association: MEMBER
diff_hunk: @@ -171,27 +179,38 @@ func (p *Permission) LogString() string {
 		format += "\n\tunitsMode[%-v]: %-v"
 		args = append(args, key.LogString(), value.LogString())
 	}
+	format += "\n\tanonymousAccessMode: %-v"
+	args = append(args, p.anonymousAccessMode)
 	format += "\n\teveryoneAccessMode: %-v"
 	args = append(args, p.everyoneAccessMode)
 	format += "\n\t]>"
 	return fmt.Sprintf(format, args...)
 }
 
+func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) {
+	if accessMode >= perm_model.AccessModeRead && accessMode > (*modeMap)[unitType] {
path: models/perm/access/repo_permission.go
user: login: lunny
body: If this is safe if `*modeMap` is nil?
created_at: 2025-03-28T05:48:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_24 //nolint
+
+import (
+	"code.gitea.io/gitea/models/perm"
+
+	"xorm.io/xorm"
+)
+
+func AddRepoUnitAnonymousAccessMode(x *xorm.Engine) error {
+	type RepoUnit struct { //revive:disable-line:exported
+		AnonymousAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
path: models/migrations/v1_24/v318.go
user: login: lunny
body: Should it be updated to read if the repository is public?
created_at: 2025-03-28T05:49:35Z
author_association: MEMBER
diff_hunk: @@ -171,27 +179,38 @@ func (p *Permission) LogString() string {
 		format += "\n\tunitsMode[%-v]: %-v"
 		args = append(args, key.LogString(), value.LogString())
 	}
+	format += "\n\tanonymousAccessMode: %-v"
+	args = append(args, p.anonymousAccessMode)
 	format += "\n\teveryoneAccessMode: %-v"
 	args = append(args, p.everyoneAccessMode)
 	format += "\n\t]>"
 	return fmt.Sprintf(format, args...)
 }
 
+func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) {
+	if accessMode >= perm_model.AccessModeRead && accessMode > (*modeMap)[unitType] {
path: models/perm/access/repo_permission.go
user: login: wxiaoguang
body: Yes, Golang spec
created_at: 2025-03-28T05:50:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,867 +117,965 @@ func SettingsPost(ctx *context.Context) {
 	ctx.Data["SigningSettings"] = setting.Repository.Signing
 	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 
-	repo := ctx.Repo.Repository
-
 	switch ctx.FormString("action") {
 	case "update":
-		if ctx.HasError() {
-			ctx.HTML(http.StatusOK, tplSettingsOptions)
-			return
-		}
+		handleSettingsPostUpdate(ctx)
path: routers/web/repo/setting/setting.go
user: login: lunny
body: Looks like it's better to move these changes to another PR.
created_at: 2025-03-28T05:51:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,17 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_24 //nolint
+
+import (
+	"code.gitea.io/gitea/models/perm"
+
+	"xorm.io/xorm"
+)
+
+func AddRepoUnitAnonymousAccessMode(x *xorm.Engine) error {
+	type RepoUnit struct { //revive:disable-line:exported
+		AnonymousAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"`
path: models/migrations/v1_24/v318.go
user: login: wxiaoguang
body: No, public repo is not affected.
created_at: 2025-03-28T05:51:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,867 +117,965 @@ func SettingsPost(ctx *context.Context) {
 	ctx.Data["SigningSettings"] = setting.Repository.Signing
 	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 
-	repo := ctx.Repo.Repository
-
 	switch ctx.FormString("action") {
 	case "update":
-		if ctx.HasError() {
-			ctx.HTML(http.StatusOK, tplSettingsOptions)
-			return
-		}
+		handleSettingsPostUpdate(ctx)
path: routers/web/repo/setting/setting.go
user: login: wxiaoguang
body: But I have done it together, just don't want to waste time on another PR.
created_at: 2025-03-28T05:51:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,7 +25,8 @@ type Permission struct {
 	units     []*repo_model.RepoUnit
 	unitsMode map[unit.Type]perm_model.AccessMode
 
-	everyoneAccessMode map[unit.Type]perm_model.AccessMode
+	everyoneAccessMode  map[unit.Type]perm_model.AccessMode
+	anonymousAccessMode map[unit.Type]perm_model.AccessMode
path: models/perm/access/repo_permission.go
user: login: wxiaoguang
body: Added in https://github.com/go-gitea/gitea/pull/33257/commits/819485e1c9c8b4b0feb88893f49af6769b98c5d3
created_at: 2025-03-28T05:54:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,867 +117,965 @@ func SettingsPost(ctx *context.Context) {
 	ctx.Data["SigningSettings"] = setting.Repository.Signing
 	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 
-	repo := ctx.Repo.Repository
-
 	switch ctx.FormString("action") {
 	case "update":
-		if ctx.HasError() {
-			ctx.HTML(http.StatusOK, tplSettingsOptions)
-			return
-		}
+		handleSettingsPostUpdate(ctx)
path: routers/web/repo/setting/setting.go
user: login: wxiaoguang
body: ps: this refactoring is related, to make it easier to make related frontend changes.

If we do not put it here, it will also be in "next PR", otherwise there will be one more PR, too many PRs.
created_at: 2025-03-28T05:55:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,27 +179,38 @@ func (p *Permission) LogString() string {
 		format += "\n\tunitsMode[%-v]: %-v"
 		args = append(args, key.LogString(), value.LogString())
 	}
+	format += "\n\tanonymousAccessMode: %-v"
path: models/perm/access/repo_permission.go
user: login: hiifong
body: Here we can use the string builder for better performance.
created_at: 2025-03-28T12:49:16Z
author_association: MEMBER
diff_hunk: @@ -209,6 +228,11 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
 				break
 			}
 		}
+		for t := range perm.anonymousAccessMode {
+			if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
path: models/perm/access/repo_permission.go
user: login: hiifong
body: Here's what I prefer to write: shouldKeep |= u.Type == t, But that's just my personal opinion, and there's nothing wrong with writing it that way
created_at: 2025-03-28T13:08:11Z
author_association: MEMBER
diff_hunk: @@ -171,27 +179,38 @@ func (p *Permission) LogString() string {
 		format += "\n\tunitsMode[%-v]: %-v"
 		args = append(args, key.LogString(), value.LogString())
 	}
+	format += "\n\tanonymousAccessMode: %-v"
path: models/perm/access/repo_permission.go
user: login: wxiaoguang
body: This function won't be called in production. So it doesn't affect performance.
created_at: 2025-03-28T13:59:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -209,6 +228,11 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
 				break
 			}
 		}
+		for t := range perm.anonymousAccessMode {
+			if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
path: models/perm/access/repo_permission.go
user: login: wxiaoguang
body: Then it needs 2 lines. Current code only uses one line .......
created_at: 2025-03-28T13:59:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -209,6 +228,11 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
 				break
 			}
 		}
+		for t := range perm.anonymousAccessMode {
+			if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
path: models/perm/access/repo_permission.go
user: login: hiifong
body: You're right.
created_at: 2025-03-28T14:16:16Z
author_association: MEMBER

PR comments:
user: login: wxiaoguang
created_at: 2025-03-21T12:35:05Z
body: It seems there is no interest or strong requirement for this feature. Close, until there are enough interests and requirements.
author_association: CONTRIBUTOR
user: login: moritz-h
created_at: 2025-03-26T22:20:11Z
body: @wxiaoguang I have interest in this feature. I understood #639 with over 100 upvotes is exactly about this, so I think there is overall strong interest in this. #33127 is only half of what #639 was about.
author_association: NONE
user: login: wxiaoguang
created_at: 2025-03-26T23:58:15Z
body: Then let's try to bring it into Gitea 1.24
author_association: CONTRIBUTOR
user: login: wxiaoguang
created_at: 2025-03-28T17:25:33Z
body: Next:  Add anonymous access support for private repositories #34051 
author_association: CONTRIBUTOR