Feature #11835 - Adds Git Refs API

This commit is contained in:
Richard Mahn 2022-10-03 12:35:24 -06:00
parent a08b484549
commit 0d8c2a859d
12 changed files with 775 additions and 15 deletions

72
models/git/refs.go Normal file
View file

@ -0,0 +1,72 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package git
import (
"strings"
"code.gitea.io/gitea/modules/git"
)
// CheckReferenceEditability checks if the reference can be modified by the user or any user
func CheckReferenceEditability(refName, commitID string, repoID, userID int64) error {
refParts := strings.Split(refName, "/")
// Must have at least 3 parts, e.g. refs/heads/new-branch
if len(refParts) <= 2 {
return git.ErrInvalidRefName{
RefName: refName,
Reason: "reference name must contain at least three slash-separted components",
}
}
// Must start with 'refs/'
if refParts[0] != "refs/" {
return git.ErrInvalidRefName{
RefName: refName,
Reason: "reference must start with 'refs/'",
}
}
// 'refs/pull/*' is not allowed
if refParts[1] == "pull" {
return git.ErrInvalidRefName{
RefName: refName,
Reason: "refs/pull/* is read-only",
}
}
if refParts[1] == "tags" {
// If the 2nd part is "tags" then we need ot make sure the user is allowed to
// modify this tag (not protected or is admin)
if protectedTags, err := GetProtectedTags(repoID); err == nil {
isAllowed, err := IsUserAllowedToControlTag(protectedTags, refName, userID)
if err != nil {
return err
}
if !isAllowed {
return git.ErrProtectedRefName{
RefName: refName,
Message: "you're not authorized to change a protected tag",
}
}
}
} else if refParts[1] == "heads" {
// If the 2nd part is "heas" then we need to make sure the user is allowed to
// modify this branch (not protected or is admin)
isProtected, err := IsProtectedBranch(repoID, refName)
if err != nil {
return err
}
if !isProtected {
return git.ErrProtectedRefName{
RefName: refName,
Message: "changes must be made through a pull request",
}
}
}
return nil
}

View file

@ -0,0 +1,27 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package convert
import (
"net/url"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
)
// ToGitRef converts a git.Reference to a api.Reference
func ToGitRef(repo *repo_model.Repository, ref *git.Reference) *api.Reference {
return &api.Reference{
Ref: ref.Name,
URL: repo.APIURL() + "/git/" + util.PathEscapeSegments(ref.Name),
Object: &api.GitObject{
SHA: ref.Object.String(),
Type: ref.Type,
URL: repo.APIURL() + "/git/" + url.PathEscape(ref.Type) + "s/" + url.PathEscape(ref.Object.String()),
},
}
}

View file

