* add single commit API supporttags/v1.9.0-dev
[[projects]] | [[projects]] | ||||
branch = "master" | branch = "master" | ||||
digest = "1:ab875622908a804a327a95a1701002b150806a3c5406df51ec231eac16d3a1ca" | |||||
digest = "1:8a6c3c311918c0f08fa2899feae2c938a9bf22b51378e3720d63b80aca4e80aa" | |||||
name = "code.gitea.io/git" | name = "code.gitea.io/git" | ||||
packages = ["."] | packages = ["."] | ||||
pruneopts = "NUT" | pruneopts = "NUT" | ||||
revision = "389d3c803e12a30dffcbb54a15c2242521bc4333" | |||||
revision = "d04f81a6f8979be39da165fc034447a805071b97" | |||||
[[projects]] | [[projects]] | ||||
branch = "master" | branch = "master" |
// 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) | |||||
} |
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.Get("/:sha", repo.GetSingleCommit) | |||||
}) | |||||
m.Get("/refs", repo.GetGitAllRefs) | m.Get("/refs", repo.GetGitAllRefs) | ||||
m.Get("/refs/*", repo.GetGitRefs) | m.Get("/refs/*", repo.GetGitRefs) | ||||
m.Combo("/trees/:sha", context.RepoRef()).Get(repo.GetTree) | m.Combo("/trees/:sha", context.RepoRef()).Get(repo.GetTree) |
// 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, | |||||
}) | |||||
} |
//in: body | //in: body | ||||
Body api.GitTreeResponse `json:"body"` | Body api.GitTreeResponse `json:"body"` | ||||
} | } | ||||
// Commit | |||||
// swagger:response Commit | |||||
type swaggerCommit struct { | |||||
//in: body | |||||
Body api.Commit `json:"body"` | |||||
} |
} | } | ||||
} | } | ||||
}, | }, | ||||
"/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": { | "/repos/{owner}/{repo}/git/refs": { | ||||
"get": { | "get": { | ||||
"produces": [ | "produces": [ | ||||
}, | }, | ||||
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea" | "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": { | "CreateEmailOption": { | ||||
"description": "CreateEmailOption options when creating email addresses", | "description": "CreateEmailOption options when creating email addresses", | ||||
"type": "object", | "type": "object", | ||||
}, | }, | ||||
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea" | "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": { | "Repository": { | ||||
"description": "Repository represents a repository", | "description": "Repository represents a repository", | ||||
"type": "object", | "type": "object", | ||||
} | } | ||||
} | } | ||||
}, | }, | ||||
"Commit": { | |||||
"description": "Commit", | |||||
"schema": { | |||||
"$ref": "#/definitions/Commit" | |||||
} | |||||
}, | |||||
"DeployKey": { | "DeployKey": { | ||||
"description": "DeployKey", | "description": "DeployKey", | ||||
"schema": { | "schema": { |
// Copyright 2015 The Gogs Authors. All rights reserved. | // 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 | // Use of this source code is governed by a MIT-style | ||||
// license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||
"bytes" | "bytes" | ||||
"container/list" | "container/list" | ||||
"fmt" | "fmt" | ||||
"io" | |||||
"net/http" | "net/http" | ||||
"strconv" | "strconv" | ||||
"strings" | "strings" | ||||
return nil, nil | 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. | // GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository. | ||||
func GetFullCommitID(repoPath, shortID string) (string, error) { | func GetFullCommitID(repoPath, shortID string) (string, error) { | ||||
if len(shortID) >= 40 { | if len(shortID) >= 40 { |
var err error | var err error | ||||
commitID, err = NewCommand("rev-parse", commitID).RunInDir(repo.Path) | commitID, err = NewCommand("rev-parse", commitID).RunInDir(repo.Path) | ||||
if err != nil { | if err != nil { | ||||
if strings.Contains(err.Error(), "unknown revision or path") { | |||||
return nil, ErrNotExist{commitID, ""} | |||||
} | |||||
return nil, err | return nil, err | ||||
} | } | ||||
} | } |
} | } | ||||
} | } | ||||
// 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 "" | return "" | ||||
} | } | ||||
url := strings.TrimSuffix(sf.refURL, ".git") | |||||
url := strings.TrimSuffix(refURL, ".git") | |||||
// git://xxx/user/repo | // git://xxx/user/repo | ||||
if strings.HasPrefix(url, "git://") { | if strings.HasPrefix(url, "git://") { | ||||
if strings.Contains(urlPrefix, url[i+1:j]) { | if strings.Contains(urlPrefix, url[i+1:j]) { | ||||
return urlPrefix + url[j+1:] | 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 "http://" + url[i+1:j] + "/" + url[j+1:] | ||||
} | } | ||||
return url | 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. | // RefID returns reference ID. | ||||
func (sf *SubModuleFile) RefID() string { | func (sf *SubModuleFile) RefID() string { | ||||
return sf.refID | return sf.refID |
entries Entries | entries Entries | ||||
entriesParsed bool | entriesParsed bool | ||||
entriesRecursive Entries | |||||
entriesRecursiveParsed bool | |||||
} | } | ||||
// NewTree create a new tree according the repository and commit id | // NewTree create a new tree according the repository and commit id | ||||
if err != nil { | if err != nil { | ||||
return nil, err | return nil, err | ||||
} | } | ||||
t.entries, err = parseTreeEntries(stdout, t) | t.entries, err = parseTreeEntries(stdout, t) | ||||
if err == nil { | |||||
t.entriesParsed = true | |||||
} | |||||
return t.entries, err | return t.entries, err | ||||
} | } | ||||
// ListEntriesRecursive returns all entries of current tree recursively including all subtrees | // ListEntriesRecursive returns all entries of current tree recursively including all subtrees | ||||
func (t *Tree) ListEntriesRecursive() (Entries, error) { | 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) | stdout, err := NewCommand("ls-tree", "-t", "-r", t.ID.String()).RunInDirBytes(t.repo.Path) | ||||
if err != nil { | if err != nil { | ||||
return nil, err | 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 | |||||
} | } |