From 9ff4e1d2d9636ea8aa328427f1d31c962221263e Mon Sep 17 00:00:00 2001
From: David Svantesson <davidsvantesson@gmail.com>
Date: Thu, 13 Feb 2020 00:19:35 +0100
Subject: [PATCH] Add API branch protection endpoint (#9311)

* add API branch protection endpoint

* lint

* Change to use team names instead of ids.

* Status codes.

* fix

* Fix

* Add new branch protection options (BlockOnRejectedReviews, DismissStaleApprovals, RequireSignedCommits)

* Do xorm query directly

* fix xorm GetUserNamesByIDs

* Add some tests

* Improved GetTeamNamesByID

* http status created for CreateBranchProtection

* Correct status code in integration test

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: zeripath <art27@cantab.net>
---
 integrations/api_branch_test.go   |  68 ++++
 models/org_team.go                |  33 ++
 models/user.go                    |  11 +
 modules/convert/convert.go        |  92 ++++-
 modules/structs/repo_branch.go    |  90 ++++-
 routers/api/v1/api.go             |   9 +
 routers/api/v1/repo/branch.go     | 506 ++++++++++++++++++++++++++-
 routers/api/v1/swagger/options.go |   6 +
 routers/api/v1/swagger/repo.go    |  14 +
 templates/swagger/v1_json.tmpl    | 551 +++++++++++++++++++++++++++++-
 10 files changed, 1352 insertions(+), 28 deletions(-)

diff --git a/integrations/api_branch_test.go b/integrations/api_branch_test.go
index 037a42deec..3fe7f23229 100644
--- a/integrations/api_branch_test.go
+++ b/integrations/api_branch_test.go
@@ -30,6 +30,54 @@ func testAPIGetBranch(t *testing.T, branchName string, exists bool) {
 	assert.EqualValues(t, branchName, branch.Name)
 }
 
+func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
+	session := loginUser(t, "user2")
+	token := getTokenForLoggedInUser(t, session)
+	req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
+	resp := session.MakeRequest(t, req, expectedHTTPStatus)
+
+	if resp.Code == 200 {
+		var branchProtection api.BranchProtection
+		DecodeJSON(t, resp, &branchProtection)
+		assert.EqualValues(t, branchName, branchProtection.BranchName)
+	}
+}
+
+func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
+	session := loginUser(t, "user2")
+	token := getTokenForLoggedInUser(t, session)
+	req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections?token="+token, &api.BranchProtection{
+		BranchName: branchName,
+	})
+	resp := session.MakeRequest(t, req, expectedHTTPStatus)
+
+	if resp.Code == 201 {
+		var branchProtection api.BranchProtection
+		DecodeJSON(t, resp, &branchProtection)
+		assert.EqualValues(t, branchName, branchProtection.BranchName)
+	}
+}
+
+func testAPIEditBranchProtection(t *testing.T, branchName string, body *api.BranchProtection, expectedHTTPStatus int) {
+	session := loginUser(t, "user2")
+	token := getTokenForLoggedInUser(t, session)
+	req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user2/repo1/branch_protections/"+branchName+"?token="+token, body)
+	resp := session.MakeRequest(t, req, expectedHTTPStatus)
+
+	if resp.Code == 200 {
+		var branchProtection api.BranchProtection
+		DecodeJSON(t, resp, &branchProtection)
+		assert.EqualValues(t, branchName, branchProtection.BranchName)
+	}
+}
+
+func testAPIDeleteBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
+	session := loginUser(t, "user2")
+	token := getTokenForLoggedInUser(t, session)
+	req := NewRequestf(t, "DELETE", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
+	session.MakeRequest(t, req, expectedHTTPStatus)
+}
+
 func TestAPIGetBranch(t *testing.T) {
 	for _, test := range []struct {
 		BranchName string
@@ -43,3 +91,23 @@ func TestAPIGetBranch(t *testing.T) {
 		testAPIGetBranch(t, test.BranchName, test.Exists)
 	}
 }
+
+func TestAPIBranchProtection(t *testing.T) {
+	defer prepareTestEnv(t)()
+
+	// Branch protection only on branch that exist
+	testAPICreateBranchProtection(t, "master/doesnotexist", http.StatusNotFound)
+	// Get branch protection on branch that exist but not branch protection
+	testAPIGetBranchProtection(t, "master", http.StatusNotFound)
+
+	testAPICreateBranchProtection(t, "master", http.StatusCreated)
+	// Can only create once
+	testAPICreateBranchProtection(t, "master", http.StatusForbidden)
+
+	testAPIGetBranchProtection(t, "master", http.StatusOK)
+	testAPIEditBranchProtection(t, "master", &api.BranchProtection{
+		EnablePush: true,
+	}, http.StatusOK)
+
+	testAPIDeleteBranchProtection(t, "master", http.StatusNoContent)
+}
diff --git a/models/org_team.go b/models/org_team.go
index 214790703c..f8013d12c6 100644
--- a/models/org_team.go
+++ b/models/org_team.go
@@ -553,6 +553,23 @@ func GetTeam(orgID int64, name string) (*Team, error) {
 	return getTeam(x, orgID, name)
 }
 
+// GetTeamIDsByNames returns a slice of team ids corresponds to names.
+func GetTeamIDsByNames(orgID int64, names []string, ignoreNonExistent bool) ([]int64, error) {
+	ids := make([]int64, 0, len(names))
+	for _, name := range names {
+		u, err := GetTeam(orgID, name)
+		if err != nil {
+			if ignoreNonExistent {
+				continue
+			} else {
+				return nil, err
+			}
+		}
+		ids = append(ids, u.ID)
+	}
+	return ids, nil
+}
+
 // getOwnerTeam returns team by given team name and organization.
 func getOwnerTeam(e Engine, orgID int64) (*Team, error) {
 	return getTeam(e, orgID, ownerTeamName)
@@ -574,6 +591,22 @@ func GetTeamByID(teamID int64) (*Team, error) {
 	return getTeamByID(x, teamID)
 }
 
+// GetTeamNamesByID returns team's lower name from a list of team ids.
+func GetTeamNamesByID(teamIDs []int64) ([]string, error) {
+	if len(teamIDs) == 0 {
+		return []string{}, nil
+	}
+
+	var teamNames []string
+	err := x.Table("team").
+		Select("lower_name").
+		In("id", teamIDs).
+		Asc("name").
+		Find(&teamNames)
+
+	return teamNames, err
+}
+
 // UpdateTeam updates information of team.
 func UpdateTeam(t *Team, authChanged bool, includeAllChanged bool) (err error) {
 	if len(t.Name) == 0 {
diff --git a/models/user.go b/models/user.go
index 220a9f9a9a..0e493dbef7 100644
--- a/models/user.go
+++ b/models/user.go
@@ -1386,6 +1386,17 @@ func GetMaileableUsersByIDs(ids []int64) ([]*User, error) {
 		Find(&ous)
 }
 
+// GetUserNamesByIDs returns usernames for all resolved users from a list of Ids.
+func GetUserNamesByIDs(ids []int64) ([]string, error) {
+	unames := make([]string, 0, len(ids))
+	err := x.In("id", ids).
+		Table("user").
+		Asc("name").
+		Cols("name").
+		Find(&unames)
+	return unames, err
+}
+
 // GetUsersByIDs returns all resolved users from a list of Ids.
 func GetUsersByIDs(ids []int64) ([]*User, error) {
 	ous := make([]*User, 0, len(ids))
diff --git a/modules/convert/convert.go b/modules/convert/convert.go
index a69b09a2b7..31b46bc7eb 100644
--- a/modules/convert/convert.go
+++ b/modules/convert/convert.go
@@ -30,28 +30,86 @@ func ToEmail(email *models.EmailAddress) *api.Email {
 }
 
 // ToBranch convert a git.Commit and git.Branch to an api.Branch
-func ToBranch(repo *models.Repository, b *git.Branch, c *git.Commit, bp *models.ProtectedBranch, user *models.User) *api.Branch {
+func ToBranch(repo *models.Repository, b *git.Branch, c *git.Commit, bp *models.ProtectedBranch, user *models.User, isRepoAdmin bool) *api.Branch {
 	if bp == nil {
 		return &api.Branch{
-			Name:                b.Name,
-			Commit:              ToCommit(repo, c),
-			Protected:           false,
-			RequiredApprovals:   0,
-			EnableStatusCheck:   false,
-			StatusCheckContexts: []string{},
-			UserCanPush:         true,
-			UserCanMerge:        true,
+			Name:                          b.Name,
+			Commit:                        ToCommit(repo, c),
+			Protected:                     false,
+			RequiredApprovals:             0,
+			EnableStatusCheck:             false,
+			StatusCheckContexts:           []string{},
+			UserCanPush:                   true,
+			UserCanMerge:                  true,
+			EffectiveBranchProtectionName: "",
 		}
 	}
+	branchProtectionName := ""
+	if isRepoAdmin {
+		branchProtectionName = bp.BranchName
+	}
+
 	return &api.Branch{
-		Name:                b.Name,
-		Commit:              ToCommit(repo, c),
-		Protected:           true,
-		RequiredApprovals:   bp.RequiredApprovals,
-		EnableStatusCheck:   bp.EnableStatusCheck,
-		StatusCheckContexts: bp.StatusCheckContexts,
-		UserCanPush:         bp.CanUserPush(user.ID),
-		UserCanMerge:        bp.IsUserMergeWhitelisted(user.ID),
+		Name:                          b.Name,
+		Commit:                        ToCommit(repo, c),
+		Protected:                     true,
+		RequiredApprovals:             bp.RequiredApprovals,
+		EnableStatusCheck:             bp.EnableStatusCheck,
+		StatusCheckContexts:           bp.StatusCheckContexts,
+		UserCanPush:                   bp.CanUserPush(user.ID),
+		UserCanMerge:                  bp.IsUserMergeWhitelisted(user.ID),
+		EffectiveBranchProtectionName: branchProtectionName,
+	}
+}
+
+// ToBranchProtection convert a ProtectedBranch to api.BranchProtection
+func ToBranchProtection(bp *models.ProtectedBranch) *api.BranchProtection {
+	pushWhitelistUsernames, err := models.GetUserNamesByIDs(bp.WhitelistUserIDs)
+	if err != nil {
+		log.Error("GetUserNamesByIDs (WhitelistUserIDs): %v", err)
+	}
+	mergeWhitelistUsernames, err := models.GetUserNamesByIDs(bp.MergeWhitelistUserIDs)
+	if err != nil {
+		log.Error("GetUserNamesByIDs (MergeWhitelistUserIDs): %v", err)
+	}
+	approvalsWhitelistUsernames, err := models.GetUserNamesByIDs(bp.ApprovalsWhitelistUserIDs)
+	if err != nil {
+		log.Error("GetUserNamesByIDs (ApprovalsWhitelistUserIDs): %v", err)
+	}
+	pushWhitelistTeams, err := models.GetTeamNamesByID(bp.WhitelistTeamIDs)
+	if err != nil {
+		log.Error("GetTeamNamesByID (WhitelistTeamIDs): %v", err)
+	}
+	mergeWhitelistTeams, err := models.GetTeamNamesByID(bp.MergeWhitelistTeamIDs)
+	if err != nil {
+		log.Error("GetTeamNamesByID (MergeWhitelistTeamIDs): %v", err)
+	}
+	approvalsWhitelistTeams, err := models.GetTeamNamesByID(bp.ApprovalsWhitelistTeamIDs)
+	if err != nil {
+		log.Error("GetTeamNamesByID (ApprovalsWhitelistTeamIDs): %v", err)
+	}
+
+	return &api.BranchProtection{
+		BranchName:                  bp.BranchName,
+		EnablePush:                  bp.CanPush,
+		EnablePushWhitelist:         bp.EnableWhitelist,
+		PushWhitelistUsernames:      pushWhitelistUsernames,
+		PushWhitelistTeams:          pushWhitelistTeams,
+		PushWhitelistDeployKeys:     bp.WhitelistDeployKeys,
+		EnableMergeWhitelist:        bp.EnableMergeWhitelist,
+		MergeWhitelistUsernames:     mergeWhitelistUsernames,
+		MergeWhitelistTeams:         mergeWhitelistTeams,
+		EnableStatusCheck:           bp.EnableStatusCheck,
+		StatusCheckContexts:         bp.StatusCheckContexts,
+		RequiredApprovals:           bp.RequiredApprovals,
+		EnableApprovalsWhitelist:    bp.EnableApprovalsWhitelist,
+		ApprovalsWhitelistUsernames: approvalsWhitelistUsernames,
+		ApprovalsWhitelistTeams:     approvalsWhitelistTeams,
+		BlockOnRejectedReviews:      bp.BlockOnRejectedReviews,
+		DismissStaleApprovals:       bp.DismissStaleApprovals,
+		RequireSignedCommits:        bp.RequireSignedCommits,
+		Created:                     bp.CreatedUnix.AsTime(),
+		Updated:                     bp.UpdatedUnix.AsTime(),
 	}
 }
 
diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go
index 42bb763893..f8c9290548 100644
--- a/modules/structs/repo_branch.go
+++ b/modules/structs/repo_branch.go
@@ -4,14 +4,88 @@
 
 package structs
 
+import (
+	"time"
+)
+
 // Branch represents a repository branch
 type Branch struct {
-	Name                string         `json:"name"`
-	Commit              *PayloadCommit `json:"commit"`
-	Protected           bool           `json:"protected"`
-	RequiredApprovals   int64          `json:"required_approvals"`
-	EnableStatusCheck   bool           `json:"enable_status_check"`
-	StatusCheckContexts []string       `json:"status_check_contexts"`
-	UserCanPush         bool           `json:"user_can_push"`
-	UserCanMerge        bool           `json:"user_can_merge"`
+	Name                          string         `json:"name"`
+	Commit                        *PayloadCommit `json:"commit"`
+	Protected                     bool           `json:"protected"`
+	RequiredApprovals             int64          `json:"required_approvals"`
+	EnableStatusCheck             bool           `json:"enable_status_check"`
+	StatusCheckContexts           []string       `json:"status_check_contexts"`
+	UserCanPush                   bool           `json:"user_can_push"`
+	UserCanMerge                  bool           `json:"user_can_merge"`
+	EffectiveBranchProtectionName string         `json:"effective_branch_protection_name"`
+}
+
+// BranchProtection represents a branch protection for a repository
+type BranchProtection struct {
+	BranchName                  string   `json:"branch_name"`
+	EnablePush                  bool     `json:"enable_push"`
+	EnablePushWhitelist         bool     `json:"enable_push_whitelist"`
+	PushWhitelistUsernames      []string `json:"push_whitelist_usernames"`
+	PushWhitelistTeams          []string `json:"push_whitelist_teams"`
+	PushWhitelistDeployKeys     bool     `json:"push_whitelist_deploy_keys"`
+	EnableMergeWhitelist        bool     `json:"enable_merge_whitelist"`
+	MergeWhitelistUsernames     []string `json:"merge_whitelist_usernames"`
+	MergeWhitelistTeams         []string `json:"merge_whitelist_teams"`
+	EnableStatusCheck           bool     `json:"enable_status_check"`
+	StatusCheckContexts         []string `json:"status_check_contexts"`
+	RequiredApprovals           int64    `json:"required_approvals"`
+	EnableApprovalsWhitelist    bool     `json:"enable_approvals_whitelist"`
+	ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
+	ApprovalsWhitelistTeams     []string `json:"approvals_whitelist_teams"`
+	BlockOnRejectedReviews      bool     `json:"block_on_rejected_reviews"`
+	DismissStaleApprovals       bool     `json:"dismiss_stale_approvals"`
+	RequireSignedCommits        bool     `json:"require_signed_commits"`
+	// swagger:strfmt date-time
+	Created time.Time `json:"created_at"`
+	// swagger:strfmt date-time
+	Updated time.Time `json:"updated_at"`
+}
+
+// CreateBranchProtectionOption options for creating a branch protection
+type CreateBranchProtectionOption struct {
+	BranchName                  string   `json:"branch_name"`
+	EnablePush                  bool     `json:"enable_push"`
+	EnablePushWhitelist         bool     `json:"enable_push_whitelist"`
+	PushWhitelistUsernames      []string `json:"push_whitelist_usernames"`
+	PushWhitelistTeams          []string `json:"push_whitelist_teams"`
+	PushWhitelistDeployKeys     bool     `json:"push_whitelist_deploy_keys"`
+	EnableMergeWhitelist        bool     `json:"enable_merge_whitelist"`
+	MergeWhitelistUsernames     []string `json:"merge_whitelist_usernames"`
+	MergeWhitelistTeams         []string `json:"merge_whitelist_teams"`
+	EnableStatusCheck           bool     `json:"enable_status_check"`
+	StatusCheckContexts         []string `json:"status_check_contexts"`
+	RequiredApprovals           int64    `json:"required_approvals"`
+	EnableApprovalsWhitelist    bool     `json:"enable_approvals_whitelist"`
+	ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
+	ApprovalsWhitelistTeams     []string `json:"approvals_whitelist_teams"`
+	BlockOnRejectedReviews      bool     `json:"block_on_rejected_reviews"`
+	DismissStaleApprovals       bool     `json:"dismiss_stale_approvals"`
+	RequireSignedCommits        bool     `json:"require_signed_commits"`
+}
+
+// EditBranchProtectionOption options for editing a branch protection
+type EditBranchProtectionOption struct {
+	EnablePush                  *bool    `json:"enable_push"`
+	EnablePushWhitelist         *bool    `json:"enable_push_whitelist"`
+	PushWhitelistUsernames      []string `json:"push_whitelist_usernames"`
+	PushWhitelistTeams          []string `json:"push_whitelist_teams"`
+	PushWhitelistDeployKeys     *bool    `json:"push_whitelist_deploy_keys"`
+	EnableMergeWhitelist        *bool    `json:"enable_merge_whitelist"`
+	MergeWhitelistUsernames     []string `json:"merge_whitelist_usernames"`
+	MergeWhitelistTeams         []string `json:"merge_whitelist_teams"`
+	EnableStatusCheck           *bool    `json:"enable_status_check"`
+	StatusCheckContexts         []string `json:"status_check_contexts"`
+	RequiredApprovals           *int64   `json:"required_approvals"`
+	EnableApprovalsWhitelist    *bool    `json:"enable_approvals_whitelist"`
+	ApprovalsWhitelistUsernames []string `json:"approvals_whitelist_username"`
+	ApprovalsWhitelistTeams     []string `json:"approvals_whitelist_teams"`
+	BlockOnRejectedReviews      *bool    `json:"block_on_rejected_reviews"`
+	DismissStaleApprovals       *bool    `json:"dismiss_stale_approvals"`
+	RequireSignedCommits        *bool    `json:"require_signed_commits"`
 }
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 0a352f6e46..0ddf57b743 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -656,6 +656,15 @@ func RegisterRoutes(m *macaron.Macaron) {
 					m.Get("", repo.ListBranches)
 					m.Get("/*", context.RepoRefByType(context.RepoRefBranch), repo.GetBranch)
 				}, reqRepoReader(models.UnitTypeCode))
+				m.Group("/branch_protections", func() {
+					m.Get("", repo.ListBranchProtections)
+					m.Post("", bind(api.CreateBranchProtectionOption{}), repo.CreateBranchProtection)
+					m.Group("/:name", func() {
+						m.Get("", repo.GetBranchProtection)
+						m.Patch("", bind(api.EditBranchProtectionOption{}), repo.EditBranchProtection)
+						m.Delete("", repo.DeleteBranchProtection)
+					})
+				}, reqToken(), reqAdmin())
 				m.Group("/tags", func() {
 					m.Get("", repo.ListTags)
 				}, reqRepoReader(models.UnitTypeCode), context.ReferencesGitRepo(true))
diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go
index 8f8ca15877..fccfc2bfe1 100644
--- a/routers/api/v1/repo/branch.go
+++ b/routers/api/v1/repo/branch.go
@@ -8,6 +8,7 @@ package repo
 import (
 	"net/http"
 
+	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
 	"code.gitea.io/gitea/modules/git"
@@ -71,7 +72,7 @@ func GetBranch(ctx *context.APIContext) {
 		return
 	}
 
-	ctx.JSON(http.StatusOK, convert.ToBranch(ctx.Repo.Repository, branch, c, branchProtection, ctx.User))
+	ctx.JSON(http.StatusOK, convert.ToBranch(ctx.Repo.Repository, branch, c, branchProtection, ctx.User, ctx.Repo.IsAdmin()))
 }
 
 // ListBranches list all the branches of a repository
@@ -114,8 +115,509 @@ func ListBranches(ctx *context.APIContext) {
 			ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err)
 			return
 		}
-		apiBranches[i] = convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.User)
+		apiBranches[i] = convert.ToBranch(ctx.Repo.Repository, branches[i], c, branchProtection, ctx.User, ctx.Repo.IsAdmin())
 	}
 
 	ctx.JSON(http.StatusOK, &apiBranches)
 }
+
+// GetBranchProtection gets a branch protection
+func GetBranchProtection(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection
+	// ---
+	// summary: Get a specific branch protection for the repository
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// - name: name
+	//   in: path
+	//   description: name of protected branch
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/BranchProtection"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	repo := ctx.Repo.Repository
+	bpName := ctx.Params(":name")
+	bp, err := models.GetProtectedBranchBy(repo.ID, bpName)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
+		return
+	}
+	if bp == nil || bp.RepoID != repo.ID {
+		ctx.NotFound()
+		return
+	}
+
+	ctx.JSON(http.StatusOK, convert.ToBranchProtection(bp))
+}
+
+// ListBranchProtections list branch protections for a repo
+func ListBranchProtections(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/branch_protections repository repoListBranchProtection
+	// ---
+	// summary: List branch protections for a repository
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/BranchProtectionList"
+
+	repo := ctx.Repo.Repository
+	bps, err := repo.GetProtectedBranches()
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetProtectedBranches", err)
+		return
+	}
+	apiBps := make([]*api.BranchProtection, len(bps))
+	for i := range bps {
+		apiBps[i] = convert.ToBranchProtection(bps[i])
+	}
+
+	ctx.JSON(http.StatusOK, apiBps)
+}
+
+// CreateBranchProtection creates a branch protection for a repo
+func CreateBranchProtection(ctx *context.APIContext, form api.CreateBranchProtectionOption) {
+	// swagger:operation POST /repos/{owner}/{repo}/branch_protections repository repoCreateBranchProtection
+	// ---
+	// summary: Create a branch protections for a repository
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/CreateBranchProtectionOption"
+	// responses:
+	//   "201":
+	//     "$ref": "#/responses/BranchProtection"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+
+	repo := ctx.Repo.Repository
+
+	// Currently protection must match an actual branch
+	if !git.IsBranchExist(ctx.Repo.Repository.RepoPath(), form.BranchName) {
+		ctx.NotFound()
+		return
+	}
+
+	protectBranch, err := models.GetProtectedBranchBy(repo.ID, form.BranchName)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetProtectBranchOfRepoByName", err)
+		return
+	} else if protectBranch != nil {
+		ctx.Error(http.StatusForbidden, "Create branch protection", "Branch protection already exist")
+		return
+	}
+
+	var requiredApprovals int64
+	if form.RequiredApprovals > 0 {
+		requiredApprovals = form.RequiredApprovals
+	}
+
+	whitelistUsers, err := models.GetUserIDsByNames(form.PushWhitelistUsernames, false)
+	if err != nil {
+		if models.IsErrUserNotExist(err) {
+			ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
+			return
+		}
+		ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
+		return
+	}
+	mergeWhitelistUsers, err := models.GetUserIDsByNames(form.MergeWhitelistUsernames, false)
+	if err != nil {
+		if models.IsErrUserNotExist(err) {
+			ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
+			return
+		}
+		ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
+		return
+	}
+	approvalsWhitelistUsers, err := models.GetUserIDsByNames(form.ApprovalsWhitelistUsernames, false)
+	if err != nil {
+		if models.IsErrUserNotExist(err) {
+			ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
+			return
+		}
+		ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
+		return
+	}
+	var whitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
+	if repo.Owner.IsOrganization() {
+		whitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.PushWhitelistTeams, false)
+		if err != nil {
+			if models.IsErrTeamNotExist(err) {
+				ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
+				return
+			}
+			ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
+			return
+		}
+		mergeWhitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.MergeWhitelistTeams, false)
+		if err != nil {
+			if models.IsErrTeamNotExist(err) {
+				ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
+				return
+			}
+			ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
+			return
+		}
+		approvalsWhitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.ApprovalsWhitelistTeams, false)
+		if err != nil {
+			if models.IsErrTeamNotExist(err) {
+				ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
+				return
+			}
+			ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
+			return
+		}
+	}
+
+	protectBranch = &models.ProtectedBranch{
+		RepoID:                   ctx.Repo.Repository.ID,
+		BranchName:               form.BranchName,
+		CanPush:                  form.EnablePush,
+		EnableWhitelist:          form.EnablePush && form.EnablePushWhitelist,
+		EnableMergeWhitelist:     form.EnableMergeWhitelist,
+		WhitelistDeployKeys:      form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys,
+		EnableStatusCheck:        form.EnableStatusCheck,
+		StatusCheckContexts:      form.StatusCheckContexts,
+		EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
+		RequiredApprovals:        requiredApprovals,
+		BlockOnRejectedReviews:   form.BlockOnRejectedReviews,
+		DismissStaleApprovals:    form.DismissStaleApprovals,
+		RequireSignedCommits:     form.RequireSignedCommits,
+	}
+
+	err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
+		UserIDs:          whitelistUsers,
+		TeamIDs:          whitelistTeams,
+		MergeUserIDs:     mergeWhitelistUsers,
+		MergeTeamIDs:     mergeWhitelistTeams,
+		ApprovalsUserIDs: approvalsWhitelistUsers,
+		ApprovalsTeamIDs: approvalsWhitelistTeams,
+	})
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "UpdateProtectBranch", err)
+		return
+	}
+
+	// Reload from db to get all whitelists
+	bp, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, form.BranchName)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
+		return
+	}
+	if bp == nil || bp.RepoID != ctx.Repo.Repository.ID {
+		ctx.Error(http.StatusInternalServerError, "New branch protection not found", err)
+		return
+	}
+
+	ctx.JSON(http.StatusCreated, convert.ToBranchProtection(bp))
+
+}
+
+// EditBranchProtection edits a branch protection for a repo
+func EditBranchProtection(ctx *context.APIContext, form api.EditBranchProtectionOption) {
+	// swagger:operation PATCH /repos/{owner}/{repo}/branch_protections/{name} repository repoEditBranchProtection
+	// ---
+	// summary: Edit a branch protections for a repository. Only fields that are set will be changed
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// - name: name
+	//   in: path
+	//   description: name of protected branch
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/EditBranchProtectionOption"
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/BranchProtection"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	//   "422":
+	//     "$ref": "#/responses/validationError"
+
+	repo := ctx.Repo.Repository
+	bpName := ctx.Params(":name")
+	protectBranch, err := models.GetProtectedBranchBy(repo.ID, bpName)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
+		return
+	}
+	if protectBranch == nil || protectBranch.RepoID != repo.ID {
+		ctx.NotFound()
+		return
+	}
+
+	if form.EnablePush != nil {
+		if !*form.EnablePush {
+			protectBranch.CanPush = false
+			protectBranch.EnableWhitelist = false
+			protectBranch.WhitelistDeployKeys = false
+		} else {
+			protectBranch.CanPush = true
+			if form.EnablePushWhitelist != nil {
+				if !*form.EnablePushWhitelist {
+					protectBranch.EnableWhitelist = false
+					protectBranch.WhitelistDeployKeys = false
+				} else {
+					protectBranch.EnableWhitelist = true
+					if form.PushWhitelistDeployKeys != nil {
+						protectBranch.WhitelistDeployKeys = *form.PushWhitelistDeployKeys
+					}
+				}
+			}
+		}
+	}
+
+	if form.EnableMergeWhitelist != nil {
+		protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist
+	}
+
+	if form.EnableStatusCheck != nil {
+		protectBranch.EnableStatusCheck = *form.EnableStatusCheck
+	}
+	if protectBranch.EnableStatusCheck {
+		protectBranch.StatusCheckContexts = form.StatusCheckContexts
+	}
+
+	if form.RequiredApprovals != nil && *form.RequiredApprovals >= 0 {
+		protectBranch.RequiredApprovals = *form.RequiredApprovals
+	}
+
+	if form.EnableApprovalsWhitelist != nil {
+		protectBranch.EnableApprovalsWhitelist = *form.EnableApprovalsWhitelist
+	}
+
+	if form.BlockOnRejectedReviews != nil {
+		protectBranch.BlockOnRejectedReviews = *form.BlockOnRejectedReviews
+	}
+
+	if form.DismissStaleApprovals != nil {
+		protectBranch.DismissStaleApprovals = *form.DismissStaleApprovals
+	}
+
+	if form.RequireSignedCommits != nil {
+		protectBranch.RequireSignedCommits = *form.RequireSignedCommits
+	}
+
+	var whitelistUsers []int64
+	if form.PushWhitelistUsernames != nil {
+		whitelistUsers, err = models.GetUserIDsByNames(form.PushWhitelistUsernames, false)
+		if err != nil {
+			if models.IsErrUserNotExist(err) {
+				ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
+				return
+			}
+			ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
+			return
+		}
+	} else {
+		whitelistUsers = protectBranch.WhitelistUserIDs
+	}
+	var mergeWhitelistUsers []int64
+	if form.MergeWhitelistUsernames != nil {
+		mergeWhitelistUsers, err = models.GetUserIDsByNames(form.MergeWhitelistUsernames, false)
+		if err != nil {
+			if models.IsErrUserNotExist(err) {
+				ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
+				return
+			}
+			ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
+			return
+		}
+	} else {
+		mergeWhitelistUsers = protectBranch.MergeWhitelistUserIDs
+	}
+	var approvalsWhitelistUsers []int64
+	if form.ApprovalsWhitelistUsernames != nil {
+		approvalsWhitelistUsers, err = models.GetUserIDsByNames(form.ApprovalsWhitelistUsernames, false)
+		if err != nil {
+			if models.IsErrUserNotExist(err) {
+				ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
+				return
+			}
+			ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
+			return
+		}
+	} else {
+		approvalsWhitelistUsers = protectBranch.ApprovalsWhitelistUserIDs
+	}
+
+	var whitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
+	if repo.Owner.IsOrganization() {
+		if form.PushWhitelistTeams != nil {
+			whitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.PushWhitelistTeams, false)
+			if err != nil {
+				if models.IsErrTeamNotExist(err) {
+					ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
+					return
+				}
+				ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
+				return
+			}
+		} else {
+			whitelistTeams = protectBranch.WhitelistTeamIDs
+		}
+		if form.MergeWhitelistTeams != nil {
+			mergeWhitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.MergeWhitelistTeams, false)
+			if err != nil {
+				if models.IsErrTeamNotExist(err) {
+					ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
+					return
+				}
+				ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
+				return
+			}
+		} else {
+			mergeWhitelistTeams = protectBranch.MergeWhitelistTeamIDs
+		}
+		if form.ApprovalsWhitelistTeams != nil {
+			approvalsWhitelistTeams, err = models.GetTeamIDsByNames(repo.OwnerID, form.ApprovalsWhitelistTeams, false)
+			if err != nil {
+				if models.IsErrTeamNotExist(err) {
+					ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
+					return
+				}
+				ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
+				return
+			}
+		} else {
+			approvalsWhitelistTeams = protectBranch.ApprovalsWhitelistTeamIDs
+		}
+	}
+
+	err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{
+		UserIDs:          whitelistUsers,
+		TeamIDs:          whitelistTeams,
+		MergeUserIDs:     mergeWhitelistUsers,
+		MergeTeamIDs:     mergeWhitelistTeams,
+		ApprovalsUserIDs: approvalsWhitelistUsers,
+		ApprovalsTeamIDs: approvalsWhitelistTeams,
+	})
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "UpdateProtectBranch", err)
+		return
+	}
+
+	// Reload from db to ensure get all whitelists
+	bp, err := models.GetProtectedBranchBy(repo.ID, bpName)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err)
+		return
+	}
+	if bp == nil || bp.RepoID != ctx.Repo.Repository.ID {
+		ctx.Error(http.StatusInternalServerError, "New branch protection not found", err)
+		return
+	}
+
+	ctx.JSON(http.StatusOK, convert.ToBranchProtection(bp))
+}
+
+// DeleteBranchProtection deletes a branch protection for a repo
+func DeleteBranchProtection(ctx *context.APIContext) {
+	// swagger:operation DELETE /repos/{owner}/{repo}/branch_protections/{name} repository repoDeleteBranchProtection
+	// ---
+	// summary: Delete a specific branch protection for the repository
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// - name: name
+	//   in: path
+	//   description: name of protected branch
+	//   type: string
+	//   required: true
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+
+	repo := ctx.Repo.Repository
+	bpName := ctx.Params(":name")
+	bp, err := models.GetProtectedBranchBy(repo.ID, bpName)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
+		return
+	}
+	if bp == nil || bp.RepoID != repo.ID {
+		ctx.NotFound()
+		return
+	}
+
+	if err := ctx.Repo.Repository.DeleteProtectedBranch(bp.ID); err != nil {
+		ctx.Error(http.StatusInternalServerError, "DeleteProtectedBranch", err)
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index ab697811d0..679b4aa708 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -128,4 +128,10 @@ type swaggerParameterBodies struct {
 
 	// in:body
 	EditReactionOption api.EditReactionOption
+
+	// in:body
+	CreateBranchProtectionOption api.CreateBranchProtectionOption
+
+	// in:body
+	EditBranchProtectionOption api.EditBranchProtectionOption
 }
diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go
index 4ac5c6d2d5..2a657f3122 100644
--- a/routers/api/v1/swagger/repo.go
+++ b/routers/api/v1/swagger/repo.go
@@ -36,6 +36,20 @@ type swaggerResponseBranchList struct {
 	Body []api.Branch `json:"body"`
 }
 
+// BranchProtection
+// swagger:response BranchProtection
+type swaggerResponseBranchProtection struct {
+	// in:body
+	Body api.BranchProtection `json:"body"`
+}
+
+// BranchProtectionList
+// swagger:response BranchProtectionList
+type swaggerResponseBranchProtectionList struct {
+	// in:body
+	Body []api.BranchProtection `json:"body"`
+}
+
 // TagList
 // swagger:response TagList
 type swaggerResponseTagList struct {
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index e6a5189928..b52145a0a9 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -1797,6 +1797,227 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/branch_protections": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "List branch protections for a repository",
+        "operationId": "repoListBranchProtection",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/BranchProtectionList"
+          }
+        }
+      },
+      "post": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Create a branch protections for a repository",
+        "operationId": "repoCreateBranchProtection",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/CreateBranchProtectionOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "$ref": "#/responses/BranchProtection"
+          },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      }
+    },
+    "/repos/{owner}/{repo}/branch_protections/{name}": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Get a specific branch protection for the repository",
+        "operationId": "repoGetBranchProtection",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of protected branch",
+            "name": "name",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/BranchProtection"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "delete": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Delete a specific branch protection for the repository",
+        "operationId": "repoDeleteBranchProtection",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of protected branch",
+            "name": "name",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      },
+      "patch": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Edit a branch protections for a repository. Only fields that are set will be changed",
+        "operationId": "repoEditBranchProtection",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of protected branch",
+            "name": "name",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/EditBranchProtectionOption"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/BranchProtection"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          },
+          "422": {
+            "$ref": "#/responses/validationError"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/branches": {
       "get": {
         "produces": [
@@ -9394,6 +9615,10 @@
         "commit": {
           "$ref": "#/definitions/PayloadCommit"
         },
+        "effective_branch_protection_name": {
+          "type": "string",
+          "x-go-name": "EffectiveBranchProtectionName"
+        },
         "enable_status_check": {
           "type": "boolean",
           "x-go-name": "EnableStatusCheck"
@@ -9429,6 +9654,117 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "BranchProtection": {
+      "description": "BranchProtection represents a branch protection for a repository",
+      "type": "object",
+      "properties": {
+        "approvals_whitelist_teams": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "ApprovalsWhitelistTeams"
+        },
+        "approvals_whitelist_username": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "ApprovalsWhitelistUsernames"
+        },
+        "block_on_rejected_reviews": {
+          "type": "boolean",
+          "x-go-name": "BlockOnRejectedReviews"
+        },
+        "branch_name": {
+          "type": "string",
+          "x-go-name": "BranchName"
+        },
+        "created_at": {
+          "type": "string",
+          "format": "date-time",
+          "x-go-name": "Created"
+        },
+        "dismiss_stale_approvals": {
+          "type": "boolean",
+          "x-go-name": "DismissStaleApprovals"
+        },
+        "enable_approvals_whitelist": {
+          "type": "boolean",
+          "x-go-name": "EnableApprovalsWhitelist"
+        },
+        "enable_merge_whitelist": {
+          "type": "boolean",
+          "x-go-name": "EnableMergeWhitelist"
+        },
+        "enable_push": {
+          "type": "boolean",
+          "x-go-name": "EnablePush"
+        },
+        "enable_push_whitelist": {
+          "type": "boolean",
+          "x-go-name": "EnablePushWhitelist"
+        },
+        "enable_status_check": {
+          "type": "boolean",
+          "x-go-name": "EnableStatusCheck"
+        },
+        "merge_whitelist_teams": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "MergeWhitelistTeams"
+        },
+        "merge_whitelist_usernames": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "MergeWhitelistUsernames"
+        },
+        "push_whitelist_deploy_keys": {
+          "type": "boolean",
+          "x-go-name": "PushWhitelistDeployKeys"
+        },
+        "push_whitelist_teams": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "PushWhitelistTeams"
+        },
+        "push_whitelist_usernames": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "PushWhitelistUsernames"
+        },
+        "require_signed_commits": {
+          "type": "boolean",
+          "x-go-name": "RequireSignedCommits"
+        },
+        "required_approvals": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "RequiredApprovals"
+        },
+        "status_check_contexts": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "StatusCheckContexts"
+        },
+        "updated_at": {
+          "type": "string",
+          "format": "date-time",
+          "x-go-name": "Updated"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "Comment": {
       "description": "Comment represents a comment on a commit or issue",
       "type": "object",
@@ -9634,6 +9970,107 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "CreateBranchProtectionOption": {
+      "description": "CreateBranchProtectionOption options for creating a branch protection",
+      "type": "object",
+      "properties": {
+        "approvals_whitelist_teams": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "ApprovalsWhitelistTeams"
+        },
+        "approvals_whitelist_username": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "ApprovalsWhitelistUsernames"
+        },
+        "block_on_rejected_reviews": {
+          "type": "boolean",
+          "x-go-name": "BlockOnRejectedReviews"
+        },
+        "branch_name": {
+          "type": "string",
+          "x-go-name": "BranchName"
+        },
+        "dismiss_stale_approvals": {
+          "type": "boolean",
+          "x-go-name": "DismissStaleApprovals"
+        },
+        "enable_approvals_whitelist": {
+          "type": "boolean",
+          "x-go-name": "EnableApprovalsWhitelist"
+        },
+        "enable_merge_whitelist": {
+          "type": "boolean",
+          "x-go-name": "EnableMergeWhitelist"
+        },
+        "enable_push": {
+          "type": "boolean",
+          "x-go-name": "EnablePush"
+        },
+        "enable_push_whitelist": {
+          "type": "boolean",
+          "x-go-name": "EnablePushWhitelist"
+        },
+        "enable_status_check": {
+          "type": "boolean",
+          "x-go-name": "EnableStatusCheck"
+        },
+        "merge_whitelist_teams": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "MergeWhitelistTeams"
+        },
+        "merge_whitelist_usernames": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "MergeWhitelistUsernames"
+        },
+        "push_whitelist_deploy_keys": {
+          "type": "boolean",
+          "x-go-name": "PushWhitelistDeployKeys"
+        },
+        "push_whitelist_teams": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "PushWhitelistTeams"
+        },
+        "push_whitelist_usernames": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "PushWhitelistUsernames"
+        },
+        "require_signed_commits": {
+          "type": "boolean",
+          "x-go-name": "RequireSignedCommits"
+        },
+        "required_approvals": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "RequiredApprovals"
+        },
+        "status_check_contexts": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "StatusCheckContexts"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "CreateEmailOption": {
       "description": "CreateEmailOption options when creating email addresses",
       "type": "object",
@@ -10318,6 +10755,103 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "EditBranchProtectionOption": {
+      "description": "EditBranchProtectionOption options for editing a branch protection",
+      "type": "object",
+      "properties": {
+        "approvals_whitelist_teams": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "ApprovalsWhitelistTeams"
+        },
+        "approvals_whitelist_username": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "ApprovalsWhitelistUsernames"
+        },
+        "block_on_rejected_reviews": {
+          "type": "boolean",
+          "x-go-name": "BlockOnRejectedReviews"
+        },
+        "dismiss_stale_approvals": {
+          "type": "boolean",
+          "x-go-name": "DismissStaleApprovals"
+        },
+        "enable_approvals_whitelist": {
+          "type": "boolean",
+          "x-go-name": "EnableApprovalsWhitelist"
+        },
+        "enable_merge_whitelist": {
+          "type": "boolean",
+          "x-go-name": "EnableMergeWhitelist"
+        },
+        "enable_push": {
+          "type": "boolean",
+          "x-go-name": "EnablePush"
+        },
+        "enable_push_whitelist": {
+          "type": "boolean",
+          "x-go-name": "EnablePushWhitelist"
+        },
+        "enable_status_check": {
+          "type": "boolean",
+          "x-go-name": "EnableStatusCheck"
+        },
+        "merge_whitelist_teams": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "MergeWhitelistTeams"
+        },
+        "merge_whitelist_usernames": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "MergeWhitelistUsernames"
+        },
+        "push_whitelist_deploy_keys": {
+          "type": "boolean",
+          "x-go-name": "PushWhitelistDeployKeys"
+        },
+        "push_whitelist_teams": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "PushWhitelistTeams"
+        },
+        "push_whitelist_usernames": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "PushWhitelistUsernames"
+        },
+        "require_signed_commits": {
+          "type": "boolean",
+          "x-go-name": "RequireSignedCommits"
+        },
+        "required_approvals": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "RequiredApprovals"
+        },
+        "status_check_contexts": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          },
+          "x-go-name": "StatusCheckContexts"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "EditDeadlineOption": {
       "description": "EditDeadlineOption options for creating a deadline",
       "type": "object",
@@ -12880,6 +13414,21 @@
         }
       }
     },
+    "BranchProtection": {
+      "description": "BranchProtection",
+      "schema": {
+        "$ref": "#/definitions/BranchProtection"
+      }
+    },
+    "BranchProtectionList": {
+      "description": "BranchProtectionList",
+      "schema": {
+        "type": "array",
+        "items": {
+          "$ref": "#/definitions/BranchProtection"
+        }
+      }
+    },
     "Comment": {
       "description": "Comment",
       "schema": {
@@ -13410,7 +13959,7 @@
     "parameterBodies": {
       "description": "parameterBodies",
       "schema": {
-        "$ref": "#/definitions/EditReactionOption"
+        "$ref": "#/definitions/EditBranchProtectionOption"
       }
     },
     "redirect": {