@ -176,3 +176,69 @@ func IsErrMoreThanOne(err error) bool {
func (err *ErrMoreThanOne) Error() string {
return fmt.Sprintf("ErrMoreThanOne Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// ErrRefNotFound represents a "RefDoesMotExist" kind of error.
type ErrRefNotFound struct {
RefName string
}
// IsErrRefNotFound checks if an error is a ErrRefNotFound.
func IsErrRefNotFound(err error) bool {
_, ok := err.(ErrRefNotFound)
return ok
}
func (err ErrRefNotFound) Error() string {
return fmt.Sprintf("ref does not exist [ref_name: %s]", err.RefName)
}
// ErrInvalidRefName represents a "InvalidRefName" kind of error.
type ErrInvalidRefName struct {
RefName string
Reason string
}
// IsErrInvalidRefName checks if an error is a ErrInvalidRefName.
func IsErrInvalidRefName(err error) bool {
_, ok := err.(ErrInvalidRefName)
return ok
}
func (err ErrInvalidRefName) Error() string {
return fmt.Sprintf("ref name is not valid: %s [ref_name: %s]", err.Reason, err.RefName)
}
// ErrProtectedRefName represents a "ProtectedRefName" kind of error.
type ErrProtectedRefName struct {
RefName string
Message string
}
// IsErrProtectedRefName checks if an error is a ErrProtectedRefName.
func IsErrProtectedRefName(err error) bool {
_, ok := err.(ErrProtectedRefName)
return ok
}
func (err ErrProtectedRefName) Error() string {
str := fmt.Sprintf("ref name is protected [ref_name: %s]", err.RefName)
if err.Message != "" {
str = fmt.Sprintf("%s: %s", str, err.Message)
}
return str
}
// ErrRefAlreadyExists represents an error that ref with such name already exists.
type ErrRefAlreadyExists struct {
RefName string
}
// IsErrRefAlreadyExists checks if an error is an ErrRefAlreadyExists.
func IsErrRefAlreadyExists(err error) bool {
_, ok := err.(ErrRefAlreadyExists)
return ok
}
func (err ErrRefAlreadyExists) Error() string {
return fmt.Sprintf("ref already exists [name: %s]", err.RefName)
}

View file

@ -8,3 +8,18 @@ package git
func (repo *Repository) GetRefs() ([]*Reference, error) {
return repo.GetRefsFiltered("")
}
// GetReference gets the Reference object that a refName refers to
func (repo *Repository) GetReference(refName string) (*Reference, error) {
refs, err := repo.GetRefsFiltered(refName)
if err != nil {
return nil, err
}
var ref *Reference
for _, ref = range refs {
if ref.Name == refName {
return ref, nil
}
}
return nil, ErrRefNotFound{RefName: refName}
}

View file

@ -240,6 +240,29 @@ type CreateBranchRepoOption struct {
OldBranchName string `json:"old_branch_name" binding:"GitRefName;MaxSize(100)"`
}
// CreateGitRefOption options when creating a git ref in a repository
// swagger:model
type CreateGitRefOption struct {
// The name of the reference.
//
// required: true
RefName string `json:"ref" binding:"Required;GitRefName;MaxSize(100)"`
// The target commitish for this reference.
//
// required: true
Target string `json:"target" binding:"Required"`
}
// UpdateGitRefOption options when updating a git ref in a repository
// swagger:model
type UpdateGitRefOption struct {
// The target commitish for the reference to be updated to.
//
// required: true
Target string `json:"target" binding:"Required"`
}
// TransferRepoOption options when transfer a repository's ownership
// swagger:model
type TransferRepoOption struct {

View file

@ -1042,8 +1042,15 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Get("/{sha}", repo.GetSingleCommit)
m.Get("/{sha}.{diffType:diff|patch}", repo.DownloadCommitDiffOrPatch)
})
m.Get("/refs", repo.GetGitAllRefs)
m.Get("/refs/*", repo.GetGitRefs)
m.Group("/refs", func() {
m.Get("", repo.GetGitAllRefs)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateGitRefOption{}), repo.CreateGitRef)
m.Get("/*", repo.GetGitRefs)
m.Group("/*", func() {
m.Patch("", bind(api.UpdateGitRefOption{}), repo.UpdateGitRef)
m.Delete("", repo.DeleteGitRef)
}, reqToken(), reqRepoWriter(unit.TypeCode))
})
m.Get("/trees/{sha}", repo.GetTree)
m.Get("/blobs/{sha}", repo.GetBlob)
m.Get("/tags/{sha}", repo.GetAnnotatedTag)

View file

