Skip to content

WIP: Feature #11835 - Adds Git Refs API #21328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions models/git/refs.go
Original file line number Diff line number Diff line change
@@ -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 changes: 27 additions & 0 deletions modules/convert/git_ref.go
Original file line number Diff line number Diff line change
@@ -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()),
},
}
}
66 changes: 66 additions & 0 deletions modules/git/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,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)
}
15 changes: 15 additions & 0 deletions modules/git/repo_ref.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
23 changes: 23 additions & 0 deletions modules/structs/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,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 {
Expand Down
11 changes: 9 additions & 2 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading