mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08: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)
 | 
			
		||||
	token := getTokenForLoggedInUser(t, session)
 | 
			
		||||
 | 
			
		||||
	for _, ref := range [...]string{
 | 
			
		||||
		"commits/master", // Branch
 | 
			
		||||
		"commits/v1.1",   // Tag
 | 
			
		||||
	} {
 | 
			
		||||
		req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/%s?token="+token, user.Name, ref)
 | 
			
		||||
		session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	}
 | 
			
		||||
	//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)
 | 
			
		||||
 | 
			
		||||
	// Test getting non-existent refs
 | 
			
		||||
	req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/unknown?token="+token, user.Name)
 | 
			
		||||
	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{
 | 
			
		||||
		"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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -386,7 +386,6 @@ func ToCommitUser(sig *git.Signature) *api.CommitUser {
 | 
			
		||||
func ToCommitMeta(repo *models.Repository, tag *git.Tag) *api.CommitMeta {
 | 
			
		||||
	return &api.CommitMeta{
 | 
			
		||||
		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()),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ package git
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing"
 | 
			
		||||
@@ -16,6 +17,9 @@ import (
 | 
			
		||||
// EmptySHA defines empty git SHA
 | 
			
		||||
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
 | 
			
		||||
type SHA1 = plumbing.Hash
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -22,12 +22,32 @@ const (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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 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
 | 
			
		||||
func AddBindingRules() {
 | 
			
		||||
	addGitRefNameBindingRule()
 | 
			
		||||
@@ -44,25 +64,15 @@ func addGitRefNameBindingRule() {
 | 
			
		||||
		IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
 | 
			
		||||
			str := fmt.Sprintf("%v", val)
 | 
			
		||||
 | 
			
		||||
			if GitRefNamePattern.MatchString(str) {
 | 
			
		||||
			if GitRefNamePatternInvalid.MatchString(str) {
 | 
			
		||||
				errs.Add([]string{name}, ErrGitRefName, "GitRefName")
 | 
			
		||||
				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")
 | 
			
		||||
				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
 | 
			
		||||
		},
 | 
			
		||||
 
 | 
			
		||||
@@ -798,14 +798,14 @@ func RegisterRoutes(m *macaron.Macaron) {
 | 
			
		||||
				m.Group("/commits", func() {
 | 
			
		||||
					m.Get("", repo.GetAllCommits)
 | 
			
		||||
					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("/statuses", repo.GetCommitStatusesByRef)
 | 
			
		||||
					})
 | 
			
		||||
				}, reqRepoReader(models.UnitTypeCode))
 | 
			
		||||
				m.Group("/git", func() {
 | 
			
		||||
					m.Group("/commits", func() {
 | 
			
		||||
						m.Get("/:sha", repo.GetSingleCommit)
 | 
			
		||||
						m.Get("/:sha", repo.GetSingleCommitBySHA)
 | 
			
		||||
					})
 | 
			
		||||
					m.Get("/refs", repo.GetGitAllRefs)
 | 
			
		||||
					m.Get("/refs/*", repo.GetGitRefs)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
package repo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
@@ -16,12 +17,13 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/validation"
 | 
			
		||||
	"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
 | 
			
		||||
	// produces:
 | 
			
		||||
@@ -45,16 +47,68 @@ func GetSingleCommit(ctx *context.APIContext) {
 | 
			
		||||
	// responses:
 | 
			
		||||
	//   "200":
 | 
			
		||||
	//     "$ref": "#/responses/Commit"
 | 
			
		||||
	//   "422":
 | 
			
		||||
	//     "$ref": "#/responses/validationError"
 | 
			
		||||
	//   "404":
 | 
			
		||||
	//     "$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())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("OpenRepository", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer gitRepo.Close()
 | 
			
		||||
	commit, err := gitRepo.GetCommit(ctx.Params(":sha"))
 | 
			
		||||
	commit, err := gitRepo.GetCommit(identifier)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.NotFoundOrServerError("GetCommit", git.IsErrNotExist, err)
 | 
			
		||||
		return
 | 
			
		||||
@@ -65,7 +119,6 @@ func GetSingleCommit(ctx *context.APIContext) {
 | 
			
		||||
		ctx.ServerError("toCommit", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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": {
 | 
			
		||||
      "get": {
 | 
			
		||||
        "produces": [
 | 
			
		||||
@@ -2976,7 +3022,7 @@
 | 
			
		||||
          "repository"
 | 
			
		||||
        ],
 | 
			
		||||
        "summary": "Get a single commit from a repository",
 | 
			
		||||
        "operationId": "repoGetSingleCommit",
 | 
			
		||||
        "operationId": "repoGetSingleCommitBySHA",
 | 
			
		||||
        "parameters": [
 | 
			
		||||
          {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
@@ -3006,6 +3052,9 @@
 | 
			
		||||
          },
 | 
			
		||||
          "404": {
 | 
			
		||||
            "$ref": "#/responses/notFound"
 | 
			
		||||
          },
 | 
			
		||||
          "422": {
 | 
			
		||||
            "$ref": "#/responses/validationError"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user