@ -5,13 +5,16 @@
package repo
import (
"fmt"
"net/http"
"net/url"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/gitref"
)
// GetGitAllRefs get ref or an list all the refs of a repository
@ -89,15 +92,7 @@ func getGitRefsInternal(ctx *context.APIContext, filter string) {
apiRefs := make([]*api.Reference, len(refs))
for i := range refs {
apiRefs[i] = &api.Reference{
Ref: refs[i].Name,
URL: ctx.Repo.Repository.APIURL() + "/git/" + util.PathEscapeSegments(refs[i].Name),
Object: &api.GitObject{
SHA: refs[i].Object.String(),
Type: refs[i].Type,
URL: ctx.Repo.Repository.APIURL() + "/git/" + url.PathEscape(refs[i].Type) + "s/" + url.PathEscape(refs[i].Object.String()),
},
}
apiRefs[i] = convert.ToGitRef(ctx.Repo.Repository, refs[i])
}
// If single reference is found and it matches filter exactly return it as object
if len(apiRefs) == 1 && apiRefs[0].Ref == filter {
@ -106,3 +101,200 @@ func getGitRefsInternal(ctx *context.APIContext, filter string) {
}
ctx.JSON(http.StatusOK, &apiRefs)
}
// CreateGitRef creates a git ref for a repository that points to a target commitish
func CreateGitRef(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/git/refs repository repoCreateGitRef
// ---
// summary: Create a reference
// description: Creates a reference for your repository. You are unable to create new references for empty repositories,
// even if the commit SHA-1 hash used exists. Empty repositories are repositories without branches.
// 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/CreateGitRefOption"
// responses:
// "201":
// "$ref": "#/responses/Reference"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// description: The git ref with the same name already exists.
// "422":
// description: Unable to form reference
opt := web.GetForm(ctx).(*api.CreateGitRefOption)
if ctx.Repo.GitRepo.IsReferenceExist(opt.RefName) {
ctx.Error(http.StatusConflict, "reference exists", fmt.Errorf("reference already exists: %s", opt.RefName))
return
}
commitID, err := ctx.Repo.GitRepo.GetRefCommitID(opt.Target)
if err != nil {
if git.IsErrNotExist(err) {
ctx.Error(http.StatusNotFound, "invalid target", fmt.Errorf("target does not exist: %s", opt.Target))
return
}
ctx.Error(http.StatusInternalServerError, "GetRefCommitID", err)
return
}
ref, err := gitref.UpdateReferenceWithChecks(ctx, opt.RefName, commitID)
if err != nil {
if git.IsErrInvalidRefName(err) {
ctx.Error(http.StatusUnprocessableEntity, "invalid reference'", err)
} else if git.IsErrProtectedRefName(err) {
ctx.Error(http.StatusMethodNotAllowed, "protected reference", err)
} else if git.IsErrRefNotFound(err) {
ctx.Error(http.StatusUnprocessableEntity, "UpdateReferenceWithChecks", fmt.Errorf("unable to load reference [ref_name: %s]", opt.RefName))
} else {
ctx.InternalServerError(err)
}
return
}
ctx.JSON(http.StatusCreated, convert.ToGitRef(ctx.Repo.Repository, ref))
}
// UpdateGitRef updates a branch for a repository from a commit SHA
func UpdateGitRef(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/git/refs/{ref} repository repoUpdateGitRef
// ---
// summary: Update a reference
// description:
// 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: ref
// in: path
// description: name of the ref to update
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateGitRefOption"
// responses:
// "200":
// "$ref": "#/responses/Reference"
// "404":
// "$ref": "#/responses/notFound"
refName := fmt.Sprintf("refs/%s", ctx.Params("*"))
opt := web.GetForm(ctx).(*api.UpdateGitRefOption)
if ctx.Repo.GitRepo.IsReferenceExist(refName) {
ctx.Error(http.StatusConflict, "reference exists", fmt.Errorf("reference already exists: %s", refName))
return
}
commitID, err := ctx.Repo.GitRepo.GetRefCommitID(opt.Target)
if err != nil {
if git.IsErrNotExist(err) {
ctx.Error(http.StatusNotFound, "invalid target", fmt.Errorf("target does not exist: %s", opt.Target))
return
}
ctx.Error(http.StatusInternalServerError, "GetRefCommitID", err)
return
}
ref, err := gitref.UpdateReferenceWithChecks(ctx, refName, commitID)
if err != nil {
if git.IsErrInvalidRefName(err) {
ctx.Error(http.StatusUnprocessableEntity, "invalid reference'", err)
} else if git.IsErrProtectedRefName(err) {
ctx.Error(http.StatusMethodNotAllowed, "protected reference", err)
} else if git.IsErrRefNotFound(err) {
ctx.Error(http.StatusUnprocessableEntity, "UpdateReferenceWithChecks", fmt.Errorf("unable to load reference [ref_name: %s]", refName))
} else {
ctx.InternalServerError(err)
}
return
}
ctx.JSON(http.StatusCreated, ref)
}
// DeleteGitRef deletes a git ref for a repository that points to a target commitish
func DeleteGitRef(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/git/refs/{ref} repository repoDeleteGitRef
// ---
// summary: Delete a reference
// 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: ref
// in: path
// description: name of the ref to be deleted
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "405":
// "$ref": "#/responses/error"
// "409":
// "$ref": "#/responses/conflict"
refName := fmt.Sprintf("refs/%s", ctx.Params("*"))
if !ctx.Repo.GitRepo.IsReferenceExist(refName) {
ctx.Error(http.StatusNotFound, "git ref does not exist:", fmt.Errorf("reference does not exist: %s", refName))
return
}
err := gitref.RemoveReferenceWithChecks(ctx, refName)
if err != nil {
if git.IsErrInvalidRefName(err) {
ctx.Error(http.StatusUnprocessableEntity, "invalid reference'", err)
} else if git.IsErrProtectedRefName(err) {
ctx.Error(http.StatusMethodNotAllowed, "protected reference", err)
} else {
ctx.InternalServerError(err)
}
return
}
ctx.Status(http.StatusNoContent)
}

View file

@ -213,7 +213,7 @@ func CreateRelease(ctx *context.APIContext) {
}
} else {
if !rel.IsTag {
ctx.Error(http.StatusConflict, "GetRelease", "Release is has no Tag")
ctx.Error(http.StatusConflict, "GetRelease", "Release has no Tag")
return
}

View file

@ -172,4 +172,10 @@ type swaggerParameterBodies struct {
// in:body
CreatePushMirrorOption api.CreatePushMirrorOption
// in:body
CreateGitRefOption api.CreateGitRefOption
// in:body
UpdateGitRefOption api.UpdateGitRefOption
}

124
services/gitref/gitref.go Normal file
View file

@ -0,0 +1,124 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gitref
import (
"fmt"
"strings"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
)
// GetReference gets the Reference object that a refName refers to
func GetReference(gitRepo *git.Repository, refName string) (*git.Reference, error) {
refs, err := gitRepo.GetRefsFiltered(refName)
if err != nil {
return nil, err
}
var ref *git.Reference
for _, ref = range refs {
if ref.Name == refName {
return ref, nil
}
}
return nil, git.ErrRefNotFound{RefName: refName}
}
// UpdateReferenceWithChecks creates or updates a reference, checking for format, permissions and special cases
func UpdateReferenceWithChecks(ctx *context.APIContext, refName, commitID string) (*git.Reference, error) {
err := CheckReferenceEditability(refName, commitID, ctx.Repo.Repository.ID, ctx.Doer.ID)
if err != nil {
return nil, err
}
if err := ctx.Repo.GitRepo.SetReference(refName, commitID); err != nil {
message := err.Error()
prefix := fmt.Sprintf("exit status 128 - fatal: update_ref failed for ref '%s': ", refName)
if strings.HasPrefix(message, prefix) {
return nil, fmt.Errorf(strings.TrimRight(strings.TrimPrefix(message, prefix), "\n"))
}
return nil, err
}
return ctx.Repo.GitRepo.GetReference(refName)
}
// RemoveReferenceWithChecks deletes a reference, checking for format, permission and special cases
func RemoveReferenceWithChecks(ctx *context.APIContext, refName string) error {
err := CheckReferenceEditability(refName, "", ctx.Repo.Repository.ID, ctx.Doer.ID)
if err != nil {
return err
}
return ctx.Repo.GitRepo.RemoveReference(refName)
}
func CheckReferenceEditability(refName, commitID string, repoID, userID int64) error {
refParts := strings.Split(refName, "/")
// Must have at least 3 parts, e.g. refs/heads/new-branch
if len(refParts) <= 2 {
return git.ErrInvalidRefName{
RefName: refName,
Reason: "reference name must contain at least three slash-separted components",
}
}
refPrefix := refParts[0]
refType := refParts[2]
refRest := strings.Join(refParts[2:], "/")
// Must start with 'refs/'
if refPrefix != "refs" {
return git.ErrInvalidRefName{
RefName: refName,
Reason: "reference must start with 'refs/'",
}
}
// 'refs/pull/*' is not allowed
if refType == "pull" {
return git.ErrInvalidRefName{
RefName: refName,
Reason: "refs/pull/* is read-only",
}
}
if refType == "tags" {
// If the 2nd part is "tags" then we need ot make sure the user is allowed to
// modify this tag (not protected or is admin)
if protectedTags, err := git_model.GetProtectedTags(repoID); err == nil {
isAllowed, err := git_model.IsUserAllowedToControlTag(protectedTags, refRest, userID)
if err != nil {
return err
}
if !isAllowed {
return git.ErrProtectedRefName{
RefName: refName,
Message: "you're not authorized to change a protected tag",
}
}
}
}
if refType == "heads" {
// If the 2nd part is "heas" then we need to make sure the user is allowed to
// modify this branch (not protected or is admin)
isProtected, err := git_model.IsProtectedBranch(repoID, refRest)
if err != nil {
return err
}
if !isProtected {
return git.ErrProtectedRefName{
RefName: refName,
Message: "changes must be made through a pull request",
}
}
}
return nil
}

View file

@ -0,0 +1,40 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gitref
import (
"path/filepath"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
GiteaRootPath: filepath.Join("..", ".."),
})
}
func TestGitRef_Get(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repoPath := repo_model.RepoPath(user.Name, repo.Name)
gitRepo, err := git.OpenRepository(git.DefaultContext, repoPath)
assert.NoError(t, err)
defer gitRepo.Close()
ref, err := GetReference(gitRepo, "refs/heads/master")
assert.NoError(t, err)
assert.NotNil(t, ref)
}

