Skip to content

Commit 042089f

Browse files
Mikeschersapk
authored andcommitted
API method to list all commits of a repository (#6408)
* Added API endpoint ListAllCommits (/repos/{owner}/{repo}/git/commits) Signed-off-by: Mike Schwörer <mailport@mikescher.de> * Fixed failing drone build Signed-off-by: Mike Schwörer <mailport@mikescher.de> * Implemented requested changes (PR reviews) Signed-off-by: Mike Schwörer <mailport@mikescher.de> * gofmt Signed-off-by: Mike Schwörer <mailport@mikescher.de> * Changed api route from "/repos/{owner}/{repo}/git/commits" to "/repos/{owner}/{repo}/commits" * Removed unnecessary line * better error message when git repo is empty * make generate-swagger * fixed removed return * Update routers/api/v1/repo/commits.go Co-Authored-By: Lauris BH <lauris@nix.lv> * Update routers/api/v1/repo/commits.go Co-Authored-By: Lauris BH <lauris@nix.lv> * go fmt * Refactored common code into ToCommit() * made toCommit not exported * added check for userCache == nil
1 parent 6b3f52f commit 042089f

File tree

6 files changed

+392
-26
lines changed

6 files changed

+392
-26
lines changed

integrations/api_repo_git_commits_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import (
99
"testing"
1010

1111
"code.gitea.io/gitea/models"
12+
api "code.gitea.io/gitea/modules/structs"
13+
14+
"github.com/stretchr/testify/assert"
1215
)
1316

1417
func TestAPIReposGitCommits(t *testing.T) {
@@ -30,3 +33,58 @@ func TestAPIReposGitCommits(t *testing.T) {
3033
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/unknown?token="+token, user.Name)
3134
session.MakeRequest(t, req, http.StatusNotFound)
3235
}
36+
37+
func TestAPIReposGitCommitList(t *testing.T) {
38+
prepareTestEnv(t)
39+
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
40+
// Login as User2.
41+
session := loginUser(t, user.Name)
42+
token := getTokenForLoggedInUser(t, session)
43+
44+
// Test getting commits (Page 1)
45+
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token, user.Name)
46+
resp := session.MakeRequest(t, req, http.StatusOK)
47+
48+
var apiData []api.Commit
49+
DecodeJSON(t, resp, &apiData)
50+
51+
assert.Equal(t, 3, len(apiData))
52+
assert.Equal(t, "69554a64c1e6030f051e5c3f94bfbd773cd6a324", apiData[0].CommitMeta.SHA)
53+
assert.Equal(t, "27566bd5738fc8b4e3fef3c5e72cce608537bd95", apiData[1].CommitMeta.SHA)
54+
assert.Equal(t, "5099b81332712fe655e34e8dd63574f503f61811", apiData[2].CommitMeta.SHA)
55+
}
56+
57+
func TestAPIReposGitCommitListPage2Empty(t *testing.T) {
58+
prepareTestEnv(t)
59+
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
60+
// Login as User2.
61+
session := loginUser(t, user.Name)
62+
token := getTokenForLoggedInUser(t, session)
63+
64+
// Test getting commits (Page=2)
65+
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token+"&page=2", user.Name)
66+
resp := session.MakeRequest(t, req, http.StatusOK)
67+
68+
var apiData []api.Commit
69+
DecodeJSON(t, resp, &apiData)
70+
71+
assert.Equal(t, 0, len(apiData))
72+
}
73+
74+
func TestAPIReposGitCommitListDifferentBranch(t *testing.T) {
75+
prepareTestEnv(t)
76+
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
77+
// Login as User2.
78+
session := loginUser(t, user.Name)
79+
token := getTokenForLoggedInUser(t, session)
80+
81+
// Test getting commits (Page=1, Branch=good-sign)
82+
req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token+"&sha=good-sign", user.Name)
83+
resp := session.MakeRequest(t, req, http.StatusOK)
84+
85+
var apiData []api.Commit
86+
DecodeJSON(t, resp, &apiData)
87+
88+
assert.Equal(t, 1, len(apiData))
89+
assert.Equal(t, "f27c2b2b03dcab38beaf89b0ab4ff61f6de63441", apiData[0].CommitMeta.SHA)
90+
}

