diff --git a/integrations/api_tree_test.go b/integrations/api_tree_test.go new file mode 100644 index 0000000000000..af75025414129 --- /dev/null +++ b/integrations/api_tree_test.go @@ -0,0 +1,93 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "bytes" + "fmt" + "net/http" + "testing" + + api "code.gitea.io/sdk/gitea" + + "github.com/stretchr/testify/assert" +) + +func testAPIGetTree(t *testing.T, treePath string, exists bool, entries []*api.TreeEntry) { + prepareTestEnv(t) + + session := loginUser(t, "user2") + req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/git/trees/%s", treePath) + resp := session.MakeRequest(t, req, NoExpectedStatus) + if !exists { + assert.EqualValues(t, http.StatusNotFound, resp.HeaderCode) + return + } + assert.EqualValues(t, http.StatusOK, resp.HeaderCode) + fmt.Print(bytes.NewBuffer(resp.Body)) + var trees []*api.TreeEntry + DecodeJSON(t, resp, &trees) + + if assert.EqualValues(t, len(entries), len(trees)) { + for i, tree := range trees { + assert.EqualValues(t, entries[i], tree) + } + } +} + +func TestAPIGetTree(t *testing.T) { + for _, test := range []struct { + TreePath string + Exists bool + Listing *api.RepoTreeListing + // Entries []*api.TreeEntry + }{ + {"master", true, []*api.TreeEntry{ + &api.TreeEntry{ + Name: "README.md", + ID: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", + Type: "blob", + // Size: 30, + }, + }}, + {"master/doesnotexist", false, []*api.TreeEntry{}}, + {"feature/1", true, []*api.TreeEntry{ + &api.TreeEntry{ + Name: "README.md", + ID: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", + Type: "blob", + // Size: 30, + }, + }}, + {"feature/1/doesnotexist", false, []*api.TreeEntry{}}, + } { + testAPIGetTree(t, test.TreePath, test.Exists, test.Entries) + } +} + +// func TestAPIListRepoEntries(t *testing.T) { +// prepareTestEnv(t) + +// // TODO: Make this actually work!!! +// req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/tree/thing/thing") +// resp := MakeRequest(t, req, http.StatusOK) + +// var repo api.Repository +// DecodeJSON(t, resp, &repo) +// assert.EqualValues(t, 1, repo.ID) +// assert.EqualValues(t, "repo1", repo.Name) +// } + +// func TestVersion(t *testing.T) { +// prepareTestEnv(t) + +// setting.AppVer = "test-version-1" +// req := NewRequest(t, "GET", "/api/v1/version") +// resp := MakeRequest(t, req, http.StatusOK) + +// var version gitea.ServerVersion +// DecodeJSON(t, resp, &version) +// assert.Equal(t, setting.AppVer, string(version.Version)) +// } diff --git a/models/repo_tree.go b/models/repo_tree.go new file mode 100644 index 0000000000000..8e6e690e93731 --- /dev/null +++ b/models/repo_tree.go @@ -0,0 +1,79 @@ +// TODO: +// With model objects here, we can write tests for them and for TreeListing? Or maybe +// they should go elsewhere? + +// Then we can write fuller integration tests, with model objects that we're expecting and asserting +// against +import ( + "path/filepath" + + "code.gitea.io/git" +) + +// RepoFile represents a file blob contained in the repository +type RepoFile struct { + Path string `json:"path"` + // Mode git.EntryMode `json:"mode"` // TODO: Do we include this? It'll require exporting the mode as public in the `git` module... + Type git.ObjectType `json:"type"` + // Size int64 `json:"size"` // TODO: Do we include this? It's expensive... + SHA string `json:"sha"` + URL string `json:"url"` +} + +// RepoTreeListing represents a tree (or subtree) listing in the repository +type RepoTreeListing struct { + SHA string `json:"sha"` + Path string `json:"path"` + Tree []*RepoFile `json:"tree"` +} + +// NewRepoFile creates a new RepoFile from a Git tree entry and some metadata. +func NewRepoFile(e *git.TreeEntry, parentPath string, rawLink string) *RepoFile { + var filePath string + if parentPath != "" { + filePath = filepath.Join(parentPath, e.Name()) + } else { + filePath = e.Name() + } + return &RepoFile{ + Path: filePath, + // Mode: e.mode, // TODO: Not exported by `git.TreeEntry` + Type: e.Type, + // Size: e.Size(), // TODO: Expensive! + SHA: e.ID.String(), + URL: filepath.Join(rawLink, filePath), + } +} + +// NewRepoTreeListing creates a new RepoTreeListing from a Git tree and some metadata +func NewRepoTreeListing(t *git.Tree, treePath, rawLink string, recursive bool) (*RepoTreeListing, error) { + tree, err := t.SubTree(treePath) + if err != nil { + return nil, err + } + + var entries []*RepoFile + treeEntries, err := tree.ListEntries() + if err != nil { + return nil, err + } + treeEntries.CustomSort(base.NaturalSortLess) + for i := range treeEntries { + entry := treeEntries[i] + if entry.IsDir() && recursive { + subListing, err := treeListing(t, filepath.Join(treePath, entry.Name()), rawLink, recursive) + if err != nil { + return nil, err + } + entries = append(entries, subListing.Tree...) + } else { + entries = append(entries, models.NewRepoFile(treeEntries[i], treePath, rawLink)) + } + } + + return &RepoTreeListing{ + SHA: tree.ID.String(), + Path: treePath, + Tree: entries, + }, nil +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index c5f01d91d8c50..a86cc0011888a 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -444,6 +444,9 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/:username/:reponame", func() { m.Combo("").Get(repo.Get).Delete(reqToken(), repo.Delete) + // TODO: Expand this to make the files available as an endpoint; note that + // GitHub uses `/repos/:owner/:repo/git/trees/:sha` as documented at + // https://developer.github.com/v3/git/trees/ m.Group("/hooks", func() { m.Combo("").Get(repo.ListHooks). Post(bind(api.CreateHookOption{}), repo.CreateHook) @@ -573,6 +576,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/status", repo.GetCombinedCommitStatusByRef) m.Get("/statuses", repo.GetCommitStatusesByRef) }) + m.Get("/git/trees/*", context.RepoRef(), repo.ListContentsAtSHA) }, repoAssignment()) }) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index bf6346eebdfca..ae141a5445603 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -8,9 +8,14 @@ import ( "fmt" "net/http" "strings" + "path/filepath" + + api "code.gitea.io/sdk/gitea" + "code.gitea.io/git" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -602,3 +607,51 @@ func TopicSearch(ctx *context.Context) { "topics": topics, }) } + +// List contents of one repository +func ListContentsAtSHA(ctx *context.APIContext) { + // swagger:route GET /repos/{username}/{reponame}/git/trees/{sha} repository repoListAtSHA + // + // Produces: + // - application/json + // + // Responses: + // 200: RepoTreeListing + // 403: forbidden + // 500: error + + rawLink := filepath.Join(ctx.Repo.RepoLink, "/raw/", ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) + tree, err := ctx.Repo.GitRepo.GetTree(ctx.Repo.Commit.ID.String()) + if err != nil { + if models.IsErrRepoNotExist(err) { + ctx.Status(404) + } else { + ctx.Handle(500, "GetRepo", err) + } + return + } + + listing, err := models.NewRepoTreeListing(tree, ctx.Repo.TreePath, rawLink, ctx.QueryBool("recursive")) + if err != nil { + ctx.Handle(500, "RepoTreeListing", err) + return + } + + ctx.JSON(200, listing) + // TODO: Make output match the GitHub sample output: + // { + // "sha": "9fb037999f264ba9a7fc6274d15fa3ae2ab98312", + // "url": "https://api.github.com/repos/octocat/Hello-World/trees/9fb037999f264ba9a7fc6274d15fa3ae2ab98312", + // "tree": [ + // { + // "path": "file.rb", + // "mode": "100644", + // "type": "blob", + // "size": 30, + // "sha": "44b4fc6d56897b048c772eb4087f854f46256132", + // "url": "https://api.github.com/repos/octocat/Hello-World/git/blobs/44b4fc6d56897b048c772eb4087f854f46256132" + // }, + // ], + // "truncated": false + // } +} diff --git a/routers/repo/repo_test.go b/routers/repo/repo_test.go new file mode 100644 index 0000000000000..4e1869186684d --- /dev/null +++ b/routers/repo/repo_test.go @@ -0,0 +1,10 @@ +// Copyright, whatnot +package repo + +func TestTreeListing() { + +} + +func TestRepoFile() { + +}