Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API method to list all commits of a repository #6408

Merged
merged 25 commits into from Aug 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d4273db
Added API endpoint ListAllCommits (/repos/{owner}/{repo}/git/commits)
Mikescher Jun 11, 2019
3c3a6de
Merge branch 'master' into master
Mikescher Jun 12, 2019
69d6092
Merge branch 'master' into master
Mikescher Jun 14, 2019
20f9caf
Merge branch 'master' into master
Mikescher Jun 16, 2019
6a2fe66
Fixed failing drone build
Mikescher Jun 19, 2019
322d7ae
Merge branch 'master' into master
Mikescher Jun 19, 2019
55915ce
Merge branch 'master' into master
Mikescher Jul 22, 2019
f69b130
Merge branch 'master' into master
Mikescher Jul 23, 2019
35facc2
Merge branch 'master' into master
Mikescher Jul 25, 2019
413b290
Implemented requested changes (PR reviews)
Mikescher Jul 25, 2019
ab8afc4
gofmt
Mikescher Jul 25, 2019
9a3e66f
Changed api route from "/repos/{owner}/{repo}/git/commits" to "/repos…
Mikescher Jul 30, 2019
30e3006
Removed unnecessary line
Mikescher Jul 30, 2019
d306b00
better error message when git repo is empty
Mikescher Aug 2, 2019
3760625
make generate-swagger
Mikescher Aug 2, 2019
bf8315c
fixed removed return
Mikescher Aug 2, 2019
f0aadfe
Update routers/api/v1/repo/commits.go
Mikescher Aug 9, 2019
6a05550
Update routers/api/v1/repo/commits.go
Mikescher Aug 9, 2019
9cb32d6
go fmt
Mikescher Aug 9, 2019
97f2e09
Merge branch 'master' into master
lunny Aug 14, 2019
ec1f21a
Merge branch 'master' into master
sapk Aug 26, 2019
b5c9435
Refactored common code into ToCommit()
Mikescher Aug 26, 2019
0f9417f
Merge branch 'master' into master
Mikescher Aug 26, 2019
0852fa5
made toCommit not exported
Mikescher Aug 26, 2019
e91dbbd
added check for userCache == nil
Mikescher Aug 26, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 58 additions & 0 deletions integrations/api_repo_git_commits_test.go
Expand Up @@ -9,6 +9,9 @@ import (
"testing"

"code.gitea.io/gitea/models"
api "code.gitea.io/gitea/modules/structs"

"github.com/stretchr/testify/assert"
)

func TestAPIReposGitCommits(t *testing.T) {
Expand All @@ -30,3 +33,58 @@ func TestAPIReposGitCommits(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/unknown?token="+token, user.Name)
session.MakeRequest(t, req, http.StatusNotFound)
}

func TestAPIReposGitCommitList(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)

// Test getting commits (Page 1)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token, user.Name)
resp := session.MakeRequest(t, req, http.StatusOK)

var apiData []api.Commit
DecodeJSON(t, resp, &apiData)

assert.Equal(t, 3, len(apiData))
assert.Equal(t, "69554a64c1e6030f051e5c3f94bfbd773cd6a324", apiData[0].CommitMeta.SHA)
assert.Equal(t, "27566bd5738fc8b4e3fef3c5e72cce608537bd95", apiData[1].CommitMeta.SHA)
assert.Equal(t, "5099b81332712fe655e34e8dd63574f503f61811", apiData[2].CommitMeta.SHA)
}

func TestAPIReposGitCommitListPage2Empty(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)

// Test getting commits (Page=2)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token+"&page=2", user.Name)
resp := session.MakeRequest(t, req, http.StatusOK)

var apiData []api.Commit
DecodeJSON(t, resp, &apiData)

assert.Equal(t, 0, len(apiData))
}

func TestAPIReposGitCommitListDifferentBranch(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)

// Test getting commits (Page=1, Branch=good-sign)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token+"&sha=good-sign", user.Name)
resp := session.MakeRequest(t, req, http.StatusOK)

var apiData []api.Commit
DecodeJSON(t, resp, &apiData)

assert.Equal(t, 1, len(apiData))
assert.Equal(t, "f27c2b2b03dcab38beaf89b0ab4ff61f6de63441", apiData[0].CommitMeta.SHA)
}
6 changes: 6 additions & 0 deletions modules/structs/miscellaneous.go
Expand Up @@ -44,3 +44,9 @@ type MarkdownRender string
type ServerVersion struct {
Version string `json:"version"`
}

