summaryrefslogtreecommitdiffstats
path: root/routers
diff options
context:
space:
mode:
authorDavid Svantesson <davidsvantesson@gmail.com>2020-02-13 00:19:35 +0100
committerGitHub <noreply@github.com>2020-02-12 23:19:35 +0000
commit9ff4e1d2d9636ea8aa328427f1d31c962221263e (patch)
treeb0df096e3885a6f05c26959f784cca0ce6a9763c /routers
parent908f8952be3ba7a4e4c32b0fd0dab5eb08ca8dd4 (diff)
downloadgitea-9ff4e1d2d9636ea8aa328427f1d31c962221263e.tar.gz
gitea-9ff4e1d2d9636ea8aa328427f1d31c962221263e.zip
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>
Diffstat (limited to 'routers')
-rw-r--r--routers/api/v1/api.go9
-rw-r--r--routers/api/v1/repo/branch.go506
-rw-r--r--routers/api/v1/swagger/options.go6
-rw-r--r--routers/api/v1/swagger/repo.go14
4 files changed, 533 insertions, 2 deletions
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 {