Feature #11835 - Adds Git Refs API
This commit is contained in:
parent
a08b484549
commit
0d8c2a859d
12 changed files with 775 additions and 15 deletions
72
models/git/refs.go
Normal file
72
models/git/refs.go
Normal 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
|
||||
}
|
27
modules/convert/git_ref.go
Normal file
27
modules/convert/git_ref.go
Normal 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()),
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
124
services/gitref/gitref.go
Normal 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
|
||||
}
|
40
services/gitref/gitref_test.go
Normal file
40
services/gitref/gitref_test.go
Normal 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)
|
||||
}
|
|
@ -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": {
|
||||
|
|
Loading…
Add table
Reference in a new issue