@@ -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" |
@@ -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) | |||
} |
@@ -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) |
@@ -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, | |||
}) | |||
} |
@@ -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"` | |||
} |
@@ -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": { |
@@ -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 { |
@@ -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 | |||
} | |||
} |
@@ -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 |
@@ -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 | |||
} |