Skip to content

Commit 6455c82

Browse files
rickymawxiaoguang
andauthored
Support getting last commit message using contents-ext API (#34904)
Fix #34870 Fix #34929 --------- Co-authored-by: wxiaoguang <[email protected]>
1 parent 97fc87a commit 6455c82

File tree

11 files changed

+141
-150
lines changed

11 files changed

+141
-150
lines changed

modules/structs/repo_file.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,17 @@ type ContentsExtResponse struct {
116116

117117
// ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content
118118
type ContentsResponse struct {
119-
Name string `json:"name"`
120-
Path string `json:"path"`
121-
SHA string `json:"sha"`
122-
LastCommitSHA string `json:"last_commit_sha"`
119+
Name string `json:"name"`
120+
Path string `json:"path"`
121+
SHA string `json:"sha"`
122+
123+
LastCommitSHA *string `json:"last_commit_sha,omitempty"`
123124
// swagger:strfmt date-time
124-
LastCommitterDate time.Time `json:"last_committer_date"`
125+
LastCommitterDate *time.Time `json:"last_committer_date,omitempty"`
125126
// swagger:strfmt date-time
126-
LastAuthorDate time.Time `json:"last_author_date"`
127+
LastAuthorDate *time.Time `json:"last_author_date,omitempty"`
128+
LastCommitMessage *string `json:"last_commit_message,omitempty"`
129+
127130
// `type` will be `file`, `dir`, `symlink`, or `submodule`
128131
Type string `json:"type"`
129132
Size int64 `json:"size"`
@@ -141,8 +144,8 @@ type ContentsResponse struct {
141144
SubmoduleGitURL *string `json:"submodule_git_url"`
142145
Links *FileLinksResponse `json:"_links"`
143146

144-
LfsOid *string `json:"lfs_oid"`
145-
LfsSize *int64 `json:"lfs_size"`
147+
LfsOid *string `json:"lfs_oid,omitempty"`
148+
LfsSize *int64 `json:"lfs_size,omitempty"`
146149
}
147150

148151
// FileCommitResponse contains information generated from a Git commit for a repo's file.

routers/api/v1/repo/file.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,8 @@ func GetContentsExt(ctx *context.APIContext) {
812812
// required: true
813813
// - name: filepath
814814
// in: path
815-
// description: path of the dir, file, symlink or submodule in the repo
815+
// description: path of the dir, file, symlink or submodule in the repo. Swagger requires path parameter to be "required",
816+
// you can leave it empty or pass a single dot (".") to get the root directory.
816817
// type: string
817818
// required: true
818819
// - name: ref
@@ -823,7 +824,8 @@ func GetContentsExt(ctx *context.APIContext) {
823824
// - name: includes
824825
// in: query
825826
// description: By default this API's response only contains file's metadata. Use comma-separated "includes" options to retrieve more fields.
826-
// Option "file_content" will try to retrieve the file content, option "lfs_metadata" will try to retrieve LFS metadata.
827+
// Option "file_content" will try to retrieve the file content, "lfs_metadata" will try to retrieve LFS metadata,
828+
// "commit_metadata" will try to retrieve commit metadata, and "commit_message" will try to retrieve commit message.
827829
// type: string
828830
// required: false
829831
// responses:
@@ -832,6 +834,9 @@ func GetContentsExt(ctx *context.APIContext) {
832834
// "404":
833835
// "$ref": "#/responses/notFound"
834836

837+
if treePath := ctx.PathParam("*"); treePath == "." || treePath == "/" {
838+
ctx.SetPathParam("*", "") // workaround for swagger, it requires path parameter to be "required", but we need to list root directory
839+
}
835840
opts := files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*")}
836841
for includeOpt := range strings.SplitSeq(ctx.FormString("includes"), ",") {
837842
if includeOpt == "" {
@@ -842,6 +847,10 @@ func GetContentsExt(ctx *context.APIContext) {
842847
opts.IncludeSingleFileContent = true
843848
case "lfs_metadata":
844849
opts.IncludeLfsMetadata = true
850+
case "commit_metadata":
851+
opts.IncludeCommitMetadata = true
852+
case "commit_message":
853+
opts.IncludeCommitMessage = true
845854
default:
846855
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("unknown include option %q", includeOpt))
847856
return
@@ -883,7 +892,11 @@ func GetContents(ctx *context.APIContext) {
883892
// "$ref": "#/responses/ContentsResponse"
884893
// "404":
885894
// "$ref": "#/responses/notFound"
886-
ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*"), IncludeSingleFileContent: true})
895+
ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{
896+
TreePath: ctx.PathParam("*"),
897+
IncludeSingleFileContent: true,
898+
IncludeCommitMetadata: true,
899+
})
887900
if ctx.Written() {
888901
return
889902
}

services/repository/files/content.go

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ type GetContentsOrListOptions struct {
3939
TreePath string
4040
IncludeSingleFileContent bool // include the file's content when the tree path is a file
4141
IncludeLfsMetadata bool
42+
IncludeCommitMetadata bool
43+
IncludeCommitMessage bool
4244
}
4345

4446
// GetContentsOrList gets the metadata of a file's contents (*ContentsResponse) if treePath not a tree
@@ -132,39 +134,46 @@ func getFileContentsByEntryInternal(_ context.Context, repo *repo_model.Reposito
132134
}
133135
selfURLString := selfURL.String()
134136

135-
err = gitRepo.AddLastCommitCache(repo.GetCommitsCountCacheKey(refCommit.InputRef, refType != git.RefTypeCommit), repo.FullName(), refCommit.CommitID)
136-
if err != nil {
137-
return nil, err
138-
}
139-
140-
lastCommit, err := refCommit.Commit.GetCommitByPath(opts.TreePath)
141-
if err != nil {
142-
return nil, err
143-
}
144-
145137
// All content types have these fields in populated
146138
contentsResponse := &api.ContentsResponse{
147-
Name: entry.Name(),
148-
Path: opts.TreePath,
149-
SHA: entry.ID.String(),
150-
LastCommitSHA: lastCommit.ID.String(),
151-
Size: entry.Size(),
152-
URL: &selfURLString,
139+
Name: entry.Name(),
140+
Path: opts.TreePath,
141+
SHA: entry.ID.String(),
142+
Size: entry.Size(),
143+
URL: &selfURLString,
153144
Links: &api.FileLinksResponse{
154145
Self: &selfURLString,
155146
},
156147
}
157148

158-
// GitHub doesn't have these fields in the response, but we could follow other similar APIs to name them
159-
// https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits
160-
if lastCommit.Committer != nil {
161-
contentsResponse.LastCommitterDate = lastCommit.Committer.When
162-
}
163-
if lastCommit.Author != nil {
164-
contentsResponse.LastAuthorDate = lastCommit.Author.When
149+
if opts.IncludeCommitMetadata || opts.IncludeCommitMessage {
150+
err = gitRepo.AddLastCommitCache(repo.GetCommitsCountCacheKey(refCommit.InputRef, refType != git.RefTypeCommit), repo.FullName(), refCommit.CommitID)
151+
if err != nil {
152+
return nil, err
153+
}
154+
155+
lastCommit, err := refCommit.Commit.GetCommitByPath(opts.TreePath)
156+
if err != nil {
157+
return nil, err
158+
}
159+
160+
if opts.IncludeCommitMetadata {
161+
contentsResponse.LastCommitSHA = util.ToPointer(lastCommit.ID.String())
162+
// GitHub doesn't have these fields in the response, but we could follow other similar APIs to name them
163+
// https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits
164+
if lastCommit.Committer != nil {
165+
contentsResponse.LastCommitterDate = util.ToPointer(lastCommit.Committer.When)
166+
}
167+
if lastCommit.Author != nil {
168+
contentsResponse.LastAuthorDate = util.ToPointer(lastCommit.Author.When)
169+
}
170+
}
171+
if opts.IncludeCommitMessage {
172+
contentsResponse.LastCommitMessage = util.ToPointer(lastCommit.Message())
173+
}
165174
}
166175

167-
// Now populate the rest of the ContentsResponse based on entry type
176+
// Now populate the rest of the ContentsResponse based on the entry type
168177
if entry.IsRegular() || entry.IsExecutable() {
169178
contentsResponse.Type = string(ContentTypeRegular)
170179
// if it is listing the repo root dir, don't waste system resources on reading content

services/repository/files/content_test.go

Lines changed: 1 addition & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5,56 +5,21 @@ package files
55

66
import (
77
"testing"
8-
"time"
98

109
"code.gitea.io/gitea/models/unittest"
1110
api "code.gitea.io/gitea/modules/structs"
1211
"code.gitea.io/gitea/modules/util"
13-
"code.gitea.io/gitea/routers/api/v1/utils"
1412
"code.gitea.io/gitea/services/contexttest"
1513

1614
_ "code.gitea.io/gitea/models/actions"
1715

1816
"github.com/stretchr/testify/assert"
19-
"github.com/stretchr/testify/require"
2017
)
2118

2219
func TestMain(m *testing.M) {
2320
unittest.MainTest(m)
2421
}
2522

26-
func getExpectedReadmeContentsResponse() *api.ContentsResponse {
27-
treePath := "README.md"
28-
sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
29-
encoding := "base64"
30-
content := "IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"
31-
selfURL := "https://try.gitea.io/api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master"
32-
htmlURL := "https://try.gitea.io/user2/repo1/src/branch/master/" + treePath
33-
gitURL := "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/" + sha
34-
downloadURL := "https://try.gitea.io/user2/repo1/raw/branch/master/" + treePath
35-
return &api.ContentsResponse{
36-
Name: treePath,
37-
Path: treePath,
38-
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
39-
LastCommitSHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
40-
LastCommitterDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)),
41-
LastAuthorDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)),
42-
Type: "file",
43-
Size: 30,
44-
Encoding: &encoding,
45-
Content: &content,
46-
URL: &selfURL,
47-
HTMLURL: &htmlURL,
48-
GitURL: &gitURL,
49-
DownloadURL: &downloadURL,
50-
Links: &api.FileLinksResponse{
51-
Self: &selfURL,
52-
GitURL: &gitURL,
53-
HTMLURL: &htmlURL,
54-
},
55-
}
56-
}
57-
5823
func TestGetContents(t *testing.T) {
5924
unittest.PrepareTestEnv(t)
6025
ctx, _ := contexttest.MockContext(t, "user2/repo1")
@@ -63,45 +28,8 @@ func TestGetContents(t *testing.T) {
6328
contexttest.LoadRepoCommit(t, ctx)
6429
contexttest.LoadUser(t, ctx, 2)
6530
contexttest.LoadGitRepo(t, ctx)
66-
defer ctx.Repo.GitRepo.Close()
67-
repo, gitRepo := ctx.Repo.Repository, ctx.Repo.GitRepo
68-
refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch)
69-
require.NoError(t, err)
70-
71-
t.Run("GetContentsOrList(README.md)-MetaOnly", func(t *testing.T) {
72-
expectedContentsResponse := getExpectedReadmeContentsResponse()
73-
expectedContentsResponse.Encoding = nil // because will be in a list, doesn't have encoding and content
74-
expectedContentsResponse.Content = nil
75-
extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "README.md", IncludeSingleFileContent: false})
76-
assert.Equal(t, expectedContentsResponse, extResp.FileContents)
77-
assert.NoError(t, err)
78-
})
79-
80-
t.Run("GetContentsOrList(README.md)", func(t *testing.T) {
81-
expectedContentsResponse := getExpectedReadmeContentsResponse()
82-
extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "README.md", IncludeSingleFileContent: true})
83-
assert.Equal(t, expectedContentsResponse, extResp.FileContents)
84-
assert.NoError(t, err)
85-
})
86-
87-
t.Run("GetContentsOrList(RootDir)", func(t *testing.T) {
88-
readmeContentsResponse := getExpectedReadmeContentsResponse()
89-
readmeContentsResponse.Encoding = nil // because will be in a list, doesn't have encoding and content
90-
readmeContentsResponse.Content = nil
91-
expectedContentsListResponse := []*api.ContentsResponse{readmeContentsResponse}
92-
// even if IncludeFileContent is true, it has no effect for directory listing
93-
extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "", IncludeSingleFileContent: true})
94-
assert.Equal(t, expectedContentsListResponse, extResp.DirContents)
95-
assert.NoError(t, err)
96-
})
9731

98-
t.Run("GetContentsOrList(NoSuchTreePath)", func(t *testing.T) {
99-
extResp, err := GetContentsOrList(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: "no-such/file.md"})
100-
assert.Error(t, err)
101-
assert.EqualError(t, err, "object does not exist [id: , rel_path: no-such]")
102-
assert.Nil(t, extResp.DirContents)
103-
assert.Nil(t, extResp.FileContents)
104-
})
32+
// GetContentsOrList's behavior is fully tested in integration tests, so we don't need to test it here.
10533

