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 19 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions integrations/api_repo_git_commits_test.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -751,10 +751,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
192 changes: 192 additions & 0 deletions routers/api/v1/repo/commits.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
package repo

import (
"math"
"strconv"
"time"

"code.gitea.io/gitea/models"
Expand Down Expand Up @@ -120,3 +122,193 @@ func GetSingleCommit(ctx *context.APIContext) {
Parents: apiParents,
})
}

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

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)

var apiAuthor, apiCommitter *api.User

// Retrieve author and committer information
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) {
ctx.ServerError("Get user by author email", err)
return
} else if err == nil {
apiAuthor = author.APIFormat()
userCache[commit.Author.Email] = author
}
}
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
} else if err == nil {
apiCommitter = committer.APIFormat()
userCache[commit.Committer.Email] = committer
}
}

// 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(),
}
}

// Create json struct
apiCommits[i] = &api.Commit{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a sub-function similar to

func ToCommit(repo *models.Repository, c *git.Commit) *api.PayloadCommit {
That cloud be left in this file not necessarily to be put in convert module.
Furthermore, maybe de-duplicate with some parts with GetSingleCommit func.

CommitMeta: &api.CommitMeta{
URL: ctx.Repo.Repository.APIURL() + "/git/commits/" + commit.ID.String(),
SHA: commit.ID.String(),
},
HTMLURL: ctx.Repo.Repository.HTMLURL() + "/commit/" + commit.ID.String(),
RepoCommit: &api.RepoCommit{
URL: ctx.Repo.Repository.APIURL() + "/git/commits/" + commit.ID.String(),
Author: &api.CommitUser{
Identity: api.Identity{
Name: commit.Committer.Name,
Email: commit.Committer.Email,
},
Date: commit.Author.When.Format(time.RFC3339),
},
Committer: &api.CommitUser{
Identity: api.Identity{
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() + "/git/trees/" + commit.ID.String(),
SHA: commit.ID.String(),
},
},
Author: apiAuthor,
Committer: apiCommitter,
Parents: apiParents,
}

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)
}
29 changes: 29 additions & 0 deletions routers/api/v1/swagger/repo.go
Original file line number Diff line number Diff line change
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
Loading