// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package setting

import (
	"errors"
	"html/template"
	"net/http"
	"strings"
	"time"

	"gitea.dev/models/db"
	"gitea.dev/models/organization"
	repo_model "gitea.dev/models/repo"
	unit_model "gitea.dev/models/unit"
	user_model "gitea.dev/models/user"
	"gitea.dev/modules/git"
	"gitea.dev/modules/gitrepo"
	"gitea.dev/modules/indexer/code"
	issue_indexer "gitea.dev/modules/indexer/issues"
	"gitea.dev/modules/indexer/stats"
	"gitea.dev/modules/lfs"
	"gitea.dev/modules/log"
	"gitea.dev/modules/setting"
	"gitea.dev/modules/structs"
	"gitea.dev/modules/templates"
	"gitea.dev/modules/util"
	"gitea.dev/modules/validation"
	"gitea.dev/modules/web"
	repo_router "gitea.dev/routers/web/repo"
	actions_service "gitea.dev/services/actions"
	"gitea.dev/services/context"
	"gitea.dev/services/forms"
	"gitea.dev/services/migrations"
	mirror_service "gitea.dev/services/mirror"
	repo_service "gitea.dev/services/repository"
	wiki_service "gitea.dev/services/wiki"

	"xorm.io/xorm/convert"
)

const (
	tplSettingsOptions templates.TplName = "repo/settings/options"
	tplCollaboration   templates.TplName = "repo/settings/collaboration"
	tplBranches        templates.TplName = "repo/settings/branches"
	tplGithooks        templates.TplName = "repo/settings/githooks"
	tplGithookEdit     templates.TplName = "repo/settings/githook_edit"
	tplDeployKeys      templates.TplName = "repo/settings/deploy_keys"
)

type selectOption struct {
	Value    string
	Text     template.HTML
	Selected bool
}

// SettingsCtxData is a middleware that sets all the general context data for the
// settings template.
func SettingsCtxData(ctx *context.Context) {
	ctx.Data["Title"] = ctx.Tr("repo.settings.options")
	ctx.Data["PageIsSettingsOptions"] = true
	ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
	ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
	ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
	ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
	ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
	ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
	ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner)

	signing, _ := gitrepo.GetSigningKey(ctx)
	ctx.Data["SigningKeyAvailable"] = signing != nil
	ctx.Data["SigningSettings"] = setting.Repository.Signing
	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
	preparePullRequestSettings(ctx)

	if ctx.Doer.IsAdmin {
		if setting.Indexer.RepoIndexerEnabled {
			status, err := repo_model.GetIndexerStatus(ctx, ctx.Repo.Repository, repo_model.RepoIndexerTypeCode)
			if err != nil {
				ctx.ServerError("repo.indexer_status", err)
				return
			}
			ctx.Data["CodeIndexerStatus"] = status
		}
		status, err := repo_model.GetIndexerStatus(ctx, ctx.Repo.Repository, repo_model.RepoIndexerTypeStats)
		if err != nil {
			ctx.ServerError("repo.indexer_status", err)
			return
		}
		ctx.Data["StatsIndexerStatus"] = status
	}
	pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
	if err != nil {
		ctx.ServerError("GetPushMirrorsByRepoID", err)
		return
	}
	ctx.Data["PushMirrors"] = pushMirrors

	repo_router.PrepareBranchList(ctx)
	if ctx.Written() {
		return
	}
}

func preparePullRequestSettings(ctx *context.Context) {
	defaultUpdateStyle := repo_model.UpdateStyleMerge
	if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) {
		prUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit_model.TypePullRequests)
		defaultUpdateStyle = util.IfZero(prUnit.PullRequestsConfig().DefaultUpdateStyle, repo_model.UpdateStyleMerge)
	}

	updateBranchText := ctx.Tr("repo.pulls.update_branch")
	rebaseUpdateText := ctx.Tr("repo.pulls.update_branch_rebase")
	defaultUpdateStyleText := updateBranchText
	if defaultUpdateStyle == repo_model.UpdateStyleRebase {
		defaultUpdateStyleText = rebaseUpdateText
	}

	ctx.Data["PullsDefaultUpdateStyleText"] = defaultUpdateStyleText
	ctx.Data["PullsDefaultUpdateStyleOptions"] = []selectOption{
		{
			Value:    string(repo_model.UpdateStyleMerge),
			Text:     updateBranchText,
			Selected: defaultUpdateStyle == repo_model.UpdateStyleMerge,
		},
		{
			Value:    string(repo_model.UpdateStyleRebase),
			Text:     rebaseUpdateText,
			Selected: defaultUpdateStyle == repo_model.UpdateStyleRebase,
		},
	}
}

