package cmd import ( "fmt" "net/http" "strings" "code.gitea.io/sdk/gitea" "github.com/spf13/cobra" ) var branchProtectCmd = &cobra.Command{ Use: "protect ", Short: "Protect a branch", Long: `Create or update a branch protection rule for the given branch. If a protection rule already exists for the branch it is updated in place; otherwise a new one is created. Fields you do not set on the command line are left at their server-side defaults (or left unchanged when editing).`, Example: ` # Require 2 approving reviews before merging fgj branch protect main --require-approvals 2 # Dismiss stale approvals and require signed commits fgj branch protect main --dismiss-stale-approvals --require-signed-commits # Allow specific users to push directly, and require CI contexts fgj branch protect main \ --push-whitelist alice,bob \ --require-status-checks ci/build,ci/test`, Args: cobra.ExactArgs(1), RunE: runBranchProtect, } var branchUnprotectCmd = &cobra.Command{ Use: "unprotect ", Short: "Remove a branch protection rule", Long: "Remove the protection rule attached to a branch. If the branch has no protection this is a no-op.", Example: ` # Remove protection interactively fgj branch unprotect main # Remove without confirmation fgj branch unprotect main -y`, Args: cobra.ExactArgs(1), RunE: runBranchUnprotect, } func init() { branchCmd.AddCommand(branchProtectCmd) branchCmd.AddCommand(branchUnprotectCmd) addRepoFlags(branchProtectCmd) branchProtectCmd.Flags().Int64("require-approvals", 0, "Minimum number of approving reviews required") branchProtectCmd.Flags().Bool("dismiss-stale-approvals", false, "Dismiss stale approvals when new commits are pushed") branchProtectCmd.Flags().Bool("require-signed-commits", false, "Require commits on the branch to be signed") branchProtectCmd.Flags().Bool("block-on-rejected-reviews", false, "Block merges when a review requests changes") branchProtectCmd.Flags().Bool("block-on-outdated-branch", false, "Require the PR branch to be up-to-date with the base") branchProtectCmd.Flags().StringSlice("push-whitelist", nil, "Usernames allowed to push directly (comma-separated or repeatable)") branchProtectCmd.Flags().StringSlice("merge-whitelist", nil, "Usernames allowed to merge (comma-separated or repeatable)") branchProtectCmd.Flags().StringSlice("require-status-checks", nil, "CI status contexts that must pass (comma-separated or repeatable)") addJSONFlags(branchProtectCmd, "Output the resulting protection rule as JSON") addRepoFlags(branchUnprotectCmd) branchUnprotectCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") } func runBranchProtect(cmd *cobra.Command, args []string) error { client, owner, name, err := newBranchClient(cmd) if err != nil { return err } branchName := args[0] requireApprovals, _ := cmd.Flags().GetInt64("require-approvals") dismissStale, _ := cmd.Flags().GetBool("dismiss-stale-approvals") requireSigned, _ := cmd.Flags().GetBool("require-signed-commits") blockRejected, _ := cmd.Flags().GetBool("block-on-rejected-reviews") blockOutdated, _ := cmd.Flags().GetBool("block-on-outdated-branch") pushWhitelist, _ := cmd.Flags().GetStringSlice("push-whitelist") mergeWhitelist, _ := cmd.Flags().GetStringSlice("merge-whitelist") statusChecks, _ := cmd.Flags().GetStringSlice("require-status-checks") // Check whether a protection rule already exists for this branch. existing, resp, getErr := client.GetBranchProtection(owner, name, branchName) exists := getErr == nil && existing != nil if getErr != nil && !isNotFound(resp, getErr) { return fmt.Errorf("failed to look up branch protection for %q: %w", branchName, getErr) } var result *gitea.BranchProtection if exists { edit := gitea.EditBranchProtectionOption{ RequiredApprovals: &requireApprovals, DismissStaleApprovals: &dismissStale, RequireSignedCommits: &requireSigned, BlockOnRejectedReviews: &blockRejected, BlockOnOutdatedBranch: &blockOutdated, } if len(pushWhitelist) > 0 { enable := true edit.EnablePushWhitelist = &enable edit.PushWhitelistUsernames = pushWhitelist } if len(mergeWhitelist) > 0 { enable := true edit.EnableMergeWhitelist = &enable edit.MergeWhitelistUsernames = mergeWhitelist } if len(statusChecks) > 0 { enable := true edit.EnableStatusCheck = &enable edit.StatusCheckContexts = statusChecks } bp, _, err := client.EditBranchProtection(owner, name, branchName, edit) if err != nil { return fmt.Errorf("failed to update branch protection for %q: %w", branchName, err) } result = bp } else { create := gitea.CreateBranchProtectionOption{ BranchName: branchName, RequiredApprovals: requireApprovals, DismissStaleApprovals: dismissStale, RequireSignedCommits: requireSigned, BlockOnRejectedReviews: blockRejected, BlockOnOutdatedBranch: blockOutdated, } if len(pushWhitelist) > 0 { create.EnablePushWhitelist = true create.PushWhitelistUsernames = pushWhitelist } if len(mergeWhitelist) > 0 { create.EnableMergeWhitelist = true create.MergeWhitelistUsernames = mergeWhitelist } if len(statusChecks) > 0 { create.EnableStatusCheck = true create.StatusCheckContexts = statusChecks } bp, _, err := client.CreateBranchProtection(owner, name, create) if err != nil { return fmt.Errorf("failed to create branch protection for %q: %w", branchName, err) } result = bp } if wantJSON(cmd) { return outputJSON(cmd, result) } cs := ios.ColorScheme() fmt.Fprintf(ios.Out, "%s Protected branch %q\n", cs.SuccessIcon(), branchName) return nil } func runBranchUnprotect(cmd *cobra.Command, args []string) error { client, owner, name, err := newBranchClient(cmd) if err != nil { return err } branchName := args[0] skipConfirm, _ := cmd.Flags().GetBool("yes") if !skipConfirm && ios.IsStdinTTY() { answer, err := promptLine(fmt.Sprintf("Remove protection from branch %q in %s/%s? [y/N]: ", branchName, owner, name)) if err != nil { return err } if answer != "y" && answer != "Y" && answer != "yes" { fmt.Fprintln(ios.ErrOut, "Cancelled.") return nil } } resp, err := client.DeleteBranchProtection(owner, name, branchName) if err != nil { if isNotFound(resp, err) { fmt.Fprintf(ios.Out, "Branch %q has no protection rule; nothing to do.\n", branchName) return nil } return fmt.Errorf("failed to remove branch protection for %q: %w", branchName, err) } cs := ios.ColorScheme() fmt.Fprintf(ios.Out, "%s Removed protection from branch %q\n", cs.SuccessIcon(), branchName) return nil } // isNotFound reports whether a gitea SDK call failed with a 404. The SDK // sometimes returns a nil Response on transport-level errors, so we fall back // to a string check on the error message in that case. func isNotFound(resp *gitea.Response, err error) bool { if err == nil { return false } if resp != nil && resp.Response != nil && resp.StatusCode == http.StatusNotFound { return true } return strings.Contains(err.Error(), "404") }