// APIError is an api error with a message
type APIError struct {
Message string `json:"message"`
URL string `json:"url"`
}
11 changes: 7 additions & 4 deletions routers/api/v1/api.go
Expand Up @@ -744,10 +744,13 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Combo("/:sha").Get(repo.GetCommitStatuses).
Post(reqToken(), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
}, reqRepoReader(models.UnitTypeCode))
m.Group("/commits/:ref", func() {
// TODO: Add m.Get("") for single commit (https://developer.github.com/v3/repos/commits/#get-a-single-commit)
m.Get("/status", repo.GetCombinedCommitStatusByRef)
m.Get("/statuses", repo.GetCommitStatusesByRef)
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("/status", repo.GetCombinedCommitStatusByRef)
m.Get("/statuses", repo.GetCommitStatusesByRef)
})
}, reqRepoReader(models.UnitTypeCode))
m.Group("/git", func() {
m.Group("/commits", func() {
Expand Down
207 changes: 185 additions & 22 deletions routers/api/v1/repo/commits.go
Expand Up @@ -6,6 +6,8 @@
package repo

import (
"math"
"strconv"
"time"

"code.gitea.io/gitea/models"
Expand Down Expand Up @@ -55,25 +57,186 @@ func GetSingleCommit(ctx *context.APIContext) {
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)
json, err := toCommit(ctx, ctx.Repo.Repository, commit, nil)
if err != nil {
ctx.ServerError("toCommit", err)
return
}

ctx.JSON(200, json)
}

// GetAllCommits get all commits via
func GetAllCommits(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/commits repository repoGetAllCommits
// ---
// summary: Get a list of all commits 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: query
// description: SHA or branch to start listing commits from (usually 'master')
// type: string
// - name: page
// in: query
// description: page number of requested commits
// type: integer
// responses:
// "200":
// "$ref": "#/responses/CommitList"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/EmptyRepository"

if ctx.Repo.Repository.IsEmpty {
ctx.JSON(409, api.APIError{
Message: "Git Repository is empty.",
URL: setting.API.SwaggerURL,
})
return
}

gitRepo, err := git.OpenRepository(ctx.Repo.Repository.RepoPath())
if err != nil {
ctx.ServerError("OpenRepository", err)
return
}

page := ctx.QueryInt("page")
if page <= 0 {
page = 1
}

sha := ctx.Query("sha")

var baseCommit *git.Commit
if len(sha) == 0 {
// no sha supplied - use default branch
head, err := gitRepo.GetHEADBranch()
if err != nil {
ctx.ServerError("GetHEADBranch", err)
return
}

baseCommit, err = gitRepo.GetBranchCommit(head.Name)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}
} else {
// get commit specified by sha
baseCommit, err = gitRepo.GetCommit(sha)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}
}

// Total commit count
commitsCountTotal, err := baseCommit.CommitsCount()
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return
}

pageCount := int(math.Ceil(float64(commitsCountTotal) / float64(git.CommitsRangeSize)))

// Query commits
commits, err := baseCommit.CommitsByRange(page)
if err != nil {
ctx.ServerError("CommitsByRange", 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

userCache := make(map[string]*models.User)

apiCommits := make([]*api.Commit, commits.Len())

i := 0
for commitPointer := commits.Front(); commitPointer != nil; commitPointer = commitPointer.Next() {
commit := commitPointer.Value.(*git.Commit)

// Create json struct
apiCommits[i], err = toCommit(ctx, ctx.Repo.Repository, commit, userCache)
if err != nil {
ctx.ServerError("toCommit", err)
return
}

i++
}

ctx.SetLinkHeader(int(commitsCountTotal), git.CommitsRangeSize)

ctx.Header().Set("X-Page", strconv.Itoa(page))
ctx.Header().Set("X-PerPage", strconv.Itoa(git.CommitsRangeSize))
ctx.Header().Set("X-Total", strconv.FormatInt(commitsCountTotal, 10))
ctx.Header().Set("X-PageCount", strconv.Itoa(pageCount))
ctx.Header().Set("X-HasMore", strconv.FormatBool(page < pageCount))

ctx.JSON(200, &apiCommits)
}

func toCommit(ctx *context.APIContext, repo *models.Repository, commit *git.Commit, userCache map[string]*models.User) (*api.Commit, error) {

var apiAuthor, apiCommitter *api.User

// Retrieve author and committer information

var cacheAuthor *models.User
var ok bool
if userCache == nil {
cacheAuthor = ((*models.User)(nil))
ok = false
} else {
cacheAuthor, ok = userCache[commit.Author.Email]
}

if ok {
apiAuthor = cacheAuthor.APIFormat()
} else {
author, err := models.GetUserByEmail(commit.Author.Email)
if err != nil && !models.IsErrUserNotExist(err) {
return nil, err
} else if err == nil {
apiAuthor = author.APIFormat()
if userCache != nil {
userCache[commit.Author.Email] = author
}
}
}

var cacheCommitter *models.User
if userCache == nil {
cacheCommitter = ((*models.User)(nil))
ok = false
} else {
cacheCommitter, ok = userCache[commit.Committer.Email]
}

if ok {
apiCommitter = cacheCommitter.APIFormat()
} else {
committer, err := models.GetUserByEmail(commit.Committer.Email)
if err != nil && !models.IsErrUserNotExist(err) {
ctx.ServerError("Get user by committer email", err)
return
return nil, err
} else if err == nil {
apiCommitter = committer.APIFormat()
if userCache != nil {
userCache[commit.Committer.Email] = committer
}
}
}

Expand All @@ -82,23 +245,23 @@ func GetSingleCommit(ctx *context.APIContext) {
for i := 0; i < commit.ParentCount(); i++ {
sha, _ := commit.ParentID(i)
apiParents[i] = &api.CommitMeta{
URL: ctx.Repo.Repository.APIURL() + "/git/commits/" + sha.String(),
URL: repo.APIURL() + "/git/commits/" + sha.String(),
SHA: sha.String(),
}
}

ctx.JSON(200, &api.Commit{
return &api.Commit{
CommitMeta: &api.CommitMeta{
URL: setting.AppURL + ctx.Link[1:],
URL: repo.APIURL() + "/git/commits/" + commit.ID.String(),
SHA: commit.ID.String(),
},
HTMLURL: ctx.Repo.Repository.HTMLURL() + "/commit/" + commit.ID.String(),
HTMLURL: repo.HTMLURL() + "/commit/" + commit.ID.String(),
RepoCommit: &api.RepoCommit{
URL: setting.AppURL + ctx.Link[1:],
URL: repo.APIURL() + "/git/commits/" + commit.ID.String(),
Author: &api.CommitUser{
Identity: api.Identity{
Name: commit.Author.Name,
Email: commit.Author.Email,
Name: commit.Committer.Name,
Email: commit.Committer.Email,
},
Date: commit.Author.When.Format(time.RFC3339),
},
Expand All @@ -109,14 +272,14 @@ func GetSingleCommit(ctx *context.APIContext) {
},
Date: commit.Committer.When.Format(time.RFC3339),
},
Message: commit.Message(),
Message: commit.Summary(),
Tree: &api.CommitMeta{
URL: ctx.Repo.Repository.APIURL() + "/git/trees/" + commit.ID.String(),
URL: repo.APIURL() + "/git/trees/" + commit.ID.String(),
SHA: commit.ID.String(),
},
},
Author: apiAuthor,
Committer: apiCommitter,
Parents: apiParents,
})
}, nil
}
29 changes: 29 additions & 0 deletions routers/api/v1/swagger/repo.go
Expand Up @@ -190,6 +190,35 @@ type swaggerCommit struct {
Body api.Commit `json:"body"`
}

// CommitList
// swagger:response CommitList
type swaggerCommitList struct {
// The current page
Page int `json:"X-Page"`

// Commits per page
PerPage int `json:"X-PerPage"`

// Total commit count
Total int `json:"X-Total"`

// Total number of pages
PageCount int `json:"X-PageCount"`

// True if there is another page
HasMore bool `json:"X-HasMore"`

//in: body
Body []api.Commit `json:"body"`
}

// EmptyRepository
// swagger:response EmptyRepository
type swaggerEmptyRepository struct {
//in: body
Body api.APIError `json:"body"`
}

// FileResponse
// swagger:response FileResponse
type swaggerFileResponse struct {
Expand Down