View file

@ -4099,6 +4099,57 @@
"$ref": "#/responses/notFound"
}
}
},
"post": {
"description": "Creates a reference for your repository. You are unable to create new references for empty repositories, even if the commit SHA-1 hash used exists. Empty repositories are repositories without branches.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Create a reference",
"operationId": "repoCreateGitRef",
"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/CreateGitRefOption"
}
}
],
"responses": {
"201": {
"$ref": "#/responses/Reference"
},
"404": {
"$ref": "#/responses/notFound"
},
"409": {
"description": "The git ref with the same name already exists."
},
"422": {
"description": "Unable to form reference"
}
}
}
},
"/repos/{owner}/{repo}/git/refs/{ref}": {
@ -4142,6 +4193,107 @@
"$ref": "#/responses/notFound"
}
}
},
"delete": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Delete a reference",
"operationId": "repoDeleteGitRef",
"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 the ref to be deleted",
"name": "ref",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
},
"404": {
"$ref": "#/responses/notFound"
},
"405": {
"$ref": "#/responses/error"
},
"409": {
"$ref": "#/responses/conflict"
}
}
},
"patch": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Update a reference",
"operationId": "repoUpdateGitRef",
"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 the ref to update",
"name": "ref",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/UpdateGitRefOption"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/Reference"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/git/tags/{sha}": {
@ -14429,6 +14581,27 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"CreateGitRefOption": {
"description": "CreateGitRefOption options when creating a git ref in a repository",
"type": "object",
"required": [
"ref",
"target"
],
"properties": {
"ref": {
"description": "The name of the reference.",
"type": "string",
"x-go-name": "RefName"
},
"target": {
"description": "The target commitish for this reference.",
"type": "string",
"x-go-name": "Target"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"CreateHookOption": {
"description": "CreateHookOption options when create a hook",
"type": "object",
@ -18878,6 +19051,21 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"UpdateGitRefOption": {
"description": "UpdateGitRefOption options when updating a git ref in a repository",
"type": "object",
"required": [
"target"
],
"properties": {
"target": {
"description": "The target commitish for the reference to be updated to.",
"type": "string",
"x-go-name": "Target"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"User": {
"description": "User represents a user",
"type": "object",
@ -20080,7 +20268,7 @@
"parameterBodies": {
"description": "parameterBodies",
"schema": {
"$ref": "#/definitions/CreatePushMirrorOption"
"$ref": "#/definitions/UpdateGitRefOption"
}
},
"redirect": {