10634
t.Run("GetBlobBySHA", func(t *testing.T) {
10735
sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"

services/repository/files/file.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ import (
2222
func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treePaths []string) (files []*api.ContentsResponse) {
2323
var size int64
2424
for _, treePath := range treePaths {
25-
fileContents, _ := GetFileContents(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{TreePath: treePath, IncludeSingleFileContent: true}) // ok if fails, then will be nil
25+
// ok if fails, then will be nil
26+
fileContents, _ := GetFileContents(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{
27+
TreePath: treePath,
28+
IncludeSingleFileContent: true,
29+
IncludeCommitMetadata: true,
30+
})
2631
if fileContents != nil && fileContents.Content != nil && *fileContents.Content != "" {
2732
// if content isn't empty (e.g., due to the single blob being too large), add file size to response size
2833
size += int64(len(*fileContents.Content))

templates/swagger/v1_json.tmpl

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/api_repo_file_create_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"code.gitea.io/gitea/modules/gitrepo"
2020
"code.gitea.io/gitea/modules/setting"
2121
api "code.gitea.io/gitea/modules/structs"
22+
"code.gitea.io/gitea/modules/util"
2223
"code.gitea.io/gitea/services/context"
2324

2425
"github.com/stretchr/testify/assert"
@@ -52,8 +53,8 @@ func getCreateFileOptions() api.CreateFileOptions {
5253
func normalizeFileContentResponseCommitTime(c *api.ContentsResponse) {
5354
// decoded JSON response may contain different timezone from the one parsed by git commit
5455
// so we need to normalize the time to UTC to make "assert.Equal" pass
55-
c.LastCommitterDate = c.LastCommitterDate.UTC()
56-
c.LastAuthorDate = c.LastAuthorDate.UTC()
56+
c.LastCommitterDate = util.ToPointer(c.LastCommitterDate.UTC())
57+
c.LastAuthorDate = util.ToPointer(c.LastAuthorDate.UTC())
5758
}
5859

5960
type apiFileResponseInfo struct {
@@ -74,9 +75,9 @@ func getExpectedFileResponseForCreate(info apiFileResponseInfo) *api.FileRespons
7475
Name: path.Base(info.treePath),
7576
Path: info.treePath,
7677
SHA: sha,
77-
LastCommitSHA: info.lastCommitSHA,
78-
LastCommitterDate: info.lastCommitterWhen,
79-
LastAuthorDate: info.lastAuthorWhen,
78+
LastCommitSHA: util.ToPointer(info.lastCommitSHA),
79+
LastCommitterDate: util.ToPointer(info.lastCommitterWhen),
80+
LastAuthorDate: util.ToPointer(info.lastAuthorWhen),
8081
Size: 16,
8182
Type: "file",
8283
Encoding: &encoding,

tests/integration/api_repo_file_update_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"code.gitea.io/gitea/modules/gitrepo"
1919
"code.gitea.io/gitea/modules/setting"
2020
api "code.gitea.io/gitea/modules/structs"
21+
"code.gitea.io/gitea/modules/util"
2122
"code.gitea.io/gitea/services/context"
2223

2324
"github.com/stretchr/testify/assert"
@@ -60,9 +61,9 @@ func getExpectedFileResponseForUpdate(info apiFileResponseInfo) *api.FileRespons
6061
Name: path.Base(info.treePath),
6162
Path: info.treePath,
6263
SHA: sha,
63-
LastCommitSHA: info.lastCommitSHA,
64-
LastCommitterDate: info.lastCommitterWhen,
65-
LastAuthorDate: info.lastAuthorWhen,
64+
LastCommitSHA: util.ToPointer(info.lastCommitSHA),
65+
LastCommitterDate: util.ToPointer(info.lastCommitterWhen),
66+
LastAuthorDate: util.ToPointer(info.lastAuthorWhen),
6667
Type: "file",
6768
Size: 20,
6869
Encoding: &encoding,

0 commit comments

Comments
 (0)