mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 01:58:56 +00: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:
		| @@ -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" | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user