mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-22 12:54:53 -05:00
[API] Get a single commit via Ref (#10915)
* GET /repos/:owner/:repo/commits/:ref * add Validation Checks * Fix & Extend TEST * add two new tast cases
This commit is contained in:
parent
71979d9663
commit
3d63caa542
7 changed files with 172 additions and 33 deletions
|
@ -21,17 +21,41 @@ func TestAPIReposGitCommits(t *testing.T) {
|
||||||
session := loginUser(t, user.Name)
|
session := loginUser(t, user.Name)
|
||||||
token := getTokenForLoggedInUser(t, session)
|
token := getTokenForLoggedInUser(t, session)
|
||||||
|
|
||||||
for _, ref := range [...]string{
|
//check invalid requests for GetCommitsBySHA
|
||||||
"commits/master", // Branch
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/master?token="+token, user.Name)
|
||||||
"commits/v1.1", // Tag
|
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
} {
|
|
||||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/%s?token="+token, user.Name, ref)
|
|
||||||
session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test getting non-existent refs
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/12345?token="+token, user.Name)
|
||||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/unknown?token="+token, user.Name)
|
|
||||||
session.MakeRequest(t, req, http.StatusNotFound)
|
session.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
//check invalid requests for GetCommitsByRef
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/commits/..?token="+token, user.Name)
|
||||||
|
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/commits/branch-not-exist?token="+token, user.Name)
|
||||||
|
session.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
for _, ref := range [...]string{
|
||||||
|
"master", // Branch
|
||||||
|
"v1.1", // Tag
|
||||||
|
"65f1", // short sha
|
||||||
|
"65f1bf27bc3bf70f64657658635e66094edbcb4d", // full sha
|
||||||
|
} {
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/commits/%s?token="+token, user.Name, ref)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
commitByRef := new(api.Commit)
|
||||||
|
DecodeJSON(t, resp, commitByRef)
|
||||||
|
assert.Len(t, commitByRef.SHA, 40)
|
||||||
|
assert.EqualValues(t, commitByRef.SHA, commitByRef.RepoCommit.Tree.SHA)
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/%s?token="+token, user.Name, commitByRef.SHA)
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
commitBySHA := new(api.Commit)
|
||||||
|
DecodeJSON(t, resp, commitBySHA)
|
||||||
|
|
||||||
|
assert.EqualValues(t, commitByRef.SHA, commitBySHA.SHA)
|
||||||
|
assert.EqualValues(t, commitByRef.HTMLURL, commitBySHA.HTMLURL)
|
||||||
|
assert.EqualValues(t, commitByRef.RepoCommit.Message, commitBySHA.RepoCommit.Message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIReposGitCommitList(t *testing.T) {
|
func TestAPIReposGitCommitList(t *testing.T) {
|
||||||
|
|
|
@ -386,7 +386,6 @@ func ToCommitUser(sig *git.Signature) *api.CommitUser {
|
||||||
func ToCommitMeta(repo *models.Repository, tag *git.Tag) *api.CommitMeta {
|
func ToCommitMeta(repo *models.Repository, tag *git.Tag) *api.CommitMeta {
|
||||||
return &api.CommitMeta{
|
return &api.CommitMeta{
|
||||||
SHA: tag.Object.String(),
|
SHA: tag.Object.String(),
|
||||||
// TODO: Add the /commits API endpoint and use it here (https://developer.github.com/v3/repos/commits/#get-a-single-commit)
|
|
||||||
URL: util.URLJoin(repo.APIURL(), "git/commits", tag.ID.String()),
|
URL: util.URLJoin(repo.APIURL(), "git/commits", tag.ID.String()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ package git
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
@ -16,6 +17,9 @@ import (
|
||||||
// EmptySHA defines empty git SHA
|
// EmptySHA defines empty git SHA
|
||||||
const EmptySHA = "0000000000000000000000000000000000000000"
|
const EmptySHA = "0000000000000000000000000000000000000000"
|
||||||
|
|
||||||
|
// SHAPattern can be used to determine if a string is an valid sha
|
||||||
|
var SHAPattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)
|
||||||
|
|
||||||
// SHA1 a git commit name
|
// SHA1 a git commit name
|
||||||
type SHA1 = plumbing.Hash
|
type SHA1 = plumbing.Hash
|
||||||
|
|
||||||
|
|
|
@ -22,12 +22,32 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// GitRefNamePattern is regular expression with unallowed characters in git reference name
|
// GitRefNamePatternInvalid is regular expression with unallowed characters in git reference name
|
||||||
// They cannot have ASCII control characters (i.e. bytes whose values are lower than \040, or \177 DEL), space, tilde ~, caret ^, or colon : anywhere.
|
// They cannot have ASCII control characters (i.e. bytes whose values are lower than \040, or \177 DEL), space, tilde ~, caret ^, or colon : anywhere.
|
||||||
// They cannot have question-mark ?, asterisk *, or open bracket [ anywhere
|
// They cannot have question-mark ?, asterisk *, or open bracket [ anywhere
|
||||||
GitRefNamePattern = regexp.MustCompile(`[\000-\037\177 \\~^:?*[]+`)
|
GitRefNamePatternInvalid = regexp.MustCompile(`[\000-\037\177 \\~^:?*[]+`)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CheckGitRefAdditionalRulesValid check name is valid on additional rules
|
||||||
|
func CheckGitRefAdditionalRulesValid(name string) bool {
|
||||||
|
|
||||||
|
// Additional rules as described at https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
|
||||||
|
if strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") ||
|
||||||
|
strings.HasSuffix(name, ".") || strings.Contains(name, "..") ||
|
||||||
|
strings.Contains(name, "//") || strings.Contains(name, "@{") ||
|
||||||
|
name == "@" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parts := strings.Split(name, "/")
|
||||||
|
for _, part := range parts {
|
||||||
|
if strings.HasSuffix(part, ".lock") || strings.HasPrefix(part, ".") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// AddBindingRules adds additional binding rules
|
// AddBindingRules adds additional binding rules
|
||||||
func AddBindingRules() {
|
func AddBindingRules() {
|
||||||
addGitRefNameBindingRule()
|
addGitRefNameBindingRule()
|
||||||
|
@ -44,25 +64,15 @@ func addGitRefNameBindingRule() {
|
||||||
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
|
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
|
||||||
str := fmt.Sprintf("%v", val)
|
str := fmt.Sprintf("%v", val)
|
||||||
|
|
||||||
if GitRefNamePattern.MatchString(str) {
|
if GitRefNamePatternInvalid.MatchString(str) {
|
||||||
errs.Add([]string{name}, ErrGitRefName, "GitRefName")
|
errs.Add([]string{name}, ErrGitRefName, "GitRefName")
|
||||||
return false, errs
|
return false, errs
|
||||||
}
|
}
|
||||||
// Additional rules as described at https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
|
|
||||||
if strings.HasPrefix(str, "/") || strings.HasSuffix(str, "/") ||
|
if !CheckGitRefAdditionalRulesValid(str) {
|
||||||
strings.HasSuffix(str, ".") || strings.Contains(str, "..") ||
|
|
||||||
strings.Contains(str, "//") || strings.Contains(str, "@{") ||
|
|
||||||
str == "@" {
|
|
||||||
errs.Add([]string{name}, ErrGitRefName, "GitRefName")
|
errs.Add([]string{name}, ErrGitRefName, "GitRefName")
|
||||||
return false, errs
|
return false, errs
|
||||||
}
|
}
|
||||||
parts := strings.Split(str, "/")
|
|
||||||
for _, part := range parts {
|
|
||||||
if strings.HasSuffix(part, ".lock") || strings.HasPrefix(part, ".") {
|
|
||||||
errs.Add([]string{name}, ErrGitRefName, "GitRefName")
|
|
||||||
return false, errs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, errs
|
return true, errs
|
||||||
},
|
},
|
||||||
|
|
|
@ -798,14 +798,14 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Group("/commits", func() {
|
m.Group("/commits", func() {
|
||||||
m.Get("", repo.GetAllCommits)
|
m.Get("", repo.GetAllCommits)
|
||||||
m.Group("/:ref", func() {
|
m.Group("/:ref", func() {
|
||||||
// TODO: Add m.Get("") for single commit (https://developer.github.com/v3/repos/commits/#get-a-single-commit)
|
m.Get("", repo.GetSingleCommitByRef)
|
||||||
m.Get("/status", repo.GetCombinedCommitStatusByRef)
|
m.Get("/status", repo.GetCombinedCommitStatusByRef)
|
||||||
m.Get("/statuses", repo.GetCommitStatusesByRef)
|
m.Get("/statuses", repo.GetCommitStatusesByRef)
|
||||||
})
|
})
|
||||||
}, reqRepoReader(models.UnitTypeCode))
|
}, reqRepoReader(models.UnitTypeCode))
|
||||||
m.Group("/git", func() {
|
m.Group("/git", func() {
|
||||||
m.Group("/commits", func() {
|
m.Group("/commits", func() {
|
||||||
m.Get("/:sha", repo.GetSingleCommit)
|
m.Get("/:sha", repo.GetSingleCommitBySHA)
|
||||||
})
|
})
|
||||||
m.Get("/refs", repo.GetGitAllRefs)
|
m.Get("/refs", repo.GetGitAllRefs)
|
||||||
m.Get("/refs/*", repo.GetGitRefs)
|
m.Get("/refs/*", repo.GetGitRefs)
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -16,12 +17,13 @@ import (
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/validation"
|
||||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetSingleCommit get a commit via
|
// GetSingleCommitBySHA get a commit via sha
|
||||||
func GetSingleCommit(ctx *context.APIContext) {
|
func GetSingleCommitBySHA(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /repos/{owner}/{repo}/git/commits/{sha} repository repoGetSingleCommit
|
// swagger:operation GET /repos/{owner}/{repo}/git/commits/{sha} repository repoGetSingleCommitBySHA
|
||||||
// ---
|
// ---
|
||||||
// summary: Get a single commit from a repository
|
// summary: Get a single commit from a repository
|
||||||
// produces:
|
// produces:
|
||||||
|
@ -45,16 +47,68 @@ func GetSingleCommit(ctx *context.APIContext) {
|
||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/Commit"
|
// "$ref": "#/responses/Commit"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
sha := ctx.Params(":sha")
|
||||||
|
if !git.SHAPattern.MatchString(sha) {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "no valid sha", fmt.Sprintf("no valid sha: %s", sha))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getCommit(ctx, sha)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSingleCommitByRef get a commit via ref
|
||||||
|
func GetSingleCommitByRef(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/commits/{ref} repository repoGetSingleCommitByRef
|
||||||
|
// ---
|
||||||
|
// summary: Get a single commit 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: ref
|
||||||
|
// in: path
|
||||||
|
// description: a git ref
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/Commit"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
ref := ctx.Params("ref")
|
||||||
|
|
||||||
|
if validation.GitRefNamePatternInvalid.MatchString(ref) || !validation.CheckGitRefAdditionalRulesValid(ref) {
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "no valid sha", fmt.Sprintf("no valid ref: %s", ref))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommit(ctx, ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommit(ctx *context.APIContext, identifier string) {
|
||||||
gitRepo, err := git.OpenRepository(ctx.Repo.Repository.RepoPath())
|
gitRepo, err := git.OpenRepository(ctx.Repo.Repository.RepoPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("OpenRepository", err)
|
ctx.ServerError("OpenRepository", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
commit, err := gitRepo.GetCommit(ctx.Params(":sha"))
|
commit, err := gitRepo.GetCommit(identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.NotFoundOrServerError("GetCommit", git.IsErrNotExist, err)
|
ctx.NotFoundOrServerError("GetCommit", git.IsErrNotExist, err)
|
||||||
return
|
return
|
||||||
|
@ -65,7 +119,6 @@ func GetSingleCommit(ctx *context.APIContext) {
|
||||||
ctx.ServerError("toCommit", err)
|
ctx.ServerError("toCommit", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, json)
|
ctx.JSON(http.StatusOK, json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2511,6 +2511,52 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/commits/{ref}": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Get a single commit from a repository",
|
||||||
|
"operationId": "repoGetSingleCommitByRef",
|
||||||
|
"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": "a git ref",
|
||||||
|
"name": "ref",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/Commit"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/responses/notFound"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/responses/validationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/commits/{ref}/statuses": {
|
"/repos/{owner}/{repo}/commits/{ref}/statuses": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
@ -2976,7 +3022,7 @@
|
||||||
"repository"
|
"repository"
|
||||||
],
|
],
|
||||||
"summary": "Get a single commit from a repository",
|
"summary": "Get a single commit from a repository",
|
||||||
"operationId": "repoGetSingleCommit",
|
"operationId": "repoGetSingleCommitBySHA",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -3006,6 +3052,9 @@
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"$ref": "#/responses/notFound"
|
"$ref": "#/responses/notFound"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/responses/validationError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue