From b83b17e08705f50f0a0d17ad7dd6fbac0f247e65 Mon Sep 17 00:00:00 2001 From: Quan Anh Tong Date: Fri, 31 May 2019 17:46:01 +0700 Subject: [PATCH 1/2] add API endpoints to get/update/delete protected branch --- modules/structs/repo_branch.go | 5 +- routers/api/v1/api.go | 5 + routers/api/v1/convert/convert.go | 9 +- routers/api/v1/repo/branch.go | 214 ++++++++++++++++++++++++++++-- routers/api/v1/swagger/options.go | 3 + 5 files changed, 222 insertions(+), 14 deletions(-) diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go index a6ae6c16633d5..15163964d8b58 100644 --- a/modules/structs/repo_branch.go +++ b/modules/structs/repo_branch.go @@ -6,6 +6,7 @@ package structs // Branch represents a repository branch type Branch struct { - Name string `json:"name"` - Commit *PayloadCommit `json:"commit"` + Name string `json:"name"` + Commit *PayloadCommit `json:"commit"` + Protected bool `json:"protected"` } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 8170b79dd220d..eb70e580c74fb 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -641,6 +641,11 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/branches", func() { m.Get("", repo.ListBranches) m.Get("/*", context.RepoRefByType(context.RepoRefBranch), repo.GetBranch) + m.Group("/:branch/protection", func() { + m.Combo("").Get(repo.GetProtectedBranchBy). + Put(bind(auth.ProtectBranchForm{}), repo.UpdateProtectBranch). + Delete(repo.DeleteProtectedBranch) + }, reqToken(), reqAdmin()) }, reqRepoReader(models.UnitTypeCode)) m.Group("/tags", func() { m.Get("", repo.ListTags) diff --git a/routers/api/v1/convert/convert.go b/routers/api/v1/convert/convert.go index f1cb23de4378f..1ed455438bdfd 100644 --- a/routers/api/v1/convert/convert.go +++ b/routers/api/v1/convert/convert.go @@ -27,11 +27,12 @@ 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) *api.Branch { +// ToBranch convert a commit and branch to an api.Branch +func ToBranch(repo *models.Repository, b *git.Branch, c *git.Commit, protected bool) *api.Branch { return &api.Branch{ - Name: b.Name, - Commit: ToCommit(repo, c), + Name: b.Name, + Commit: ToCommit(repo, c), + Protected: protected, } } diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 1aaae8723bde6..7e1af5006efef 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -6,6 +6,12 @@ package repo import ( + "net/http" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/routers/api/v1/convert" @@ -46,23 +52,30 @@ func GetBranch(ctx *context.APIContext) { ctx.NotFound() return } - branch, err := ctx.Repo.Repository.GetBranch(ctx.Repo.BranchName) + branchName := ctx.Repo.BranchName + branch, err := ctx.Repo.Repository.GetBranch(branchName) if err != nil { if git.IsErrBranchNotExist(err) { ctx.NotFound(err) } else { - ctx.Error(500, "GetBranch", err) + ctx.Error(http.StatusInternalServerError, "GetBranch", err) } return } + protected, err := ctx.Repo.Repository.IsProtectedBranch(branchName, ctx.Repo.Owner) + if err != nil { + ctx.Error(http.StatusInternalServerError, "IsProtectedBranch", err) + return + } + c, err := branch.GetCommit() if err != nil { - ctx.Error(500, "GetCommit", err) + ctx.Error(http.StatusInternalServerError, "GetCommit", err) return } - ctx.JSON(200, convert.ToBranch(ctx.Repo.Repository, branch, c)) + ctx.JSON(http.StatusOK, convert.ToBranch(ctx.Repo.Repository, branch, c, protected)) } // ListBranches list all the branches of a repository @@ -88,7 +101,7 @@ func ListBranches(ctx *context.APIContext) { // "$ref": "#/responses/BranchList" branches, err := ctx.Repo.Repository.GetBranches() if err != nil { - ctx.Error(500, "GetBranches", err) + ctx.Error(http.StatusInternalServerError, "GetBranches", err) return } @@ -96,11 +109,196 @@ func ListBranches(ctx *context.APIContext) { for i := range branches { c, err := branches[i].GetCommit() if err != nil { - ctx.Error(500, "GetCommit", err) + ctx.Error(http.StatusInternalServerError, "GetCommit", err) + return + } + + protected, err := ctx.Repo.Repository.IsProtectedBranch(branches[i].Name, ctx.Repo.Owner) + if err != nil { + ctx.Error(http.StatusInternalServerError, "IsProtectedBranch", err) + return + } + + apiBranches[i] = convert.ToBranch(ctx.Repo.Repository, branches[i], c, protected) + } + + ctx.JSON(http.StatusOK, &apiBranches) +} + +// GetProtectedBranchBy getting protected branch by ID/Name +func GetProtectedBranchBy(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/branches/{branch}/protection repository repoGetBranchProtection + // --- + // summary: Retrieve a specific branch protection from 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 + // - name: branch + // in: path + // description: branch to get + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/Branch" + protectBranch, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, ctx.Params(":branch")) + if err != nil { + if !git.IsErrBranchNotExist(err) { + ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err) return } - apiBranches[i] = convert.ToBranch(ctx.Repo.Repository, branches[i], c) } + ctx.JSON(http.StatusOK, protectBranch) +} - ctx.JSON(200, &apiBranches) +// UpdateProtectBranch saves branch protection options of repository. +// If ID is 0, it creates a new record. Otherwise, updates existing record. +// This function also performs check if whitelist user and team's IDs have been changed +// to avoid unnecessary whitelist delete and regenerate. +func UpdateProtectBranch(ctx *context.APIContext, f auth.ProtectBranchForm) { + // swagger:operation PUT /repos/{owner}/{repo}/branches/{branch}/protection repository repoUpdateProtectBranch + // --- + // summary: Update branch protection of 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 + // - name: branch + // in: path + // description: branch to update + // type: string + // required: true + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/ProtectBranchForm" + // responses: + // "200": + // "$ref": "#/responses/Branch" + branch := ctx.Params(":branch") + protectBranch, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branch) + if err != nil { + if !git.IsErrBranchNotExist(err) { + ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err) + return + } + } + + if f.Protected { + if protectBranch == nil { + protectBranch = &models.ProtectedBranch{ + RepoID: ctx.Repo.Repository.ID, + BranchName: branch, + } + } + + var whitelistUsers, whitelistTeams, mergeWhitelistUsers, mergeWhitelistTeams, approvalsWhitelistUsers, approvalsWhitelistTeams []int64 + protectBranch.EnableWhitelist = f.EnableWhitelist + if strings.TrimSpace(f.WhitelistUsers) != "" { + whitelistUsers, _ = base.StringsToInt64s(strings.Split(f.WhitelistUsers, ",")) + } + if strings.TrimSpace(f.WhitelistTeams) != "" { + whitelistTeams, _ = base.StringsToInt64s(strings.Split(f.WhitelistTeams, ",")) + } + protectBranch.EnableMergeWhitelist = f.EnableMergeWhitelist + if strings.TrimSpace(f.MergeWhitelistUsers) != "" { + mergeWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistUsers, ",")) + } + if strings.TrimSpace(f.MergeWhitelistTeams) != "" { + mergeWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.MergeWhitelistTeams, ",")) + } + protectBranch.RequiredApprovals = f.RequiredApprovals + if strings.TrimSpace(f.ApprovalsWhitelistUsers) != "" { + approvalsWhitelistUsers, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistUsers, ",")) + } + if strings.TrimSpace(f.ApprovalsWhitelistTeams) != "" { + approvalsWhitelistTeams, _ = base.StringsToInt64s(strings.Split(f.ApprovalsWhitelistTeams, ",")) + } + if err = models.UpdateProtectBranch(ctx.Repo.Repository, protectBranch, models.WhitelistOptions{ + UserIDs: whitelistUsers, + TeamIDs: whitelistTeams, + MergeUserIDs: mergeWhitelistUsers, + MergeTeamIDs: mergeWhitelistTeams, + ApprovalsUserIDs: approvalsWhitelistUsers, + ApprovalsTeamIDs: approvalsWhitelistTeams, + }); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateProtectBranch", err) + return + } + } else if protectBranch != nil { + if err := ctx.Repo.Repository.DeleteProtectedBranch(protectBranch.ID); err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteProtectedBranch", err) + return + } + } + ctx.JSON(http.StatusOK, protectBranch) +} + +// DeleteProtectedBranch removes ProtectedBranch relation between the user and repository. +func DeleteProtectedBranch(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/branches/{branch}/protection repository repoDeleteProtectedBranch + // --- + // summary: Remove branch protection from 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 + // - name: branch + // in: path + // description: branch to remove protection + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + branchName := ctx.Params(":branch") + protected, err := ctx.Repo.Repository.IsProtectedBranch(branchName, ctx.Repo.Owner) + if err != nil { + ctx.Error(http.StatusInternalServerError, "IsProtectedBranch", err) + return + } + if !protected { + ctx.Status(http.StatusNoContent) + return + } + protectBranch, err := models.GetProtectedBranchBy(ctx.Repo.Repository.ID, branchName) + if err != nil { + if !git.IsErrBranchNotExist(err) { + ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err) + return + } + } + if err = ctx.Repo.Repository.DeleteProtectedBranch(protectBranch.ID); err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteProtectedBranch", err) + } + ctx.Status(http.StatusNoContent) } diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index c1196eeb71581..e0c28bcf5ade9 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -86,6 +86,9 @@ type swaggerParameterBodies struct { // in:body CreateForkOption api.CreateForkOption + // in:body + ProtectBranchForm auth.ProtectBranchForm + // in:body CreateStatusOption api.CreateStatusOption From 03a243526fe02ba1ea265a4d9799906b5049976e Mon Sep 17 00:00:00 2001 From: Quan Anh Tong Date: Tue, 11 Jun 2019 12:25:42 +0700 Subject: [PATCH 2/2] make generate-swagger --- templates/swagger/v1_json.tmpl | 166 +++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 11f41611726d2..128cf6e03841f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1371,6 +1371,130 @@ } } }, + "/repos/{owner}/{repo}/branches/{branch}/protection": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Retrieve a specific branch protection from a 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": "branch to get", + "name": "branch", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Branch" + } + } + }, + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Update branch protection of a repository", + "operationId": "repoUpdateProtectBranch", + "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": "branch to update", + "name": "branch", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ProtectBranchForm" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/Branch" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Remove branch protection from a repository", + "operationId": "repoDeleteProtectedBranch", + "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": "branch to remove protection", + "name": "branch", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + } + } + } + }, "/repos/{owner}/{repo}/collaborators": { "get": { "produces": [ @@ -6941,6 +7065,10 @@ "name": { "type": "string", "x-go-name": "Name" + }, + "protected": { + "type": "boolean", + "x-go-name": "Protected" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -9115,6 +9243,44 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ProtectBranchForm": { + "description": "ProtectBranchForm form for changing protected branch settings", + "type": "object", + "properties": { + "ApprovalsWhitelistTeams": { + "type": "string" + }, + "ApprovalsWhitelistUsers": { + "type": "string" + }, + "EnableMergeWhitelist": { + "type": "boolean" + }, + "EnableWhitelist": { + "type": "boolean" + }, + "MergeWhitelistTeams": { + "type": "string" + }, + "MergeWhitelistUsers": { + "type": "string" + }, + "Protected": { + "type": "boolean" + }, + "RequiredApprovals": { + "type": "integer", + "format": "int64" + }, + "WhitelistTeams": { + "type": "string" + }, + "WhitelistUsers": { + "type": "string" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/auth" + }, "PublicKey": { "description": "PublicKey publickey is a user key to push code to repository", "type": "object",