* GET /repos/:owner/:repo/commits/:ref * add Validation Checks * Fix & Extend TEST * add two new tast casestags/v1.13.0-dev
session := loginUser(t, user.Name) | session := loginUser(t, user.Name) | ||||
token := getTokenForLoggedInUser(t, session) | token := getTokenForLoggedInUser(t, session) | ||||
//check invalid requests for GetCommitsBySHA | |||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/master?token="+token, user.Name) | |||||
session.MakeRequest(t, req, http.StatusUnprocessableEntity) | |||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/12345?token="+token, user.Name) | |||||
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{ | for _, ref := range [...]string{ | ||||
"commits/master", // Branch | |||||
"commits/v1.1", // Tag | |||||
"master", // Branch | |||||
"v1.1", // Tag | |||||
"65f1", // short sha | |||||
"65f1bf27bc3bf70f64657658635e66094edbcb4d", // full sha | |||||
} { | } { | ||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/%s?token="+token, user.Name, ref) | |||||
session.MakeRequest(t, req, http.StatusOK) | |||||
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) | |||||
} | } | ||||
// Test getting non-existent refs | |||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/unknown?token="+token, user.Name) | |||||
session.MakeRequest(t, req, http.StatusNotFound) | |||||
} | } | ||||
func TestAPIReposGitCommitList(t *testing.T) { | func TestAPIReposGitCommitList(t *testing.T) { |
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()), | ||||
} | } | ||||
} | } |
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" | ||||
// 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 | ||||
) | ) | ||||
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() | ||||
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, "/") || | |||||
strings.HasSuffix(str, ".") || strings.Contains(str, "..") || | |||||
strings.Contains(str, "//") || strings.Contains(str, "@{") || | |||||
str == "@" { | |||||
if !CheckGitRefAdditionalRulesValid(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 | ||||
}, | }, |
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) |
package repo | package repo | ||||
import ( | import ( | ||||
"fmt" | |||||
"math" | "math" | ||||
"net/http" | "net/http" | ||||
"strconv" | "strconv" | ||||
"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 | |||||
func GetSingleCommit(ctx *context.APIContext) { | |||||
// swagger:operation GET /repos/{owner}/{repo}/git/commits/{sha} repository repoGetSingleCommit | |||||
// GetSingleCommitBySHA get a commit via sha | |||||
func GetSingleCommitBySHA(ctx *context.APIContext) { | |||||
// 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: | ||||
// 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 | ||||
ctx.ServerError("toCommit", err) | ctx.ServerError("toCommit", err) | ||||
return | return | ||||
} | } | ||||
ctx.JSON(http.StatusOK, json) | ctx.JSON(http.StatusOK, json) | ||||
} | } | ||||
} | } | ||||
} | } | ||||
}, | }, | ||||
"/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": [ | ||||
"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", | ||||
}, | }, | ||||
"404": { | "404": { | ||||
"$ref": "#/responses/notFound" | "$ref": "#/responses/notFound" | ||||
}, | |||||
"422": { | |||||
"$ref": "#/responses/validationError" | |||||
} | } | ||||
} | } | ||||
} | } |