mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-22 07:18:31 -04:00
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>
This commit is contained in:
parent
908f8952be
commit
9ff4e1d2d9
10 changed files with 1352 additions and 28 deletions
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue