1
1
mirror of https://github.com/go-gitea/gitea synced 2024-11-16 15:14:24 +00:00

Add tag protection via rest api #17862 (#31295)

Add tag protection manage via rest API.

---------

Co-authored-by: Alexander Kogay <kogay.a@citilink.ru>
Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
mzroot 2024-06-14 19:56:10 +03:00 committed by GitHub
parent 4e7b067a7f
commit d4e4226c3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 778 additions and 0 deletions

View File

@ -110,6 +110,19 @@ func GetProtectedTagByID(ctx context.Context, id int64) (*ProtectedTag, error) {
return tag, nil return tag, nil
} }
// GetProtectedTagByNamePattern gets protected tag by name_pattern
func GetProtectedTagByNamePattern(ctx context.Context, repoID int64, pattern string) (*ProtectedTag, error) {
tag := &ProtectedTag{NamePattern: pattern, RepoID: repoID}
has, err := db.GetEngine(ctx).Get(tag)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return tag, nil
}
// IsUserAllowedToControlTag checks if a user can control the specific tag. // IsUserAllowedToControlTag checks if a user can control the specific tag.
// It returns true if the tag name is not protected or the user is allowed to control it. // It returns true if the tag name is not protected or the user is allowed to control it.
func IsUserAllowedToControlTag(ctx context.Context, tags []*ProtectedTag, tagName string, userID int64) (bool, error) { func IsUserAllowedToControlTag(ctx context.Context, tags []*ProtectedTag, tagName string, userID int64) (bool, error) {

View File

@ -3,6 +3,8 @@
package structs package structs
import "time"
// Tag represents a repository tag // Tag represents a repository tag
type Tag struct { type Tag struct {
Name string `json:"name"` Name string `json:"name"`
@ -38,3 +40,29 @@ type CreateTagOption struct {
Message string `json:"message"` Message string `json:"message"`
Target string `json:"target"` Target string `json:"target"`
} }
// TagProtection represents a tag protection
type TagProtection struct {
ID int64 `json:"id"`
NamePattern string `json:"name_pattern"`
WhitelistUsernames []string `json:"whitelist_usernames"`
WhitelistTeams []string `json:"whitelist_teams"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
}
// CreateTagProtectionOption options for creating a tag protection
type CreateTagProtectionOption struct {
NamePattern string `json:"name_pattern"`
WhitelistUsernames []string `json:"whitelist_usernames"`
WhitelistTeams []string `json:"whitelist_teams"`
}
// EditTagProtectionOption options for editing a tag protection
type EditTagProtectionOption struct {
NamePattern *string `json:"name_pattern"`
WhitelistUsernames []string `json:"whitelist_usernames"`
WhitelistTeams []string `json:"whitelist_teams"`
}

View File

@ -1168,6 +1168,15 @@ func Routes() *web.Route {
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag) m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag)
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true)) }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
m.Group("/tag_protections", func() {
m.Combo("").Get(repo.ListTagProtection).
Post(bind(api.CreateTagProtectionOption{}), mustNotBeArchived, repo.CreateTagProtection)
m.Group("/{id}", func() {
m.Combo("").Get(repo.GetTagProtection).
Patch(bind(api.EditTagProtectionOption{}), mustNotBeArchived, repo.EditTagProtection).
Delete(repo.DeleteTagProtection)
})
}, reqToken(), reqAdmin())
m.Group("/actions", func() { m.Group("/actions", func() {
m.Get("/tasks", repo.ListActionTasks) m.Get("/tasks", repo.ListActionTasks)
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true)) }, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))

View File

@ -7,9 +7,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
@ -287,3 +291,349 @@ func DeleteTag(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
} }
// ListTagProtection lists tag protections for a repo
func ListTagProtection(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/tag_protections repository repoListTagProtection
// ---
// summary: List tag 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/TagProtectionList"
repo := ctx.Repo.Repository
pts, err := git_model.GetProtectedTags(ctx, repo.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTags", err)
return
}
apiPts := make([]*api.TagProtection, len(pts))
for i := range pts {
apiPts[i] = convert.ToTagProtection(ctx, pts[i], repo)
}
ctx.JSON(http.StatusOK, apiPts)
}
// GetTagProtection gets a tag protection
func GetTagProtection(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/tag_protections/{id} repository repoGetTagProtection
// ---
// summary: Get a specific tag 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: id
// in: path
// description: id of the tag protect to get
// type: integer
// required: true
// responses:
// "200":
// "$ref": "#/responses/TagProtection"
// "404":
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
id := ctx.ParamsInt64(":id")
pt, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || repo.ID != pt.RepoID {
ctx.NotFound()
return
}
ctx.JSON(http.StatusOK, convert.ToTagProtection(ctx, pt, repo))
}
// CreateTagProtection creates a tag protection for a repo
func CreateTagProtection(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/tag_protections repository repoCreateTagProtection
// ---
// summary: Create a tag 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/CreateTagProtectionOption"
// responses:
// "201":
// "$ref": "#/responses/TagProtection"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateTagProtectionOption)
repo := ctx.Repo.Repository
namePattern := strings.TrimSpace(form.NamePattern)
if namePattern == "" {
ctx.Error(http.StatusBadRequest, "name_pattern are empty", "name_pattern are empty")
return
}
if len(form.WhitelistUsernames) == 0 && len(form.WhitelistTeams) == 0 {
ctx.Error(http.StatusBadRequest, "both whitelist_usernames and whitelist_teams are empty", "both whitelist_usernames and whitelist_teams are empty")
return
}
pt, err := git_model.GetProtectedTagByNamePattern(ctx, repo.ID, namePattern)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectTagOfRepo", err)
return
} else if pt != nil {
ctx.Error(http.StatusForbidden, "Create tag protection", "Tag protection already exist")
return
}
var whitelistUsers, whitelistTeams []int64
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
return
}
ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
return
}
if repo.Owner.IsOrganization() {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
return
}
ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
return
}
}
protectTag := &git_model.ProtectedTag{
RepoID: repo.ID,
NamePattern: strings.TrimSpace(namePattern),
AllowlistUserIDs: whitelistUsers,
AllowlistTeamIDs: whitelistTeams,
}
if err := git_model.InsertProtectedTag(ctx, protectTag); err != nil {
ctx.Error(http.StatusInternalServerError, "InsertProtectedTag", err)
return
}
pt, err = git_model.GetProtectedTagByID(ctx, protectTag.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.Error(http.StatusInternalServerError, "New tag protection not found", err)
return
}
ctx.JSON(http.StatusCreated, convert.ToTagProtection(ctx, pt, repo))
}
// EditTagProtection edits a tag protection for a repo
func EditTagProtection(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/tag_protections/{id} repository repoEditTagProtection
// ---
// summary: Edit a tag 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: id
// in: path
// description: id of protected tag
// type: integer
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditTagProtectionOption"
// responses:
// "200":
// "$ref": "#/responses/TagProtection"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
repo := ctx.Repo.Repository
form := web.GetForm(ctx).(*api.EditTagProtectionOption)
id := ctx.ParamsInt64(":id")
pt, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.NotFound()
return
}
if form.NamePattern != nil {
pt.NamePattern = *form.NamePattern
}
var whitelistUsers, whitelistTeams []int64
if form.WhitelistTeams != nil {
if repo.Owner.IsOrganization() {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
return
}
ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
return
}
}
pt.AllowlistTeamIDs = whitelistTeams
}
if form.WhitelistUsernames != nil {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
return
}
ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
return
}
pt.AllowlistUserIDs = whitelistUsers
}
err = git_model.UpdateProtectedTag(ctx, pt)
if err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateProtectedTag", err)
return
}
pt, err = git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.Error(http.StatusInternalServerError, "New tag protection not found", "New tag protection not found")
return
}
ctx.JSON(http.StatusOK, convert.ToTagProtection(ctx, pt, repo))
}
// DeleteTagProtection
func DeleteTagProtection(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/tag_protections/{id} repository repoDeleteTagProtection
// ---
// summary: Delete a specific tag 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: id
// in: path
// description: id of protected tag
// type: integer
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
id := ctx.ParamsInt64(":id")
pt, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.NotFound()
return
}
err = git_model.DeleteProtectedTag(ctx, pt)
if err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteProtectedTag", err)
return
}
ctx.Status(http.StatusNoContent)
}

View File

@ -170,6 +170,12 @@ type swaggerParameterBodies struct {
// in:body // in:body
CreateTagOption api.CreateTagOption CreateTagOption api.CreateTagOption
// in:body
CreateTagProtectionOption api.CreateTagProtectionOption
// in:body
EditTagProtectionOption api.EditTagProtectionOption
// in:body // in:body
CreateAccessTokenOption api.CreateAccessTokenOption CreateAccessTokenOption api.CreateAccessTokenOption

View File

@ -70,6 +70,20 @@ type swaggerResponseAnnotatedTag struct {
Body api.AnnotatedTag `json:"body"` Body api.AnnotatedTag `json:"body"`
} }
// TagProtectionList
// swagger:response TagProtectionList
type swaggerResponseTagProtectionList struct {
// in:body
Body []api.TagProtection `json:"body"`
}
// TagProtection
// swagger:response TagProtection
type swaggerResponseTagProtection struct {
// in:body
Body api.TagProtection `json:"body"`
}
// Reference // Reference
// swagger:response Reference // swagger:response Reference
type swaggerResponseReference struct { type swaggerResponseReference struct {

View File

@ -408,6 +408,32 @@ func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api.
} }
} }
// ToTagProtection convert a git.ProtectedTag to an api.TagProtection
func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo_model.Repository) *api.TagProtection {
readers, err := access_model.GetRepoReaders(ctx, repo)
if err != nil {
log.Error("GetRepoReaders: %v", err)
}
whitelistUsernames := getWhitelistEntities(readers, pt.AllowlistUserIDs)
teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead)
if err != nil {
log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
}
whitelistTeams := getWhitelistEntities(teamReaders, pt.AllowlistTeamIDs)
return &api.TagProtection{
ID: pt.ID,
NamePattern: pt.NamePattern,
WhitelistUsernames: whitelistUsernames,
WhitelistTeams: whitelistTeams,
Created: pt.CreatedUnix.AsTime(),
Updated: pt.UpdatedUnix.AsTime(),
}
}
// ToTopicResponse convert from models.Topic to api.TopicResponse // ToTopicResponse convert from models.Topic to api.TopicResponse
func ToTopicResponse(topic *repo_model.Topic) *api.TopicResponse { func ToTopicResponse(topic *repo_model.Topic) *api.TopicResponse {
return &api.TopicResponse{ return &api.TopicResponse{

View File

@ -13797,6 +13797,233 @@
} }
} }
}, },
"/repos/{owner}/{repo}/tag_protections": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "List tag protections for a repository",
"operationId": "repoListTagProtection",
"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/TagProtectionList"
}
}
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Create a tag protections for a repository",
"operationId": "repoCreateTagProtection",
"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/CreateTagProtectionOption"
}
}
],
"responses": {
"201": {
"$ref": "#/responses/TagProtection"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/validationError"
},
"423": {
"$ref": "#/responses/repoArchivedError"
}
}
}
},
"/repos/{owner}/{repo}/tag_protections/{id}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Get a specific tag protection for the repository",
"operationId": "repoGetTagProtection",
"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": "integer",
"description": "id of the tag protect to get",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/TagProtection"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Delete a specific tag protection for the repository",
"operationId": "repoDeleteTagProtection",
"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": "integer",
"description": "id of protected tag",
"name": "id",
"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 tag protections for a repository. Only fields that are set will be changed",
"operationId": "repoEditTagProtection",
"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": "integer",
"description": "id of protected tag",
"name": "id",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/EditTagProtectionOption"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/TagProtection"
},
"404": {
"$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/validationError"
},
"423": {
"$ref": "#/responses/repoArchivedError"
}
}
}
},
"/repos/{owner}/{repo}/tags": { "/repos/{owner}/{repo}/tags": {
"get": { "get": {
"produces": [ "produces": [
@ -19954,6 +20181,31 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"CreateTagProtectionOption": {
"description": "CreateTagProtectionOption options for creating a tag protection",
"type": "object",
"properties": {
"name_pattern": {
"type": "string",
"x-go-name": "NamePattern"
},
"whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "WhitelistTeams"
},
"whitelist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "WhitelistUsernames"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"CreateTeamOption": { "CreateTeamOption": {
"description": "CreateTeamOption options for creating a team", "description": "CreateTeamOption options for creating a team",
"type": "object", "type": "object",
@ -20870,6 +21122,31 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"EditTagProtectionOption": {
"description": "EditTagProtectionOption options for editing a tag protection",
"type": "object",
"properties": {
"name_pattern": {
"type": "string",
"x-go-name": "NamePattern"
},
"whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "WhitelistTeams"
},
"whitelist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "WhitelistUsernames"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"EditTeamOption": { "EditTeamOption": {
"description": "EditTeamOption options for editing a team", "description": "EditTeamOption options for editing a team",
"type": "object", "type": "object",
@ -24024,6 +24301,46 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"TagProtection": {
"description": "TagProtection represents a tag protection",
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Created"
},
"id": {
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"name_pattern": {
"type": "string",
"x-go-name": "NamePattern"
},
"updated_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Updated"
},
"whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "WhitelistTeams"
},
"whitelist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "WhitelistUsernames"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"Team": { "Team": {
"description": "Team represents a team in an organization", "description": "Team represents a team in an organization",
"type": "object", "type": "object",
@ -25635,6 +25952,21 @@
} }
} }
}, },
"TagProtection": {
"description": "TagProtection",
"schema": {
"$ref": "#/definitions/TagProtection"
}
},
"TagProtectionList": {
"description": "TagProtectionList",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/TagProtection"
}
}
},
"TasksList": { "TasksList": {
"description": "TasksList", "description": "TasksList",
"schema": { "schema": {