// Settings show a repository's settings page
func Settings(ctx *context.Context) {
	ctx.HTML(http.StatusOK, tplSettingsOptions)
}

// SettingsPost response for changes of a repository
func SettingsPost(ctx *context.Context) {
	ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
	ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
	ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
	ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
	ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
	ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval

	signing, _ := gitrepo.GetSigningKey(ctx)
	ctx.Data["SigningKeyAvailable"] = signing != nil
	ctx.Data["SigningSettings"] = setting.Repository.Signing
	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled

	switch ctx.FormString("action") {
	case "update":
		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)
	}
}

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 !strings.EqualFold(repo.LowerName, 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.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
			case db.IsErrNameReserved(err):
				ctx.RenderWithErrDeprecated(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.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form)
				case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
					ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
				case setting.Repository.AllowDeleteOfUnadoptedRepositories:
					ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
				default:
					ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
				}
			case db.IsErrNamePatternNotAllowed(err):
				ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
			default:
				ctx.ServerError("ChangeRepositoryName", err)
			}
			return
		}

		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

	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")
}

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
	}

	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.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &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
	}

	u, err := gitrepo.GitRemoteGetURL(ctx, ctx.Repo.Repository, pullMirror.GetRemoteName())
	if err != nil {
		ctx.Data["Err_MirrorAddress"] = true
		handleSettingRemoteAddrError(ctx, err, form)
		return
	}
	if u.User != nil {
		if form.MirrorUsername == "" {
			form.MirrorUsername = u.User.Username()
		}
		if form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() {
			form.MirrorPassword, _ = u.User.Password()
		}
	}

	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
	}

	if err := mirror_service.UpdateAddress(ctx, pullMirror, address); err != nil {
		ctx.ServerError("UpdateAddress", err)
		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

	if len(form.LFSEndpoint) > 0 {
		ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
		if ep == nil {
			ctx.Data["Err_LFSEndpoint"] = true
			ctx.RenderWithErrDeprecated(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
		}
	}

	pullMirror.LFS = form.LFS
	pullMirror.LFSEndpoint = form.LFSEndpoint
	if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
		ctx.ServerError("UpdateMirror", err)
		return
	}

	ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
	ctx.Redirect(repo.Link() + "/settings")
}

func handleSettingsPostMirrorSync(ctx *context.Context) {
	repo := ctx.Repo.Repository
	if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
		ctx.NotFound(nil)
		return
	}

	mirror_service.AddPullMirrorToQueue(repo.ID)

	ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
	ctx.Redirect(repo.Link() + "/settings")
}

func handleSettingsPostPushMirrorSync(ctx *context.Context) {
	form := web.GetForm(ctx).(*forms.RepoSettingForm)
	repo := ctx.Repo.Repository

	if !setting.Mirror.Enabled {
		ctx.NotFound(nil)
		return
	}

	m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
	if m == nil {
		ctx.NotFound(nil)
		return
	}

	mirror_service.AddPushMirrorToQueue(m.ID)

	ctx.Flash.Info(ctx.Tr("repo.settings.push_mirror_sync_in_progress", m.RemoteAddress))
	ctx.Redirect(repo.Link() + "/settings")
}

func handleSettingsPostPushMirrorUpdate(ctx *context.Context) {
	form := web.GetForm(ctx).(*forms.RepoSettingForm)
	repo := ctx.Repo.Repository

	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

	interval, err := time.ParseDuration(form.PushMirrorInterval)
	if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
		ctx.RenderWithErrDeprecated(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.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")
}

func handleSettingsPostPushMirrorRemove(ctx *context.Context) {
	form := web.GetForm(ctx).(*forms.RepoSettingForm)
	repo := ctx.Repo.Repository

	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

	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
	}

	if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
		ctx.ServerError("DeletePushMirrorByID", err)
		return
	}

	ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
	ctx.Redirect(repo.Link() + "/settings")
}

