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 }