[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:
6543 2020-04-08 04:54:46 +02:00 committed by GitHub
parent 71979d9663
commit 3d63caa542
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 172 additions and 33 deletions

View file

@ -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) {

View file

@ -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()),
} }
} }

View file

@ -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

View file

@ -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
}, },

View file

@ -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)

View file

@ -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)
} }

View file

@ -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"
} }
} }
} }