Skip to content

Commit d4273db

Browse files
committed
Added API endpoint ListAllCommits (/repos/{owner}/{repo}/git/commits)
Signed-off-by: Mike Schwörer <[email protected]>
1 parent 74690f6 commit d4273db

File tree

4 files changed

+345
-0
lines changed

4 files changed

+345
-0
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/sdk/gitea"
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/git/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/git/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/git/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+
}

routers/api/v1/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,7 @@ func RegisterRoutes(m *macaron.Macaron) {
757757
}, reqRepoReader(models.UnitTypeCode))
758758
m.Group("/git", func() {
759759
m.Group("/commits", func() {
760+
m.Get("", repo.GetAllCommits)
760761
m.Get("/:sha", repo.GetSingleCommit)
761762
})
762763
m.Get("/refs", repo.GetGitAllRefs)

routers/api/v1/repo/commits.go

+203
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"
@@ -120,3 +122,204 @@ func GetSingleCommit(ctx *context.APIContext) {
120122
Parents: apiParents,
121123
})
122124
}
125+
126+
// GetAllCommits get all commits via
127+
func GetAllCommits(ctx *context.APIContext) {
128+
// swagger:operation GET /repos/{owner}/{repo}/git/commits repository repoGetAllCommits
129+
// ---
130+
// summary: Get a list of all commits from a repository
131+
// produces:
132+
// - application/json
133+
// parameters:
134+
// - name: owner
135+
// in: path
136+
// description: owner of the repo
137+
// type: string
138+
// required: true
139+
// - name: repo
140+
// in: path
141+
// description: name of the repo
142+
// type: string
143+
// required: true
144+
// - name: sha
145+
// in: query
146+
// description: SHA or branch to start listing commits from (usually 'master')
147+
// type: string
148+
// - name: page
149+
// in: query
150+
// description: page number of requested commits
151+
// type: integer
152+
// responses:
153+
// "200":
154+
// "$ref": "#/responses/CommitList"
155+
// "404":
156+
// "$ref": "#/responses/notFound"
157+
158+
gitRepo, err := git.OpenRepository(ctx.Repo.Repository.RepoPath())
159+
if err != nil {
160+
ctx.ServerError("OpenRepository", err)
161+
return
162+
}
163+
164+
page := ctx.QueryInt("page")
165+
if page <= 0 {
166+
page = 1
167+
}
168+
169+
sha := ctx.Query("sha")
170+
171+
var baseCommit *git.Commit
172+
if len(sha) == 0 {
173+
// no sha supplied - use default branch
174+
head, err := gitRepo.GetHEADBranch()
175+
if err != nil {
176+
ctx.ServerError("GetHEADBranch", err)
177+
return
178+
}
179+
baseCommit, err = gitRepo.GetBranchCommit(head.Name)
180+
if err != nil {
181+
ctx.ServerError("GetCommit", err)
182+
return
183+
}
184+
} else {
185+
// get commit specified by sha
186+
baseCommit, err = gitRepo.GetCommit(sha)
187+
if err != nil {
188+
ctx.ServerError("GetCommit", err)
189+
return
190+
}
191+
}
192+
193+
// Total commit count
194+
commitsCountTotal, err := baseCommit.CommitsCount()
195+
if err != nil {
196+
ctx.ServerError("GetCommitsCount", err)
197+
return
198+
}
199+
200+
pageCount := int(math.Ceil(float64(commitsCountTotal) / float64(git.CommitsRangeSize)))
201+
202+
// Query commits
203+
commits, err := baseCommit.CommitsByRange(page)
204+
if err != nil {
205+
ctx.ServerError("CommitsByRange", err)
206+
return
207+
}
208+
209+
userCache := make(map[string]*models.User)
210+
211+
apiCommits := make([]*api.Commit, commits.Len())
212+
213+
i := 0
214+
for commitPointer := commits.Front(); commitPointer != nil; commitPointer = commitPointer.Next() {
215+
commit := commitPointer.Value.(*git.Commit)
216+
217+
var apiAuthor, apiCommitter *api.User
218+
219+
// Retrieve author and committer information
220+
cacheAuthor, ok := userCache[commit.Author.Email]
221+
if ok {
222+
apiAuthor = cacheAuthor.APIFormat()
223+
} else {
224+
author, err := models.GetUserByEmail(commit.Author.Email)
225+
if err != nil && !models.IsErrUserNotExist(err) {
226+
ctx.ServerError("Get user by author email", err)
227+
return
228+
} else if err == nil {
229+
apiAuthor = author.APIFormat()
230+
userCache[commit.Author.Email] = author
231+
}
232+
}
233+
cacheCommitter, ok := userCache[commit.Committer.Email]
234+
if ok {
235+
apiCommitter = cacheCommitter.APIFormat()
236+
} else {
237+
committer, err := models.GetUserByEmail(commit.Committer.Email)
238+
if err != nil && !models.IsErrUserNotExist(err) {
239+
ctx.ServerError("Get user by committer email", err)
240+
return
241+
} else if err == nil {
242+
apiCommitter = committer.APIFormat()
243+
userCache[commit.Committer.Email] = committer
244+
}
245+
}
246+
247+
// Retrieve parent(s) of the commit
248+
apiParents := make([]*api.CommitMeta, commit.ParentCount())
249+
for i := 0; i < commit.ParentCount(); i++ {
250+
sha, _ := commit.ParentID(i)
251+
apiParents[i] = &api.CommitMeta{
252+
URL: ctx.Repo.Repository.APIURL() + "/git/commits/" + sha.String(),
253+
SHA: sha.String(),
254+
}
255+
}
256+
257+
// Create json struct
258+
apiCommits[i] = &api.Commit{
259+
CommitMeta: &api.CommitMeta{
260+
URL: ctx.Repo.Repository.APIURL() + "/git/commits/" + commit.ID.String(),
261+
SHA: commit.ID.String(),
262+
},
263+
HTMLURL: ctx.Repo.Repository.HTMLURL() + "/commits/" + commit.ID.String(),
264+
RepoCommit: &api.RepoCommit{
265+
URL: setting.AppURL + ctx.Link[1:],
266+
Author: &api.CommitUser{
267+
Identity: api.Identity{
268+
Name: commit.Committer.Name,
269+
Email: commit.Committer.Email,
270+
},
271+
Date: commit.Author.When.Format(time.RFC3339),
272+
},
273+
Committer: &api.CommitUser{
274+
Identity: api.Identity{
275+
Name: commit.Committer.Name,
276+
Email: commit.Committer.Email,
277+
},
278+
Date: commit.Committer.When.Format(time.RFC3339),
279+
},
280+
Message: commit.Summary(),
281+
Tree: &api.CommitMeta{
282+
URL: ctx.Repo.Repository.APIURL() + "/trees/" + commit.ID.String(),
283+
SHA: commit.ID.String(),
284+
},
285+
},
286+
Author: apiAuthor,
287+
Committer: apiCommitter,
288+
Parents: apiParents,
289+
}
290+
291+
i++
292+
}
293+
294+
ctx.SetLinkHeader(int(commitsCountTotal), git.CommitsRangeSize)
295+
296+
ctx.Header().Set("X-Page", strconv.Itoa(page))
297+
ctx.Header().Set("X-PerPage", strconv.Itoa(git.CommitsRangeSize))
298+
ctx.Header().Set("X-Total", strconv.FormatInt(commitsCountTotal, 10))
299+
ctx.Header().Set("X-PageCount", strconv.Itoa(pageCount))
300+
ctx.Header().Set("X-HasMore", strconv.FormatBool(page < pageCount))
301+
302+
ctx.JSON(200, &apiCommits)
303+
}
304+
305+
// CommitList
306+
// swagger:response CommitList
307+
type swaggerCommitList struct {
308+
// The current page
309+
Page int `json:"X-Page"`
310+
311+
// Commits per page
312+
PerPage int `json:"X-PerPage"`
313+
314+
// Total commit count
315+
Total int `json:"X-Total"`
316+
317+
// Total number of pages
318+
PageCount int `json:"X-PageCount"`
319+
320+
// True if there is another page
321+
HasMore bool `json:"X-HasMore"`
322+
323+
//in: body
324+
Body []api.Commit `json:"body"`
325+
}