modules/structs/miscellaneous.go

+6
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,9 @@ type MarkdownRender string
4444
type ServerVersion struct {
4545
Version string `json:"version"`
4646
}
47+
48+
// APIError is an api error with a message
49+
type APIError struct {
50+
Message string `json:"message"`
51+
URL string `json:"url"`
52+
}

routers/api/v1/api.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -744,10 +744,13 @@ func RegisterRoutes(m *macaron.Macaron) {
744744
m.Combo("/:sha").Get(repo.GetCommitStatuses).
745745
Post(reqToken(), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
746746
}, reqRepoReader(models.UnitTypeCode))
747-
m.Group("/commits/:ref", func() {
748-
// TODO: Add m.Get("") for single commit (https://developer.github.com/v3/repos/commits/#get-a-single-commit)
749-
m.Get("/status", repo.GetCombinedCommitStatusByRef)
750-
m.Get("/statuses", repo.GetCommitStatusesByRef)
747+
m.Group("/commits", func() {
748+
m.Get("", repo.GetAllCommits)
749+
m.Group("/:ref", func() {
750+
// TODO: Add m.Get("") for single commit (https://developer.github.com/v3/repos/commits/#get-a-single-commit)
751+
m.Get("/status", repo.GetCombinedCommitStatusByRef)
752+
m.Get("/statuses", repo.GetCommitStatusesByRef)
753+
})
751754
}, reqRepoReader(models.UnitTypeCode))
752755
m.Group("/git", func() {
753756
m.Group("/commits", func() {

routers/api/v1/repo/commits.go

+185-22
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package repo
77

88
import (
9+
"math"
10+
"strconv"
911
"time"
1012

1113
"code.gitea.io/gitea/models"
@@ -55,25 +57,186 @@ func GetSingleCommit(ctx *context.APIContext) {
5557
return
5658
}
5759

58-
// Retrieve author and committer information
59-
var apiAuthor, apiCommitter *api.User
60-
author, err := models.GetUserByEmail(commit.Author.Email)
61-
if err != nil && !models.IsErrUserNotExist(err) {
62-
ctx.ServerError("Get user by author email", err)
60+
json, err := toCommit(ctx, ctx.Repo.Repository, commit, nil)
61+
if err != nil {
62+
ctx.ServerError("toCommit", err)
63+
return
64+
}
65+
66+
ctx.JSON(200, json)
67+
}
68+
69+
// GetAllCommits get all commits via
70+
func GetAllCommits(ctx *context.APIContext) {
71+
// swagger:operation GET /repos/{owner}/{repo}/commits repository repoGetAllCommits
72+
// ---
73+
// summary: Get a list of all commits from a repository
74+
// produces:
75+
// - application/json
76+
// parameters:
77+
// - name: owner
78+
// in: path
79+
// description: owner of the repo
80+
// type: string
81+
// required: true
82+
// - name: repo
83+
// in: path
84+
// description: name of the repo
85+
// type: string
86+
// required: true
87+
// - name: sha
88+
// in: query
89+
// description: SHA or branch to start listing commits from (usually 'master')
90+
// type: string
91+
// - name: page
92+
// in: query
93+
// description: page number of requested commits
94+
// type: integer
95+
// responses:
96+
// "200":
97+
// "$ref": "#/responses/CommitList"
98+
// "404":
99+
// "$ref": "#/responses/notFound"
100+
// "409":
101+
// "$ref": "#/responses/EmptyRepository"
102+
103+
if ctx.Repo.Repository.IsEmpty {
104+
ctx.JSON(409, api.APIError{
105+
Message: "Git Repository is empty.",
106+
URL: setting.API.SwaggerURL,
107+
})
108+
return
109+
}
110+
111+
gitRepo, err := git.OpenRepository(ctx.Repo.Repository.RepoPath())
112+
if err != nil {
113+
ctx.ServerError("OpenRepository", err)
114+
return
115+
}
116+
117+
page := ctx.QueryInt("page")
118+
if page <= 0 {
119+
page = 1
120+
}
121+
122+
sha := ctx.Query("sha")
123+
124+
var baseCommit *git.Commit
125+
if len(sha) == 0 {
126+
// no sha supplied - use default branch
127+
head, err := gitRepo.GetHEADBranch()
128+
if err != nil {
129+
ctx.ServerError("GetHEADBranch", err)
130+
return
131+
}
132+
133+
baseCommit, err = gitRepo.GetBranchCommit(head.Name)
134+
if err != nil {
135+
ctx.ServerError("GetCommit", err)
136+
return
137+
}
138+
} else {
139+
// get commit specified by sha
140+
baseCommit, err = gitRepo.GetCommit(sha)
141+
if err != nil {
142+
ctx.ServerError("GetCommit", err)
143+
return
144+
}
145+
}
146+
147+
// Total commit count
148+
commitsCountTotal, err := baseCommit.CommitsCount()
149+
if err != nil {
150+
ctx.ServerError("GetCommitsCount", err)
151+
return
152+
}
153+
154+
pageCount := int(math.Ceil(float64(commitsCountTotal) / float64(git.CommitsRangeSize)))
155+
156+
// Query commits
157+
commits, err := baseCommit.CommitsByRange(page)
158+
if err != nil {
159+
ctx.ServerError("CommitsByRange", err)
63160
return
64-
} else if err == nil {
65-
apiAuthor = author.APIFormat()
66161
}
67-
// Save one query if the author is also the committer
68-
if commit.Committer.Email == commit.Author.Email {
69-
apiCommitter = apiAuthor
162+
163+
userCache := make(map[string]*models.User)
164+
165+
apiCommits := make([]*api.Commit, commits.Len())
166+
167+
i := 0
168+
for commitPointer := commits.Front(); commitPointer != nil; commitPointer = commitPointer.Next() {
169+
commit := commitPointer.Value.(*git.Commit)
170+
171+
// Create json struct
172+
apiCommits[i], err = toCommit(ctx, ctx.Repo.Repository, commit, userCache)
173+
if err != nil {
174+
ctx.ServerError("toCommit", err)
175+
return
176+
}
177+
178+
i++
179+
}
180+
181+
ctx.SetLinkHeader(int(commitsCountTotal), git.CommitsRangeSize)
182+
183+
ctx.Header().Set("X-Page", strconv.Itoa(page))
184+
ctx.Header().Set("X-PerPage", strconv.Itoa(git.CommitsRangeSize))
185+
ctx.Header().Set("X-Total", strconv.FormatInt(commitsCountTotal, 10))
186+
ctx.Header().Set("X-PageCount", strconv.Itoa(pageCount))
187+
ctx.Header().Set("X-HasMore", strconv.FormatBool(page < pageCount))
188+
189+
ctx.JSON(200, &apiCommits)
190+
}
191+
192+
func toCommit(ctx *context.APIContext, repo *models.Repository, commit *git.Commit, userCache map[string]*models.User) (*api.Commit, error) {
193+
194+
var apiAuthor, apiCommitter *api.User
195+
196+
// Retrieve author and committer information
197+
198+
var cacheAuthor *models.User
199+
var ok bool
200+
if userCache == nil {
201+
cacheAuthor = ((*models.User)(nil))
202+
ok = false
203+
} else {
204+
cacheAuthor, ok = userCache[commit.Author.Email]
205+
}
206+
207+
if ok {
208+
apiAuthor = cacheAuthor.APIFormat()
209+
} else {
210+
author, err := models.GetUserByEmail(commit.Author.Email)
211+
if err != nil && !models.IsErrUserNotExist(err) {
212+
return nil, err
213+
} else if err == nil {
214+
apiAuthor = author.APIFormat()
215+
if userCache != nil {
216+
userCache[commit.Author.Email] = author
217+
}
218+
}
219+
}
220+
221+
var cacheCommitter *models.User
222+
if userCache == nil {
223+
cacheCommitter = ((*models.User)(nil))
224+
ok = false
225+
} else {
226+
cacheCommitter, ok = userCache[commit.Committer.Email]
227+
}
228+
229+
if ok {
230+
apiCommitter = cacheCommitter.APIFormat()
70231
} else {
71232
committer, err := models.GetUserByEmail(commit.Committer.Email)
72233
if err != nil && !models.IsErrUserNotExist(err) {
73-
ctx.ServerError("Get user by committer email", err)
74-
return
234+
return nil, err
75235
} else if err == nil {
76236
apiCommitter = committer.APIFormat()
237+
if userCache != nil {
238+
userCache[commit.Committer.Email] = committer
239+
}
77240
}
78241
}
79242

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

90-
ctx.JSON(200, &api.Commit{
253+
return &api.Commit{
91254
CommitMeta: &api.CommitMeta{
92-
URL: setting.AppURL + ctx.Link[1:],
255+
URL: repo.APIURL() + "/git/commits/" + commit.ID.String(),
93256
SHA: commit.ID.String(),
94257
},
95-
HTMLURL: ctx.Repo.Repository.HTMLURL() + "/commit/" + commit.ID.String(),
258+
HTMLURL: repo.HTMLURL() + "/commit/" + commit.ID.String(),
96259
RepoCommit: &api.RepoCommit{
97-
URL: setting.AppURL + ctx.Link[1:],
260+
URL: repo.APIURL() + "/git/commits/" + commit.ID.String(),
98261
Author: &api.CommitUser{
99262
Identity: api.Identity{
100-
Name: commit.Author.Name,
101-
Email: commit.Author.Email,
263+
Name: commit.Committer.Name,
264+
Email: commit.Committer.Email,
102265
},
103266
Date: commit.Author.When.Format(time.RFC3339),
104267
},
@@ -109,14 +272,14 @@ func GetSingleCommit(ctx *context.APIContext) {
109272
},
110273
Date: commit.Committer.When.Format(time.RFC3339),
111274
},
112-
Message: commit.Message(),
275+
Message: commit.Summary(),
113276
Tree: &api.CommitMeta{
114-
URL: ctx.Repo.Repository.APIURL() + "/git/trees/" + commit.ID.String(),
277+
URL: repo.APIURL() + "/git/trees/" + commit.ID.String(),
115278
SHA: commit.ID.String(),
116279
},
117280
},
118281
Author: apiAuthor,
119282
Committer: apiCommitter,
120283
Parents: apiParents,
121-
})
284+
}, nil
122285
}

routers/api/v1/swagger/repo.go

+29
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,35 @@ type swaggerCommit struct {
190190
Body api.Commit `json:"body"`
191191
}
192192

193+
// CommitList
194+
// swagger:response CommitList
195+
type swaggerCommitList struct {
196+
// The current page
197+
Page int `json:"X-Page"`
198+
199+
// Commits per page
200+
PerPage int `json:"X-PerPage"`
201+
202+
// Total commit count
203+
Total int `json:"X-Total"`
204+
205+
// Total number of pages
206+
PageCount int `json:"X-PageCount"`
207+
208+
// True if there is another page
209+
HasMore bool `json:"X-HasMore"`
210+
211+
//in: body
212+
Body []api.Commit `json:"body"`
213+
}
214+
215+
// EmptyRepository
216+
// swagger:response EmptyRepository
217+
type swaggerEmptyRepository struct {
218+
//in: body
219+
Body api.APIError `json:"body"`
220+
}
221+
193222
// FileResponse
194223
// swagger:response FileResponse
195224
type swaggerFileResponse struct {

0 commit comments

Comments
 (0)