diff --git a/github/client_repository_tree.go b/github/client_repository_tree.go new file mode 100644 index 00000000..0c206c1d --- /dev/null +++ b/github/client_repository_tree.go @@ -0,0 +1,150 @@ +/* +Copyright 2020 The Flux CD contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package github + +import ( + "context" + + "github.com/fluxcd/go-git-providers/gitprovider" + "github.com/google/go-github/v42/github" +) + +// TreeClient implements the gitprovider.TreeClient interface. +var _ gitprovider.TreeClient = &TreeClient{} + +// TreeClient operates on the trees in a specific repository. +type TreeClient struct { + *clientContext + ref gitprovider.RepositoryRef +} + +// Create creates,updates,deletes a tree +func (c *TreeClient) Create(ctx context.Context, tree *gitprovider.TreeInfo) (*gitprovider.TreeInfo, error) { + repoName := c.ref.GetRepository() + repoOwner := c.ref.GetIdentity() + + treeEntries := make([]*github.TreeEntry, 0) + for _, treeEntry := range tree.Tree { + treeEntries = append(treeEntries, &github.TreeEntry{ + Path: &treeEntry.Path, + Mode: &treeEntry.Mode, + Type: &treeEntry.Type, + Size: &treeEntry.Size, + SHA: &treeEntry.SHA, + URL: &treeEntry.URL, + }) + } + githubTree, _, err := c.c.Client().Git.CreateTree(ctx, repoOwner, repoName, tree.SHA, treeEntries) + if err != nil { + return nil, err + } + + responseTreeEntries := make([]*gitprovider.TreeEntry, 0) + for _, responseTreeEntry := range githubTree.Entries { + size := 0 + if *responseTreeEntry.Type != "tree" { + size = *responseTreeEntry.Size + } + responseTreeEntries = append(responseTreeEntries, &gitprovider.TreeEntry{ + Path: *responseTreeEntry.Path, + Mode: *responseTreeEntry.Mode, + Type: *responseTreeEntry.Type, + Size: size, + SHA: *responseTreeEntry.SHA, + URL: *responseTreeEntry.URL, + }) + } + + responseTreeInfo := gitprovider.TreeInfo{ + SHA: *githubTree.SHA, + Tree: responseTreeEntries, + Truncated: *githubTree.Truncated, + } + + return &responseTreeInfo, nil +} + +// Get returns a tree +func (c *TreeClient) Get(ctx context.Context, sha string, recursive bool) (*gitprovider.TreeInfo, error) { + // GET /repos/{owner}/{repo}/git/trees + repoName := c.ref.GetRepository() + repoOwner := c.ref.GetIdentity() + githubTree, _, err := c.c.Client().Git.GetTree(ctx, repoOwner, repoName, sha, true) + if err != nil { + return nil, err + } + + treeEntries := make([]*gitprovider.TreeEntry, 0) + for _, treeEntry := range githubTree.Entries { + size := 0 + if *treeEntry.Type != "tree" { + size = *treeEntry.Size + } + treeEntries = append(treeEntries, &gitprovider.TreeEntry{ + Path: *treeEntry.Path, + Mode: *treeEntry.Mode, + Type: *treeEntry.Type, + Size: size, + SHA: *treeEntry.SHA, + URL: *treeEntry.URL, + }) + } + + treeInfo := gitprovider.TreeInfo{ + SHA: *githubTree.SHA, + Tree: treeEntries, + Truncated: *githubTree.Truncated, + } + + return &treeInfo, nil + +} + +// List files (blob) in a tree +func (c *TreeClient) List(ctx context.Context, sha string, recursive bool) ([]*gitprovider.TreeEntry, error) { + treeInfo, err := c.Get(ctx, sha, recursive) + if err != nil { + return nil, err + } + treeEntries := make([]*gitprovider.TreeEntry, 0) + for _, treeEntry := range treeInfo.Tree { + if treeEntry.Type == "blob" { + treeEntries = append(treeEntries, &gitprovider.TreeEntry{ + Path: treeEntry.Path, + Mode: treeEntry.Mode, + Type: treeEntry.Type, + Size: treeEntry.Size, + SHA: treeEntry.SHA, + URL: treeEntry.URL, + }) + } + } + + return treeEntries, nil +} + +func createTreeEntry(githubTreeEntry github.TreeEntry) *gitprovider.TreeEntry { + newTreeEntry := gitprovider.TreeEntry{ + Path: *githubTreeEntry.Path, + Mode: *githubTreeEntry.Mode, + Type: *githubTreeEntry.Type, + Size: *githubTreeEntry.Size, + SHA: *githubTreeEntry.SHA, + URL: *githubTreeEntry.URL, + } + return &newTreeEntry +} diff --git a/github/integration_test.go b/github/integration_test.go index 192cfe45..81b7e0d5 100644 --- a/github/integration_test.go +++ b/github/integration_test.go @@ -562,6 +562,87 @@ var _ = Describe("GitHub Provider", func() { }) + It("should be possible to get and list repo tree", func() { + + userRepoRef := newUserRepoRef(testUser, testUserRepoName) + + userRepo, err := c.UserRepositories().Get(ctx, userRepoRef) + Expect(err).ToNot(HaveOccurred()) + + defaultBranch := userRepo.Get().DefaultBranch + + path0 := "clustersDir/cluster/machine.yaml" + content0 := "machine yaml content" + path1 := "clustersDir/cluster/machine1.yaml" + content1 := "machine1 yaml content" + path2 := "clustersDir/cluster2/clusterSubDir/machine2.yaml" + content2 := "machine2 yaml content" + + files := []gitprovider.CommitFile{ + { + Path: &path0, + Content: &content0, + }, + { + Path: &path1, + Content: &content1, + }, + { + Path: &path2, + Content: &content2, + }, + } + + commitFiles := make([]gitprovider.CommitFile, 0) + for _, file := range files { + path := file.Path + content := file.Content + commitFiles = append(commitFiles, gitprovider.CommitFile{ + Path: path, + Content: content, + }) + } + + commit, err := userRepo.Commits().Create(ctx, *defaultBranch, "added files", commitFiles) + Expect(err).ToNot(HaveOccurred()) + commitSha := commit.Get().Sha + + // get tree + tree, err := userRepo.Trees().Get(ctx, commitSha, true) + Expect(err).ToNot(HaveOccurred()) + + // Tree should have length 9 for : LISENCE, README.md, 3 blob (files), 4 tree (directories) + Expect(tree.Tree).To(HaveLen(9)) + + // itemsToBeIgnored initially with 2 for LICENSE and README.md, and will also include tree types + itemsToBeIgnored := 2 + for ind, treeEntry := range tree.Tree { + if treeEntry.Type == "blob" { + if treeEntry.Path == "LICENSE" || treeEntry.Path == "README.md" { + continue + } + Expect(*&treeEntry.Path).To(Equal(*files[ind-itemsToBeIgnored].Path)) + continue + + } + itemsToBeIgnored += 1 + } + + // List tree items + treeEntries, err := userRepo.Trees().List(ctx, commitSha, true) + Expect(err).ToNot(HaveOccurred()) + + // Tree Entries should have length 5 for : LISENCE, README.md, 3 blob (files) + Expect(treeEntries).To(HaveLen(5)) + for ind, treeEntry := range treeEntries { + if treeEntry.Path == "LICENSE" || treeEntry.Path == "README.md" { + continue + } + Expect(*&treeEntry.Path).To(Equal(*files[ind-2].Path)) + } + + }) + AfterSuite(func() { if os.Getenv("SKIP_CLEANUP") == "1" { return diff --git a/github/resource_repository.go b/github/resource_repository.go index e86b4bad..a7fa54e0 100644 --- a/github/resource_repository.go +++ b/github/resource_repository.go @@ -52,6 +52,10 @@ func newUserRepository(ctx *clientContext, apiObj *github.Repository, ref gitpro clientContext: ctx, ref: ref, }, + trees: &TreeClient{ + clientContext: ctx, + ref: ref, + }, } } @@ -68,6 +72,7 @@ type userRepository struct { branches *BranchClient pullRequests *PullRequestClient files *FileClient + trees *TreeClient } func (r *userRepository) Get() gitprovider.RepositoryInfo { @@ -110,6 +115,10 @@ func (r *userRepository) Files() gitprovider.FileClient { return r.files } +func (r *userRepository) Trees() gitprovider.TreeClient { + return r.trees +} + // Update will apply the desired state in this object to the server. // Only set fields will be respected (i.e. PATCH behaviour). // In order to apply changes to this object, use the .Set({Resource}Info) error diff --git a/gitlab/client_repository_tree.go b/gitlab/client_repository_tree.go new file mode 100644 index 00000000..7dbfd391 --- /dev/null +++ b/gitlab/client_repository_tree.go @@ -0,0 +1,50 @@ +/* +Copyright 2020 The Flux CD contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gitlab + +import ( + "context" + "fmt" + + "github.com/fluxcd/go-git-providers/gitprovider" +) + +// TreeClient implements the gitprovider.TreeClient interface. +var _ gitprovider.TreeClient = &TreeClient{} + +// TreeClient operates on the trees in a specific repository. +type TreeClient struct { + *clientContext + ref gitprovider.RepositoryRef +} + +// Create creates,updates,deletes a tree +func (c *TreeClient) Create(ctx context.Context, tree *gitprovider.TreeInfo) (*gitprovider.TreeInfo, error) { + return nil, fmt.Errorf("error creaing tree %s. not implemented in gitlab yet", tree.SHA) + +} + +// Get returns a tree +func (c *TreeClient) Get(ctx context.Context, sha string, recursive bool) (*gitprovider.TreeInfo, error) { + return nil, fmt.Errorf("error getting tree %s. not implemented in gitlab yet", sha) + +} + +// List files (blob) in a tree +func (c *TreeClient) List(ctx context.Context, sha string, recursive bool) ([]*gitprovider.TreeEntry, error) { + return nil, fmt.Errorf("error listing tree items %s. not implemented in gitlab yet", sha) +} diff --git a/gitlab/resource_repository.go b/gitlab/resource_repository.go index eae17395..36468f6a 100644 --- a/gitlab/resource_repository.go +++ b/gitlab/resource_repository.go @@ -51,6 +51,10 @@ func newUserProject(ctx *clientContext, apiObj *gogitlab.Project, ref gitprovide clientContext: ctx, ref: ref, }, + trees: &TreeClient{ + clientContext: ctx, + ref: ref, + }, } } @@ -67,6 +71,7 @@ type userProject struct { branches *BranchClient pullRequests *PullRequestClient files *FileClient + trees *TreeClient } func (p *userProject) Get() gitprovider.RepositoryInfo { @@ -109,6 +114,10 @@ func (p *userProject) Files() gitprovider.FileClient { return p.files } +func (p *userProject) Trees() gitprovider.TreeClient { + return p.trees +} + // The internal API object will be overridden with the received server data. func (p *userProject) Update(ctx context.Context) error { // PATCH /repos/{owner}/{repo} diff --git a/gitprovider/client.go b/gitprovider/client.go index 55726b8e..3be0c428 100644 --- a/gitprovider/client.go +++ b/gitprovider/client.go @@ -243,3 +243,12 @@ type FileClient interface { // GetFiles fetch files content from specific path and branch Get(ctx context.Context, path, branch string, optFns ...FilesGetOption) ([]*CommitFile, error) } + +type TreeClient interface { + // Create allows for creating or editing tree + Create(ctx context.Context, tree *TreeInfo) (*TreeInfo, error) + // Get retrieves tree information and items + Get(ctx context.Context, sha string, recursive bool) (*TreeInfo, error) + // List retrieves list of tree files (files/blob) + List(ctx context.Context, sha string, recursive bool) ([]*TreeEntry, error) +} diff --git a/gitprovider/resources.go b/gitprovider/resources.go index 257e109b..65f61020 100644 --- a/gitprovider/resources.go +++ b/gitprovider/resources.go @@ -77,8 +77,11 @@ type UserRepository interface { // PullRequests gives access to this specific repository pull requests PullRequests() PullRequestClient - // Files gives access to this specific repository pull requests + // Files gives access to this specific repository files Files() FileClient + + // Trees gives access to this specific repository trees + Trees() TreeClient } // OrgRepository describes a repository owned by an organization. @@ -156,3 +159,14 @@ type PullRequest interface { // Get returns high-level information about this pull request. Get() PullRequestInfo } + +type Tree interface { + // Object implements the Object interface, + // allowing access to the underlying object returned from the API. + Object + + // Get returns high-level information about this tree. + Create() TreeInfo + Get() TreeInfo + List() TreeEntry +} diff --git a/gitprovider/types_repository.go b/gitprovider/types_repository.go index 7c815c44..f8af74f2 100644 --- a/gitprovider/types_repository.go +++ b/gitprovider/types_repository.go @@ -225,3 +225,17 @@ type PullRequestInfo struct { // +required WebURL string `json:"web_url"` } + +type TreeEntry struct { + Path string `json:"path"` + Mode string `json:"mode"` + Type string `json:"type"` + Size int `json:"size"` + SHA string `json:"sha"` + URL string `json:"url"` +} +type TreeInfo struct { + SHA string `json:"sha"` + Tree []*TreeEntry `json:"tree"` + Truncated bool `json:"truncated"` +} diff --git a/stash/client_repository_tree.go b/stash/client_repository_tree.go new file mode 100644 index 00000000..2a8be8f6 --- /dev/null +++ b/stash/client_repository_tree.go @@ -0,0 +1,50 @@ +/* +Copyright 2020 The Flux CD contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stash + +import ( + "context" + "fmt" + + "github.com/fluxcd/go-git-providers/gitprovider" +) + +// TreeClient implements the gitprovider.TreeClient interface. +var _ gitprovider.TreeClient = &TreeClient{} + +// TreeClient operates on the trees in a specific repository. +type TreeClient struct { + *clientContext + ref gitprovider.RepositoryRef +} + +// Create creates,updates,deletes a tree +func (c *TreeClient) Create(ctx context.Context, tree *gitprovider.TreeInfo) (*gitprovider.TreeInfo, error) { + return nil, fmt.Errorf("error creaing tree %s. not implemented in stash yet", tree.SHA) + +} + +// Get returns a tree +func (c *TreeClient) Get(ctx context.Context, sha string, recursive bool) (*gitprovider.TreeInfo, error) { + return nil, fmt.Errorf("error getting tree %s. not implemented in stash yet", sha) + +} + +// List files (blob) in a tree +func (c *TreeClient) List(ctx context.Context, sha string, recursive bool) ([]*gitprovider.TreeEntry, error) { + return nil, fmt.Errorf("error listing tree items %s. not implemented in stash yet", sha) +} diff --git a/stash/resource_repository.go b/stash/resource_repository.go index 7dda26ce..66afd0ac 100644 --- a/stash/resource_repository.go +++ b/stash/resource_repository.go @@ -52,6 +52,10 @@ func newUserRepository(ctx *clientContext, apiObj *Repository, ref gitprovider.R clientContext: ctx, ref: ref, }, + trees: &TreeClient{ + clientContext: ctx, + ref: ref, + }, } } @@ -66,6 +70,7 @@ type userRepository struct { pullRequests *PullRequestClient commits *CommitClient files *FileClient + trees *TreeClient } func (r *userRepository) Branches() gitprovider.BranchClient { @@ -84,6 +89,10 @@ func (r *userRepository) Files() gitprovider.FileClient { return r.files } +func (r *userRepository) Trees() gitprovider.TreeClient { + return r.trees +} + func (r *userRepository) Get() gitprovider.RepositoryInfo { return repositoryFromAPI(&r.repository) }