code.gitea.io/gitea@v1.21.7/routers/web/repo/setting/protected_branch.go (about)

     1  // Copyright 2017 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package setting
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"net/url"
    10  	"strings"
    11  	"time"
    12  
    13  	git_model "code.gitea.io/gitea/models/git"
    14  	"code.gitea.io/gitea/models/organization"
    15  	"code.gitea.io/gitea/models/perm"
    16  	access_model "code.gitea.io/gitea/models/perm/access"
    17  	"code.gitea.io/gitea/modules/base"
    18  	"code.gitea.io/gitea/modules/context"
    19  	"code.gitea.io/gitea/modules/web"
    20  	"code.gitea.io/gitea/routers/web/repo"
    21  	"code.gitea.io/gitea/services/forms"
    22  	pull_service "code.gitea.io/gitea/services/pull"
    23  	"code.gitea.io/gitea/services/repository"
    24  
    25  	"github.com/gobwas/glob"
    26  )
    27  
    28  const (
    29  	tplProtectedBranch base.TplName = "repo/settings/protected_branch"
    30  )
    31  
    32  // ProtectedBranchRules render the page to protect the repository
    33  func ProtectedBranchRules(ctx *context.Context) {
    34  	ctx.Data["Title"] = ctx.Tr("repo.settings.branches")
    35  	ctx.Data["PageIsSettingsBranches"] = true
    36  
    37  	rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID)
    38  	if err != nil {
    39  		ctx.ServerError("GetProtectedBranches", err)
    40  		return
    41  	}
    42  	ctx.Data["ProtectedBranches"] = rules
    43  
    44  	repo.PrepareBranchList(ctx)
    45  	if ctx.Written() {
    46  		return
    47  	}
    48  
    49  	ctx.HTML(http.StatusOK, tplBranches)
    50  }
    51  
    52  // SettingsProtectedBranch renders the protected branch setting page
    53  func SettingsProtectedBranch(c *context.Context) {
    54  	ruleName := c.FormString("rule_name")
    55  	var rule *git_model.ProtectedBranch
    56  	if ruleName != "" {
    57  		var err error
    58  		rule, err = git_model.GetProtectedBranchRuleByName(c, c.Repo.Repository.ID, ruleName)
    59  		if err != nil {
    60  			c.ServerError("GetProtectBranchOfRepoByName", err)
    61  			return
    62  		}
    63  	}
    64  
    65  	if rule == nil {
    66  		// No options found, create defaults.
    67  		rule = &git_model.ProtectedBranch{}
    68  	}
    69  
    70  	c.Data["PageIsSettingsBranches"] = true
    71  	c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + rule.RuleName
    72  
    73  	users, err := access_model.GetRepoReaders(c.Repo.Repository)
    74  	if err != nil {
    75  		c.ServerError("Repo.Repository.GetReaders", err)
    76  		return
    77  	}
    78  	c.Data["Users"] = users
    79  	c.Data["whitelist_users"] = strings.Join(base.Int64sToStrings(rule.WhitelistUserIDs), ",")
    80  	c.Data["merge_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistUserIDs), ",")
    81  	c.Data["approvals_whitelist_users"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistUserIDs), ",")
    82  	c.Data["status_check_contexts"] = strings.Join(rule.StatusCheckContexts, "\n")
    83  	contexts, _ := git_model.FindRepoRecentCommitStatusContexts(c, c.Repo.Repository.ID, 7*24*time.Hour) // Find last week status check contexts
    84  	c.Data["recent_status_checks"] = contexts
    85  
    86  	if c.Repo.Owner.IsOrganization() {
    87  		teams, err := organization.OrgFromUser(c.Repo.Owner).TeamsWithAccessToRepo(c.Repo.Repository.ID, perm.AccessModeRead)
    88  		if err != nil {
    89  			c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
    90  			return
    91  		}
    92  		c.Data["Teams"] = teams
    93  		c.Data["whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.WhitelistTeamIDs), ",")
    94  		c.Data["merge_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.MergeWhitelistTeamIDs), ",")
    95  		c.Data["approvals_whitelist_teams"] = strings.Join(base.Int64sToStrings(rule.ApprovalsWhitelistTeamIDs), ",")
    96  	}
    97  
    98  	c.Data["Rule"] = rule
    99  	c.HTML(http.StatusOK, tplProtectedBranch)
   100  }
   101  
   102  // SettingsProtectedBranchPost updates the protected branch settings
   103  func SettingsProtectedBranchPost(ctx *context.Context) {
   104  	f := web.GetForm(ctx).(*forms.ProtectBranchForm)
   105  	var protectBranch *git_model.ProtectedBranch
   106  	if f.RuleName == "" {
   107  		ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_rule_name"))
   108  		ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit", ctx.Repo.RepoLink))
   109  		return
   110  	}
   111  
   112  	var err error
   113  	if f.RuleID > 0 {
   114  		// If the RuleID isn't 0, it must be an edit operation. So we get rule by id.
   115  		protectBranch, err = git_model.GetProtectedBranchRuleByID(ctx, ctx.Repo.Repository.ID, f.RuleID)
   116  		if err != nil {
   117  			ctx.ServerError("GetProtectBranchOfRepoByID", err)
   118  			return
   119  		}
   120  		if protectBranch != nil && protectBranch.RuleName != f.RuleName {
   121  			// RuleName changed. We need to check if there is a rule with the same name.
   122  			// If a rule with the same name exists, an error should be returned.
   123  			sameNameProtectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, f.RuleName)
   124  			if err != nil {
   125  				ctx.ServerError("GetProtectBranchOfRepoByName", err)
   126  				return
   127  			}
   128  			if sameNameProtectBranch != nil {
   129  				ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_duplicate_rule_name"))
   130  				ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit?rule_name=%s", ctx.Repo.RepoLink, protectBranch.RuleName))
   131  				return
   132  			}
   133  		}
   134  	} else {
   135  		// FIXME: If a new ProtectBranch has a duplicate RuleName, an error should be returned.
   136  		// Currently, if a new ProtectBranch with a duplicate RuleName is created, the existing ProtectBranch will be updated.
   137  		// But we cannot modify this logic now because many unit tests rely on it.
   138  		protectBranch, err = git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, f.RuleName)
   139  		if err != nil {
   140  			ctx.ServerError("GetProtectBranchOfRepoByName", err)
   141  			return
   142  		}
   143  	}
   144  	if protectBranch == nil {
   145  		// No options found, create defaults.
   146  		protectBranch = &git_model.ProtectedBranch{
   147  			RepoID:   ctx.Repo.Repository.ID,
   148  			RuleName: f.RuleName,
   149  		}
   150  	}
   151  
   152  	var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64
   153  	protectBranch.RuleName = f.RuleName
   154  	if f.RequiredApprovals < 0 {
   155  		ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_approvals_min"))
   156  		ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit?rule_name=%s", ctx.Repo.RepoLink, f.RuleName))
   157  		return
   158  	}
   159  
   160  	switch f.EnablePush {
   161  	case "all":
   162  		protectBranch.CanPush = true
   163  		protectBranch.EnableWhitelist = false
   164  		protectBranch.WhitelistDeployKeys = false
   165  	case "whitelist":
   166  		protectBranch.CanPush = true
   167  		protectBranch.EnableWhitelist = true
   168  		protectBranch.WhitelistDeployKeys = f.WhitelistDeployKeys
   169  		if strings.TrimSpace(f.WhitelistUsers) != "" {
   170  			whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ","))
   171  		}
   172  		if strings.TrimSpace(f.WhitelistTeams) != "" {
   173  			whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ","))
   174  		}
   175  	default:
   176  		protectBranch.CanPush = false
   177  		protectBranch.EnableWhitelist = false
   178  		protectBranch.WhitelistDeployKeys = false
   179  	}
   180  
   181  	protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist
   182  	if f.EnableMergeWhitelist {
   183  		if strings.TrimSpace(f.MergeWhitelistUsers) != "" {
   184  			mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ","))
   185  		}
   186  		if strings.TrimSpace(f.MergeWhitelistTeams) != "" {
   187  			mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ","))
   188  		}
   189  	}
   190  
   191  	protectBranch.EnableStatusCheck = f.EnableStatusCheck
   192  	if f.EnableStatusCheck {
   193  		patterns := strings.Split(strings.ReplaceAll(f.StatusCheckContexts, "\r", "\n"), "\n")
   194  		validPatterns := make([]string, 0, len(patterns))
   195  		for _, pattern := range patterns {
   196  			trimmed := strings.TrimSpace(pattern)
   197  			if trimmed == "" {
   198  				continue
   199  			}
   200  			if _, err := glob.Compile(trimmed); err != nil {
   201  				ctx.Flash.Error(ctx.Tr("repo.settings.protect_invalid_status_check_pattern", pattern))
   202  				ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit?rule_name=%s", ctx.Repo.RepoLink, url.QueryEscape(protectBranch.RuleName)))
   203  				return
   204  			}
   205  			validPatterns = append(validPatterns, trimmed)
   206  		}
   207  		if len(validPatterns) == 0 {
   208  			// if status check is enabled, patterns slice is not allowed to be empty
   209  			ctx.Flash.Error(ctx.Tr("repo.settings.protect_no_valid_status_check_patterns"))
   210  			ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit?rule_name=%s", ctx.Repo.RepoLink, url.QueryEscape(protectBranch.RuleName)))
   211  			return
   212  		}
   213  		protectBranch.StatusCheckContexts = validPatterns
   214  	} else {
   215  		protectBranch.StatusCheckContexts = nil
   216  	}
   217  
   218  	protectBranch.RequiredApprovals = f.RequiredApprovals
   219  	protectBranch.EnableApprovalsWhitelist = f.EnableApprovalsWhitelist
   220  	if f.EnableApprovalsWhitelist {
   221  		if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" {
   222  			approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ","))
   223  		}
   224  		if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" {
   225  			approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ","))
   226  		}
   227  	}
   228  	protectBranch.BlockOnRejectedReviews = f.BlockOnRejectedReviews
   229  	protectBranch.BlockOnOfficialReviewRequests = f.BlockOnOfficialReviewRequests
   230  	protectBranch.DismissStaleApprovals = f.DismissStaleApprovals
   231  	protectBranch.RequireSignedCommits = f.RequireSignedCommits
   232  	protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
   233  	protectBranch.UnprotectedFilePatterns = f.UnprotectedFilePatterns
   234  	protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
   235  
   236  	err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
   237  		UserIDs:          whitelistUsers,
   238  		TeamIDs:          whitelistTeams,
   239  		MergeUserIDs:     mergeWhitelistUsers,
   240  		MergeTeamIDs:     mergeWhitelistTeams,
   241  		ApprovalsUserIDs: approvalsWhitelistUsers,
   242  		ApprovalsTeamIDs: approvalsWhitelistTeams,
   243  	})
   244  	if err != nil {
   245  		ctx.ServerError("UpdateProtectBranch", err)
   246  		return
   247  	}
   248  
   249  	// FIXME: since we only need to recheck files protected rules, we could improve this
   250  	matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, protectBranch.RuleName)
   251  	if err != nil {
   252  		ctx.ServerError("FindAllMatchedBranches", err)
   253  		return
   254  	}
   255  	for _, branchName := range matchedBranches {
   256  		if err = pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, branchName); err != nil {
   257  			ctx.ServerError("CheckPRsForBaseBranch", err)
   258  			return
   259  		}
   260  	}
   261  
   262  	ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", protectBranch.RuleName))
   263  	ctx.Redirect(fmt.Sprintf("%s/settings/branches?rule_name=%s", ctx.Repo.RepoLink, protectBranch.RuleName))
   264  }
   265  
   266  // DeleteProtectedBranchRulePost delete protected branch rule by id
   267  func DeleteProtectedBranchRulePost(ctx *context.Context) {
   268  	ruleID := ctx.ParamsInt64("id")
   269  	if ruleID <= 0 {
   270  		ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
   271  		ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
   272  		return
   273  	}
   274  
   275  	rule, err := git_model.GetProtectedBranchRuleByID(ctx, ctx.Repo.Repository.ID, ruleID)
   276  	if err != nil {
   277  		ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
   278  		ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
   279  		return
   280  	}
   281  
   282  	if rule == nil {
   283  		ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
   284  		ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
   285  		return
   286  	}
   287  
   288  	if err := git_model.DeleteProtectedBranch(ctx, ctx.Repo.Repository.ID, ruleID); err != nil {
   289  		ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", rule.RuleName))
   290  		ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
   291  		return
   292  	}
   293  
   294  	ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", rule.RuleName))
   295  	ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
   296  }
   297  
   298  // RenameBranchPost responses for rename a branch
   299  func RenameBranchPost(ctx *context.Context) {
   300  	form := web.GetForm(ctx).(*forms.RenameBranchForm)
   301  
   302  	if !ctx.Repo.CanCreateBranch() {
   303  		ctx.NotFound("RenameBranch", nil)
   304  		return
   305  	}
   306  
   307  	if ctx.HasError() {
   308  		ctx.Flash.Error(ctx.GetErrMsg())
   309  		ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
   310  		return
   311  	}
   312  
   313  	msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To)
   314  	if err != nil {
   315  		ctx.ServerError("RenameBranch", err)
   316  		return
   317  	}
   318  
   319  	if msg == "target_exist" {
   320  		ctx.Flash.Error(ctx.Tr("repo.settings.rename_branch_failed_exist", form.To))
   321  		ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
   322  		return
   323  	}
   324  
   325  	if msg == "from_not_exist" {
   326  		ctx.Flash.Error(ctx.Tr("repo.settings.rename_branch_failed_not_exist", form.From))
   327  		ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
   328  		return
   329  	}
   330  
   331  	ctx.Flash.Success(ctx.Tr("repo.settings.rename_branch_success", form.From, form.To))
   332  	ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
   333  }