From ecefa9e724460deb70b97dd7c52fc8f4db94be93 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 3 Feb 2019 11:35:17 +0800 Subject: [PATCH] Add single commit API support (#5843) * add single commit API support --- Gopkg.lock | 4 +- integrations/api_repo_git_commits_test.go | 32 +++++ routers/api/v1/api.go | 3 + routers/api/v1/repo/commits.go | 119 ++++++++++++++++++ routers/api/v1/swagger/repo.go | 7 ++ templates/swagger/v1_json.tmpl | 142 ++++++++++++++++++++++ vendor/code.gitea.io/git/commit.go | 52 ++++++++ vendor/code.gitea.io/git/repo_commit.go | 3 + vendor/code.gitea.io/git/submodule.go | 16 ++- vendor/code.gitea.io/git/tree.go | 22 +++- 10 files changed, 389 insertions(+), 11 deletions(-) create mode 100644 integrations/api_repo_git_commits_test.go create mode 100644 routers/api/v1/repo/commits.go diff --git a/Gopkg.lock b/Gopkg.lock index 1727b91af..65cdf7efa 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -3,11 +3,11 @@ [[projects]] branch = "master" - digest = "1:ab875622908a804a327a95a1701002b150806a3c5406df51ec231eac16d3a1ca" + digest = "1:8a6c3c311918c0f08fa2899feae2c938a9bf22b51378e3720d63b80aca4e80aa" name = "code.gitea.io/git" packages = ["."] pruneopts = "NUT" - revision = "389d3c803e12a30dffcbb54a15c2242521bc4333" + revision = "d04f81a6f8979be39da165fc034447a805071b97" [[projects]] branch = "master" diff --git a/integrations/api_repo_git_commits_test.go b/integrations/api_repo_git_commits_test.go new file mode 100644 index 000000000..587e9de5b --- /dev/null +++ b/integrations/api_repo_git_commits_test.go @@ -0,0 +1,32 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/models" +) + +func TestAPIReposGitCommits(t *testing.T) { + prepareTestEnv(t) + user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) + // Login as User2. + 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) + } + + // 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) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 7e7bf6a50..55f5c6629 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -617,6 +617,9 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/statuses", repo.GetCommitStatusesByRef) }, reqRepoReader(models.UnitTypeCode)) m.Group("/git", func() { + m.Group("/commits", func() { + m.Get("/:sha", repo.GetSingleCommit) + }) m.Get("/refs", repo.GetGitAllRefs) m.Get("/refs/*", repo.GetGitRefs) m.Combo("/trees/:sha", context.RepoRef()).Get(repo.GetTree) diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go new file mode 100644 index 000000000..a4cf5037d --- /dev/null +++ b/routers/api/v1/repo/commits.go @@ -0,0 +1,119 @@ +// Copyright 2018 The Gogs Authors. All rights reserved. +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repo + +import ( + "time" + + "code.gitea.io/git" + api "code.gitea.io/sdk/gitea" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" +) + +// GetSingleCommit get a commit via +func GetSingleCommit(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/git/commits/{sha} repository repoGetSingleCommit + // --- + // 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: sha + // in: path + // description: the commit hash + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/Commit" + // "404": + // "$ref": "#/responses/notFound" + + gitRepo, err := git.OpenRepository(ctx.Repo.Repository.RepoPath()) + if err != nil { + ctx.ServerError("OpenRepository", err) + return + } + commit, err := gitRepo.GetCommit(ctx.Params(":sha")) + if err != nil { + ctx.NotFoundOrServerError("GetCommit", git.IsErrNotExist, err) + return + } + + // Retrieve author and committer information + var apiAuthor, apiCommitter *api.User + author, err := models.GetUserByEmail(commit.Author.Email) + if err != nil && !models.IsErrUserNotExist(err) { + ctx.ServerError("Get user by author email", err) + return + } else if err == nil { + apiAuthor = author.APIFormat() + } + // Save one query if the author is also the committer + if commit.Committer.Email == commit.Author.Email { + apiCommitter = apiAuthor + } else { + committer, err := models.GetUserByEmail(commit.Committer.Email) + if err != nil && !models.IsErrUserNotExist(err) { + ctx.ServerError("Get user by committer email", err) + return + } else if err == nil { + apiCommitter = committer.APIFormat() + } + } + + // Retrieve parent(s) of the commit + apiParents := make([]*api.CommitMeta, commit.ParentCount()) + for i := 0; i < commit.ParentCount(); i++ { + sha, _ := commit.ParentID(i) + apiParents[i] = &api.CommitMeta{ + URL: ctx.Repo.Repository.APIURL() + "/git/commits/" + sha.String(), + SHA: sha.String(), + } + } + + ctx.JSON(200, &api.Commit{ + CommitMeta: &api.CommitMeta{ + URL: setting.AppURL + ctx.Link[1:], + SHA: commit.ID.String(), + }, + HTMLURL: ctx.Repo.Repository.HTMLURL() + "/commits/" + commit.ID.String(), + RepoCommit: &api.RepoCommit{ + URL: setting.AppURL + ctx.Link[1:], + Author: &api.CommitUser{ + Name: commit.Author.Name, + Email: commit.Author.Email, + Date: commit.Author.When.Format(time.RFC3339), + }, + Committer: &api.CommitUser{ + Name: commit.Committer.Name, + Email: commit.Committer.Email, + Date: commit.Committer.When.Format(time.RFC3339), + }, + Message: commit.Summary(), + Tree: &api.CommitMeta{ + URL: ctx.Repo.Repository.APIURL() + "/trees/" + commit.ID.String(), + SHA: commit.ID.String(), + }, + }, + Author: apiAuthor, + Committer: apiCommitter, + Parents: apiParents, + }) +} diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index 0c9f95f96..5b930e295 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -140,3 +140,10 @@ type swaggerGitTreeResponse struct { //in: body Body api.GitTreeResponse `json:"body"` } + +// Commit +// swagger:response Commit +type swaggerCommit struct { + //in: body + Body api.Commit `json:"body"` +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 801bab51f..0ce6b805f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1622,6 +1622,49 @@ } } }, + "/repos/{owner}/{repo}/git/commits/{sha}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get a single commit from a repository", + "operationId": "repoGetSingleCommit", + "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": "the commit hash", + "name": "sha", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/Commit" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/git/refs": { "get": { "produces": [ @@ -6174,6 +6217,75 @@ }, "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea" }, + "Commit": { + "type": "object", + "title": "Commit contains information generated from a Git commit.", + "properties": { + "author": { + "$ref": "#/definitions/User" + }, + "commit": { + "$ref": "#/definitions/RepoCommit" + }, + "committer": { + "$ref": "#/definitions/User" + }, + "html_url": { + "type": "string", + "x-go-name": "HTMLURL" + }, + "parents": { + "type": "array", + "items": { + "$ref": "#/definitions/CommitMeta" + }, + "x-go-name": "Parents" + }, + "sha": { + "type": "string", + "x-go-name": "SHA" + }, + "url": { + "type": "string", + "x-go-name": "URL" + } + }, + "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea" + }, + "CommitMeta": { + "type": "object", + "title": "CommitMeta contains meta information of a commit in terms of API.", + "properties": { + "sha": { + "type": "string", + "x-go-name": "SHA" + }, + "url": { + "type": "string", + "x-go-name": "URL" + } + }, + "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea" + }, + "CommitUser": { + "type": "object", + "title": "CommitUser contains information of a user in the context of a commit.", + "properties": { + "date": { + "type": "string", + "x-go-name": "Date" + }, + "email": { + "type": "string", + "x-go-name": "Email" + }, + "name": { + "type": "string", + "x-go-name": "Name" + } + }, + "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea" + }, "CreateEmailOption": { "description": "CreateEmailOption options when creating email addresses", "type": "object", @@ -7952,6 +8064,30 @@ }, "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea" }, + "RepoCommit": { + "type": "object", + "title": "RepoCommit contains information of a commit in the context of a repository.", + "properties": { + "author": { + "$ref": "#/definitions/CommitUser" + }, + "committer": { + "$ref": "#/definitions/CommitUser" + }, + "message": { + "type": "string", + "x-go-name": "Message" + }, + "tree": { + "$ref": "#/definitions/CommitMeta" + }, + "url": { + "type": "string", + "x-go-name": "URL" + } + }, + "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea" + }, "Repository": { "description": "Repository represents a repository", "type": "object", @@ -8382,6 +8518,12 @@ } } }, + "Commit": { + "description": "Commit", + "schema": { + "$ref": "#/definitions/Commit" + } + }, "DeployKey": { "description": "DeployKey", "schema": { diff --git a/vendor/code.gitea.io/git/commit.go b/vendor/code.gitea.io/git/commit.go index 5e8c91d30..227df09b7 100644 --- a/vendor/code.gitea.io/git/commit.go +++ b/vendor/code.gitea.io/git/commit.go @@ -1,4 +1,5 @@ // Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -9,6 +10,7 @@ import ( "bytes" "container/list" "fmt" + "io" "net/http" "strconv" "strings" @@ -279,6 +281,56 @@ func (c *Commit) GetSubModule(entryname string) (*SubModule, error) { return nil, nil } +// CommitFileStatus represents status of files in a commit. +type CommitFileStatus struct { + Added []string + Removed []string + Modified []string +} + +// NewCommitFileStatus creates a CommitFileStatus +func NewCommitFileStatus() *CommitFileStatus { + return &CommitFileStatus{ + []string{}, []string{}, []string{}, + } +} + +// GetCommitFileStatus returns file status of commit in given repository. +func GetCommitFileStatus(repoPath, commitID string) (*CommitFileStatus, error) { + stdout, w := io.Pipe() + done := make(chan struct{}) + fileStatus := NewCommitFileStatus() + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) < 2 { + continue + } + + switch fields[0][0] { + case 'A': + fileStatus.Added = append(fileStatus.Added, fields[1]) + case 'D': + fileStatus.Removed = append(fileStatus.Removed, fields[1]) + case 'M': + fileStatus.Modified = append(fileStatus.Modified, fields[1]) + } + } + done <- struct{}{} + }() + + stderr := new(bytes.Buffer) + err := NewCommand("show", "--name-status", "--pretty=format:''", commitID).RunInDirPipeline(repoPath, w, stderr) + w.Close() // Close writer to exit parsing goroutine + if err != nil { + return nil, concatenateError(err, stderr.String()) + } + + <-done + return fileStatus, nil +} + // GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository. func GetFullCommitID(repoPath, shortID string) (string, error) { if len(shortID) >= 40 { diff --git a/vendor/code.gitea.io/git/repo_commit.go b/vendor/code.gitea.io/git/repo_commit.go index d5cab8f87..484568585 100644 --- a/vendor/code.gitea.io/git/repo_commit.go +++ b/vendor/code.gitea.io/git/repo_commit.go @@ -140,6 +140,9 @@ func (repo *Repository) GetCommit(commitID string) (*Commit, error) { var err error commitID, err = NewCommand("rev-parse", commitID).RunInDir(repo.Path) if err != nil { + if strings.Contains(err.Error(), "unknown revision or path") { + return nil, ErrNotExist{commitID, ""} + } return nil, err } } diff --git a/vendor/code.gitea.io/git/submodule.go b/vendor/code.gitea.io/git/submodule.go index a0fe7b4a5..294df3986 100644 --- a/vendor/code.gitea.io/git/submodule.go +++ b/vendor/code.gitea.io/git/submodule.go @@ -29,13 +29,12 @@ func NewSubModuleFile(c *Commit, refURL, refID string) *SubModuleFile { } } -// RefURL guesses and returns reference URL. -func (sf *SubModuleFile) RefURL(urlPrefix string, parentPath string) string { - if sf.refURL == "" { +func getRefURL(refURL, urlPrefix, parentPath string) string { + if refURL == "" { return "" } - url := strings.TrimSuffix(sf.refURL, ".git") + url := strings.TrimSuffix(refURL, ".git") // git://xxx/user/repo if strings.HasPrefix(url, "git://") { @@ -67,12 +66,21 @@ func (sf *SubModuleFile) RefURL(urlPrefix string, parentPath string) string { if strings.Contains(urlPrefix, url[i+1:j]) { return urlPrefix + url[j+1:] } + if strings.HasPrefix(url, "ssh://") || strings.HasPrefix(url, "git+ssh://") { + k := strings.Index(url[j+1:], "/") + return "http://" + url[i+1:j] + "/" + url[j+1:][k+1:] + } return "http://" + url[i+1:j] + "/" + url[j+1:] } return url } +// RefURL guesses and returns reference URL. +func (sf *SubModuleFile) RefURL(urlPrefix string, parentPath string) string { + return getRefURL(sf.refURL, urlPrefix, parentPath) +} + // RefID returns reference ID. func (sf *SubModuleFile) RefID() string { return sf.refID diff --git a/vendor/code.gitea.io/git/tree.go b/vendor/code.gitea.io/git/tree.go index b67bf5584..b65fe1940 100644 --- a/vendor/code.gitea.io/git/tree.go +++ b/vendor/code.gitea.io/git/tree.go @@ -18,6 +18,9 @@ type Tree struct { entries Entries entriesParsed bool + + entriesRecursive Entries + entriesRecursiveParsed bool } // NewTree create a new tree according the repository and commit id @@ -67,20 +70,29 @@ func (t *Tree) ListEntries() (Entries, error) { if err != nil { return nil, err } + t.entries, err = parseTreeEntries(stdout, t) + if err == nil { + t.entriesParsed = true + } + return t.entries, err } // ListEntriesRecursive returns all entries of current tree recursively including all subtrees func (t *Tree) ListEntriesRecursive() (Entries, error) { - if t.entriesParsed { - return t.entries, nil + if t.entriesRecursiveParsed { + return t.entriesRecursive, nil } stdout, err := NewCommand("ls-tree", "-t", "-r", t.ID.String()).RunInDirBytes(t.repo.Path) - if err != nil { return nil, err } - t.entries, err = parseTreeEntries(stdout, t) - return t.entries, err + + t.entriesRecursive, err = parseTreeEntries(stdout, t) + if err == nil { + t.entriesRecursiveParsed = true + } + + return t.entriesRecursive, err }