func handleSettingsPostPushMirrorAdd(ctx *context.Context) {
	form := web.GetForm(ctx).(*forms.RepoSettingForm)
	repo := ctx.Repo.Repository

	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

	interval, err := time.ParseDuration(form.PushMirrorInterval)
	if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
		ctx.Data["Err_PushMirrorInterval"] = true
		ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
		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
	}

	remoteSuffix := util.CryptoRandomString(10)

	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:    "remote_mirror_" + remoteSuffix,
		SyncOnCommit:  form.PushMirrorSyncOnCommit,
		Interval:      interval,
		RemoteAddress: remoteAddress,
	}
	if err := db.Insert(ctx, m); err != nil {
		ctx.ServerError("InsertPushMirror", err)
		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
	}

	ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
	ctx.Redirect(repo.Link() + "/settings")
}

func newRepoUnit(repo *repo_model.Repository, unitType unit_model.Type, config convert.Conversion) repo_model.RepoUnit {
	repoUnit := repo_model.RepoUnit{RepoID: repo.ID, Type: unitType, Config: config}
	for _, u := range repo.Units {
		if u.Type == unitType {
			repoUnit.EveryoneAccessMode = u.EveryoneAccessMode
			repoUnit.AnonymousAccessMode = u.AnonymousAccessMode
		}
	}
	return repoUnit
}

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

	// This section doesn't require repo_name/RepoName to be set in the form, don't show it
	// as an error on the UI for this action
	ctx.Data["Err_RepoName"] = nil

	if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
		repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
		repoChanged = true
	}

	if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() {
		units = append(units, newRepoUnit(repo, unit_model.TypeCode, nil))
	} else if !unit_model.TypeCode.UnitGlobalDisabled() {
		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
	}

	if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
		if !validation.IsValidURL(form.ExternalWikiURL) {
			ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
			ctx.Redirect(repo.Link() + "/settings")
			return
		}

		units = append(units, newRepoUnit(repo, unit_model.TypeExternalWiki, &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, newRepoUnit(repo, unit_model.TypeWiki, new(repo_model.UnitConfig)))
		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
	} else {
		if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
		}
		if !unit_model.TypeWiki.UnitGlobalDisabled() {
			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
		}
	}

	if form.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 form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
		if !validation.IsValidURL(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, newRepoUnit(repo, unit_model.TypeExternalTracker, &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, newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{
			EnableTimetracker:                form.EnableTimetracker,
			AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
			EnableDependencies:               form.EnableIssueDependencies,
		}))
		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
	} else {
		if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
		}
		if !unit_model.TypeIssues.UnitGlobalDisabled() {
			deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
		}
	}

	if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
		units = append(units, newRepoUnit(repo, unit_model.TypeProjects, &repo_model.ProjectsConfig{
			ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
		}))
	} else if !unit_model.TypeProjects.UnitGlobalDisabled() {
		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
	}

	if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() {
		units = append(units, newRepoUnit(repo, unit_model.TypeReleases, nil))
	} else if !unit_model.TypeReleases.UnitGlobalDisabled() {
		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases)
	}

	if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() {
		units = append(units, newRepoUnit(repo, unit_model.TypePackages, nil))
	} else if !unit_model.TypePackages.UnitGlobalDisabled() {
		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages)
	}

	if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() {
		defaultUpdateStyle := util.IfZero(repo_model.UpdateStyle(form.PullsDefaultUpdateStyle), repo_model.UpdateStyleMerge)
		prConfig := &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,
			AllowMergeUpdate:              form.PullsAllowMergeUpdate,
			AllowRebaseUpdate:             form.PullsAllowRebaseUpdate,
			DefaultUpdateStyle:            defaultUpdateStyle,
			DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
			DefaultMergeStyle:             repo_model.MergeStyle(form.PullsDefaultMergeStyle),
			DefaultAllowMaintainerEdit:    form.DefaultAllowMaintainerEdit,
			DefaultTargetBranch:           strings.TrimSpace(form.DefaultTargetBranch),
		}
		if err := prConfig.ValidateUpdateSettings(); err != nil {
			ctx.Flash.Error(err.Error())
			ctx.Redirect(repo.Link() + "/settings")
			return
		}
		units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, prConfig))
	} else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
	}

	if len(units) == 0 {
		ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit"))
		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
		return
	}

	if err := repo_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)

	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
	trustModel := repo_model.ToTrustModel(form.TrustModel)
	if trustModel != repo.TrustModel {
		repo.TrustModel = trustModel
		if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "trust_model"); err != nil {
			ctx.ServerError("UpdateRepositoryColsNoAutoTime", err)
			return
		}
		log.Trace("Repository signing 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")
}

