| @@ -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 | |||
| } | |||