* GET /repos/:owner/:repo/commits/:ref * add Validation Checks * Fix & Extend TEST * add two new tast casestags/v1.21.12.1
| @@ -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) | ||||
| //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) { | ||||
| @@ -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, "/") || | |||||
| 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 | ||||
| }, | }, | ||||
| @@ -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 | |||||
| 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: | ||||
| @@ -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" | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||