func handleSettingsPostAdmin(ctx *context.Context) {
	if !ctx.Doer.IsAdmin {
		ctx.HTTPError(http.StatusForbidden)
		return
	}

	repo := ctx.Repo.Repository
	form := web.GetForm(ctx).(*forms.RepoSettingForm)
	if repo.IsFsckEnabled != form.EnableHealthCheck {
		repo.IsFsckEnabled = form.EnableHealthCheck
		if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_fsck_enabled"); err != nil {
			ctx.ServerError("UpdateRepositoryColsNoAutoTime", err)
			return
		}
		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")
}

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
		}
	case "code":
		if !setting.Indexer.RepoIndexerEnabled {
			ctx.HTTPError(http.StatusForbidden)
			return
		}
		code.UpdateRepoIndexer(ctx.Repo.Repository)
	default:
		ctx.NotFound(nil)
		return
	}

	log.Trace("Repository reindex for %s requested: %s/%s", form.RequestReindexType, ctx.Repo.Owner.Name, repo.Name)

	ctx.Flash.Success(ctx.Tr("repo.settings.reindex_requested"))
	ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}

func handleSettingsPostConvert(ctx *context.Context) {
	form := web.GetForm(ctx).(*forms.RepoSettingForm)
	repo := ctx.Repo.Repository
	if !ctx.Repo.Permission.IsOwner() {
		ctx.JSONErrorNotFound()
		return
	}
	if repo.Name != form.RepoName {
		ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
		return
	}

	if !repo.IsMirror {
		ctx.JSONErrorNotFound()
		return
	}
	repo.IsMirror = false

	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.JSONRedirect(repo.Link())
}

func handleSettingsPostConvertFork(ctx *context.Context) {
	form := web.GetForm(ctx).(*forms.RepoSettingForm)
	repo := ctx.Repo.Repository
	if !ctx.Repo.Permission.IsOwner() {
		ctx.JSONErrorNotFound()
		return
	}
	if err := repo.LoadOwner(ctx); err != nil {
		ctx.ServerError("Convert Fork", err)
		return
	}
	if repo.Name != form.RepoName {
		ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
		return
	}

	if !repo.IsFork {
		ctx.JSONErrorNotFound()
		return
	}

	if !ctx.Doer.CanForkRepoIn(ctx.Repo.Owner) {
		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.JSONRedirect(repo.Link() + "/settings")
		return
	}

	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
	}

	log.Trace("Repository converted from fork to regular: %s", repo.FullName())
	ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
	ctx.JSONRedirect(repo.Link())
}

func handleSettingsPostTransfer(ctx *context.Context) {
	form := web.GetForm(ctx).(*forms.RepoSettingForm)
	repo := ctx.Repo.Repository
	if !ctx.Repo.Permission.IsOwner() {
		ctx.JSONErrorNotFound()
		return
	}
	if repo.Name != form.RepoName {
		ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
		return
	}

	newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name"))
	if err != nil {
		if user_model.IsErrUserNotExist(err) {
			ctx.JSONError(ctx.Tr("form.enterred_invalid_owner_name"))
			return
		}
		ctx.ServerError("IsUserExist", err)
		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.JSONError(ctx.Tr("form.enterred_invalid_owner_name"))
			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.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
		} else if repo_model.IsErrRepoTransferInProgress(err) {
			ctx.JSONError(ctx.Tr("repo.settings.transfer_in_progress"))
		} else if repo_service.IsRepositoryLimitReached(err) {
			limit := err.(repo_service.LimitReachedError).Limit
			ctx.JSONError(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit))
		} else if errors.Is(err, user_model.ErrBlockedUser) {
			ctx.JSONError(ctx.Tr("repo.settings.transfer.blocked_user"))
		} else {
			ctx.ServerError("TransferOwnership", err)
		}

		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.JSONRedirect(repo.Link() + "/settings")
}