templates/swagger/v1_json.tmpl

+83
Original file line numberDiff line numberDiff line change
@@ -1914,6 +1914,54 @@
19141914
}
19151915
}
19161916
},
1917+
"/repos/{owner}/{repo}/git/commits": {
1918+
"get": {
1919+
"produces": [
1920+
"application/json"
1921+
],
1922+
"tags": [
1923+
"repository"
1924+
],
1925+
"summary": "Get a list of all commits from a repository",
1926+
"operationId": "repoGetAllCommits",
1927+
"parameters": [
1928+
{
1929+
"type": "string",
1930+
"description": "owner of the repo",
1931+
"name": "owner",
1932+
"in": "path",
1933+
"required": true
1934+
},
1935+
{
1936+
"type": "string",
1937+
"description": "name of the repo",
1938+
"name": "repo",
1939+
"in": "path",
1940+
"required": true
1941+
},
1942+
{
1943+
"type": "string",
1944+
"description": "SHA or branch to start listing commits from (usually 'master')",
1945+
"name": "sha",
1946+
"in": "query"
1947+
},
1948+
{
1949+
"type": "integer",
1950+
"description": "page number of requested commits",
1951+
"name": "page",
1952+
"in": "query"
1953+
}
1954+
],
1955+
"responses": {
1956+
"200": {
1957+
"$ref": "#/responses/CommitList"
1958+
},
1959+
"404": {
1960+
"$ref": "#/responses/notFound"
1961+
}
1962+
}
1963+
}
1964+
},
19171965
"/repos/{owner}/{repo}/git/commits/{sha}": {
19181966
"get": {
19191967
"produces": [
@@ -9876,6 +9924,41 @@
98769924
"$ref": "#/definitions/Commit"
98779925
}
98789926
},
9927+
"CommitList": {
9928+
"description": "CommitList",
9929+
"schema": {
9930+
"type": "array",
9931+
"items": {
9932+
"$ref": "#/definitions/Commit"
9933+
}
9934+
},
9935+
"headers": {
9936+
"X-HasMore": {
9937+
"type": "boolean",
9938+
"description": "True if there is another page"
9939+
},
9940+
"X-Page": {
9941+
"type": "integer",
9942+
"format": "int64",
9943+
"description": "The current page"
9944+
},
9945+
"X-PageCount": {
9946+
"type": "integer",
9947+
"format": "int64",
9948+
"description": "Total number of pages"
9949+
},
9950+
"X-PerPage": {
9951+
"type": "integer",
9952+
"format": "int64",
9953+
"description": "Commits per page"
9954+
},
9955+
"X-Total": {
9956+
"type": "integer",
9957+
"format": "int64",
9958+
"description": "Total commit count"
9959+
}
9960+
}
9961+
},
98799962
"DeployKey": {
98809963
"description": "DeployKey",
98819964
"schema": {

0 commit comments

Comments
 (0)