func handleSettingsPostCancelTransfer(ctx *context.Context) {
	repo := ctx.Repo.Repository
	if !ctx.Repo.Permission.IsOwner() {
		ctx.HTTPError(http.StatusNotFound)
		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)
		}
		return
	}

	if err := repo_service.CancelRepositoryTransfer(ctx, repoTransfer, ctx.Doer); err != nil {
		ctx.ServerError("CancelRepositoryTransfer", err)
		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")
}

func handleSettingsPostDelete(ctx *context.Context) {
	form := web.GetForm(ctx).(*forms.RepoSettingForm)
	repo := ctx.Repo.Repository
	if !ctx.Repo.Permission.IsOwner() {
		ctx.JSONErrorNotFound()
		return
	}
	if repo.Name != form.RepoName {
		ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
		return
	}

	// Close the gitrepository before doing this.
	if ctx.Repo.GitRepo != nil {
		ctx.Repo.GitRepo.Close()
	}

	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)

	ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
	ctx.JSONRedirect(ctx.Repo.Owner.DashboardLink())
}

func handleSettingsPostDeleteWiki(ctx *context.Context) {
	form := web.GetForm(ctx).(*forms.RepoSettingForm)
	repo := ctx.Repo.Repository
	if !ctx.Repo.Permission.IsOwner() {
		ctx.JSONErrorNotFound()
		return
	}
	if repo.Name != form.RepoName {
		ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
		return
	}

	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.JSONRedirect(ctx.Repo.RepoLink + "/settings")
}

func handleSettingsPostArchive(ctx *context.Context) {
	repo := ctx.Repo.Repository
	if !ctx.Repo.Permission.IsOwner() {
		ctx.HTTPError(http.StatusForbidden)
		return
	}

	if repo.IsMirror {
		ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
		ctx.Redirect(ctx.Repo.RepoLink + "/settings")
		return
	}

	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
	}

	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)
	}

	// update issue indexer
	issue_indexer.UpdateRepoIndexer(ctx, repo.ID)

	ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))

	log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
	ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}

func handleSettingsPostUnarchive(ctx *context.Context) {
	repo := ctx.Repo.Repository
	if !ctx.Repo.Permission.IsOwner() {
		ctx.HTTPError(http.StatusForbidden)
		return
	}

	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
	}

	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)
		}
	}

	// update issue indexer
	issue_indexer.UpdateRepoIndexer(ctx, repo.ID)

	ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))

	log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
	ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}

func handleSettingsPostVisibility(ctx *context.Context) {
	repo := ctx.Repo.Repository
	if repo.IsFork {
		ctx.JSONError(ctx.Tr("repo.settings.visibility.fork_error"))
		return
	}

	private := ctx.FormOptionalBool("private").ValueOrDefault(true) // default to true for privacy & safety

	// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
	if !private && setting.Repository.ForcePrivate && !ctx.Doer.IsAdmin {
		ctx.JSONError(ctx.Tr("form.repository_force_private"))
		return
	}
	if private && repo.FullName() != ctx.FormString("confirm_repo_name") {
		ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
		return
	}

	err := repo_service.MakeRepoPrivate(ctx, repo, private)
	if err != nil {
		log.Error("Tried to change the visibility of the repo: %s", err)
		ctx.JSONError(ctx.Tr("repo.settings.visibility.error"))
		return
	}

	ctx.Flash.Success(ctx.Tr("repo.settings.visibility.success"))
	ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings")
}

func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {
	if git.IsErrInvalidCloneAddr(err) {
		addrErr := err.(*git.ErrInvalidCloneAddr)
		switch {
		case addrErr.IsProtocolInvalid:
			ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form)
		case addrErr.IsURLError:
			ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", addrErr.Host), tplSettingsOptions, form)
		case addrErr.IsPermissionDenied:
			if addrErr.LocalPath {
				ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form)
			} else {
				ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form)
			}
		case addrErr.IsInvalidPath:
			ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
		default:
			ctx.ServerError("Unknown error", err)
		}
		return
	}
	ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form)
}
