diff --git a/github/client_repository_file.go b/github/client_repository_file.go index b77cd055..6030b925 100644 --- a/github/client_repository_file.go +++ b/github/client_repository_file.go @@ -34,13 +34,20 @@ type FileClient struct { ref gitprovider.RepositoryRef } -// Get fetches and returns the contents of a file from a given branch and path -func (c *FileClient) Get(ctx context.Context, path, branch string) ([]*gitprovider.CommitFile, error) { +// Get fetches and returns the contents of a file or multiple files in a directory from a given branch and path with possible options of FilesGetOption +// If a file path is given, the contents of the file are returned +// If a directory path is given, the contents of the files in the path's root are returned +func (c *FileClient) Get(ctx context.Context, path, branch string, optFns ...gitprovider.FilesGetOption) ([]*gitprovider.CommitFile, error) { opts := &github.RepositoryContentGetOptions{ Ref: branch, } + fileOpts := gitprovider.FilesGetOptions{} + for _, opt := range optFns { + opt.ApplyFilesGetOptions(&fileOpts) + } + _, directoryContent, _, err := c.c.Client().Repositories.GetContents(ctx, c.ref.GetIdentity(), c.ref.GetRepository(), path, opts) if err != nil { return nil, err @@ -62,15 +69,14 @@ func (c *FileClient) Get(ctx context.Context, path, branch string) ([]*gitprovid if err != nil { return nil, err } - err = output.Close() - if err != nil { - return nil, err - } + defer output.Close() + contentStr := string(content) files = append(files, &gitprovider.CommitFile{ Path: filePath, Content: &contentStr, }) + } return files, nil diff --git a/github/client_repository_tree.go b/github/client_repository_tree.go new file mode 100644 index 00000000..65e9ac31 --- /dev/null +++ b/github/client_repository_tree.go @@ -0,0 +1,95 @@ +/* +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" + "strings" + + "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 +} + +// Get returns a single tree using the SHA1 value for that tree. +// uses https://docs.github.com/en/rest/git/trees#get-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, recursive) + if err != nil { + return nil, err + } + + treeEntries := make([]*gitprovider.TreeEntry, len(githubTree.Entries)) + for ind, treeEntry := range githubTree.Entries { + size := 0 + if *treeEntry.Type != "tree" { + size = *treeEntry.Size + } + treeEntries[ind] = &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 givent the tree sha (path is not used with Github Tree client) +func (c *TreeClient) List(ctx context.Context, sha string, path 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" { + if path == "" || (path != "" && strings.HasPrefix(treeEntry.Path, path)) { + 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 +} diff --git a/github/integration_test.go b/github/integration_test.go index 05d8c599..d817161b 100644 --- a/github/integration_test.go +++ b/github/integration_test.go @@ -562,6 +562,97 @@ 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 : LICENSE, 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 with no path provided + treeEntries, err := userRepo.Trees().List(ctx, commitSha, "", true) + Expect(err).ToNot(HaveOccurred()) + + // Tree Entries should have length 5 for : LICENSE, 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)) + } + + //List tree items with path provided to filter on + treeEntries, err = userRepo.Trees().List(ctx, commitSha, "clustersDir/", true) + Expect(err).ToNot(HaveOccurred()) + + // Tree Entries should have length 3 for :3 blob (files) + Expect(treeEntries).To(HaveLen(3)) + for ind, treeEntry := range treeEntries { + Expect(treeEntry.Path).To(Equal(*files[ind].Path)) + } + + }) + AfterSuite(func() { if os.Getenv("SKIP_CLEANUP") == "1" { return diff --git a/github/resource_repository.go b/github/resource_repository.go index 13fda824..abfcf886 100644 --- a/github/resource_repository.go +++ b/github/resource_repository.go @@ -78,6 +78,10 @@ func newUserRepository(ctx *clientContext, apiObj *github.Repository, ref gitpro clientContext: ctx, ref: ref, }, + trees: &TreeClient{ + clientContext: ctx, + ref: ref, + }, } } @@ -95,6 +99,7 @@ type userRepository struct { branches *BranchClient pullRequests *PullRequestClient files *FileClient + trees *TreeClient } func (r *userRepository) Get() gitprovider.RepositoryInfo { @@ -137,6 +142,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_file.go b/gitlab/client_repository_file.go index d93043b4..cfcb2ef2 100644 --- a/gitlab/client_repository_file.go +++ b/gitlab/client_repository_file.go @@ -35,12 +35,21 @@ type FileClient struct { ref gitprovider.RepositoryRef } -// Get fetches and returns the contents of a file from a given branch and path -func (c *FileClient) Get(_ context.Context, path, branch string) ([]*gitprovider.CommitFile, error) { +// Get fetches and returns the contents of a file or multiple files in a directory from a given branch and path with possible options of FilesGetOption +// If a file path is given, the contents of the file are returned +// If a directory path is given, the contents of the files in the path's root are returned +func (c *FileClient) Get(ctx context.Context, path, branch string, optFns ...gitprovider.FilesGetOption) ([]*gitprovider.CommitFile, error) { + + filesGetOpts := gitprovider.FilesGetOptions{} + + for _, opt := range optFns { + opt.ApplyFilesGetOptions(&filesGetOpts) + } opts := &gitlab.ListTreeOptions{ - Path: &path, - Ref: &branch, + Path: &path, + Ref: &branch, + Recursive: &filesGetOpts.Recursive, } listFiles, _, err := c.c.Client().Repositories.ListTree(getRepoPath(c.ref), opts) @@ -54,6 +63,9 @@ func (c *FileClient) Get(_ context.Context, path, branch string) ([]*gitprovider files := make([]*gitprovider.CommitFile, 0) for _, file := range listFiles { + if file.Type == "tree" { + continue + } fileDownloaded, _, err := c.c.Client().RepositoryFiles.GetFile(getRepoPath(c.ref), file.Path, fileOpts) if err != nil { return nil, err diff --git a/gitlab/client_repository_tree.go b/gitlab/client_repository_tree.go new file mode 100644 index 00000000..86e0ea4f --- /dev/null +++ b/gitlab/client_repository_tree.go @@ -0,0 +1,70 @@ +/* +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" + "github.com/xanzy/go-gitlab" +) + +// 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 +} + +// 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, sha is represented by the branch name +func (c *TreeClient) List(ctx context.Context, sha string, path string, recursive bool) ([]*gitprovider.TreeEntry, error) { + opts := &gitlab.ListTreeOptions{ + Path: &path, + Ref: &sha, + Recursive: &recursive, + } + + treeFiles, _, err := c.c.Client().Repositories.ListTree(getRepoPath(c.ref), opts) + if err != nil { + return nil, err + } + + treeEntries := make([]*gitprovider.TreeEntry, 0) + for _, treeEntry := range treeFiles { + if treeEntry.Type == "blob" { + size := 0 + treeEntries = append(treeEntries, &gitprovider.TreeEntry{ + Path: treeEntry.Path, + Mode: treeEntry.Mode, + Type: treeEntry.Type, + Size: size, + ID: treeEntry.ID, + }) + } + } + + return treeEntries, nil +} diff --git a/gitlab/integration_test.go b/gitlab/integration_test.go index 96f3693d..dc9e4294 100644 --- a/gitlab/integration_test.go +++ b/gitlab/integration_test.go @@ -886,6 +886,100 @@ var _ = Describe("GitLab Provider", func() { }) + It("should be possible to download files from path and branch specified with nested directory", func() { + + userRepoRef := newUserRepoRef(testUserName, testRepoName) + + userRepo, err := c.UserRepositories().Get(ctx, userRepoRef) + Expect(err).ToNot(HaveOccurred()) + + defaultBranch := userRepo.Get().DefaultBranch + + path0 := "clustersDir/cluster/machine.yaml" + content0 := "machine0 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, + }) + } + + _, err = userRepo.Commits().Create(ctx, *defaultBranch, "added files", commitFiles) + Expect(err).ToNot(HaveOccurred()) + + options := gitprovider.FilesGetOptions{Recursive: *gitprovider.BoolVar(true)} + downloadedFiles, err := userRepo.Files().Get(ctx, "clustersDir", *defaultBranch, &options) + Expect(err).ToNot(HaveOccurred()) + for ind, downloadedFile := range downloadedFiles { + Expect(*downloadedFile).To(Equal(files[ind])) + } + + }) + It("should be possible list repo tree files", func() { + userRepoRef := newUserRepoRef(testUserName, testRepoName) + + userRepo, err := c.UserRepositories().Get(ctx, userRepoRef) + Expect(err).ToNot(HaveOccurred()) + + defaultBranch := userRepo.Get().DefaultBranch + + path0 := "clustersDir/cluster/machine.yaml" + content0 := "machine0 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, + }, + } + + // List tree items + treeEntries, err := userRepo.Trees().List(ctx, *defaultBranch, "clustersDir/", true) + Expect(err).ToNot(HaveOccurred()) + + // Tree Entries should have length 3 for : 3 blob (files) + Expect(treeEntries).To(HaveLen(3)) + for ind, treeEntry := range treeEntries { + Expect(treeEntry.Path).To(Equal(*files[ind].Path)) + } + + }) + AfterSuite(func() { if os.Getenv("SKIP_CLEANUP") == "1" { return 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 b95720d7..63bd0c13 100644 --- a/gitprovider/client.go +++ b/gitprovider/client.go @@ -241,5 +241,14 @@ type PullRequestClient interface { // This client can be accessed through Repository.Branches(). type FileClient interface { // GetFiles fetch files content from specific path and branch - Get(ctx context.Context, path, branch string) ([]*CommitFile, error) + Get(ctx context.Context, path, branch string, optFns ...FilesGetOption) ([]*CommitFile, error) +} + +// TreeClient operates on the trees for a Git repository which describe the hierarchy between files in the repository +// This client can be accessed through Repository.Trees() +type TreeClient interface { + // Get retrieves tree information and items + Get(ctx context.Context, sha string, recursive bool) (*TreeInfo, error) + // List retrieves list of tree files (files/blob) from given tree sha/id or path+branch + List(ctx context.Context, sha string, path string, recursive bool) ([]*TreeEntry, error) } diff --git a/gitprovider/options.go b/gitprovider/options.go index 8109e334..a3c30ba9 100644 --- a/gitprovider/options.go +++ b/gitprovider/options.go @@ -77,3 +77,20 @@ func (opts *RepositoryCreateOptions) ValidateOptions() error { } return errs.Error() } + +// FilesGetOptions specifies optional options when fetcing files. +type FilesGetOptions struct { + Recursive bool +} + +// FilesGetOption is an interface for applying options when fetching/getting files +type FilesGetOption interface { + ApplyFilesGetOptions(target *FilesGetOptions) +} + +// ApplyFilesGetOptions applies target options onto the invoked opts +func (opts *FilesGetOptions) ApplyFilesGetOptions(target *FilesGetOptions) { + // Go through each field in opts, and apply it to target if set + target.Recursive = opts.Recursive + +} diff --git a/gitprovider/resources.go b/gitprovider/resources.go index 257e109b..a8586745 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,15 @@ type PullRequest interface { // Get returns high-level information about this pull request. Get() PullRequestInfo } + +// Tree represents a git tree which is the hierarchical structure of your git data. +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. + Get() TreeInfo + // List files (blob) in a tree + List() TreeEntry +} diff --git a/gitprovider/types_repository.go b/gitprovider/types_repository.go index 7c815c44..24cc6ce6 100644 --- a/gitprovider/types_repository.go +++ b/gitprovider/types_repository.go @@ -225,3 +225,35 @@ type PullRequestInfo struct { // +required WebURL string `json:"web_url"` } + +// TreeEntry contains info about each tree object's structure in TreeInfo whether it is a file or tree +type TreeEntry struct { + // Path is the path of the file/blob or sub tree in a tree + Path string `json:"path"` + // Mode of the file/tree. + // (100644:file (blob), 100755:executable (blob), 040000:subdirectory(tree),160000:submodule(commit),120000:blob that specifies the path of a symlink) + Mode string `json:"mode"` + // Type is the item type, It is either blob, tree, or commit. + Type string `json:"type"` + // Size is the size of the file/blob if the type is a blob, it is not populated if the type is a tree + Size int `json:"size"` + // SHA is the SHA1 checksum ID of the object in the tree + SHA string `json:"sha"` + // Content is the content of a blob file, either content aor sha are set. If both are using Github will return an error + Content string `json:"content"` + // URL is the url that can be used to retrieve the details of the blob, tree of commit + URL string `json:"url"` + // Id is the id of the tree entry retrieved from Gitlab (Optional) + ID string `json:"id"` +} + +// TreeInfo contains high-level information about a git Tree representing the hierarchy between files in a Git repository +type TreeInfo struct { + // SHA is the SHA1 checksum ID of the tree, or the branch name + SHA string `json:"sha"` + // Tree is the list of TreeEntry objects describing the structure of the tree + Tree []*TreeEntry `json:"tree"` + // Truncated represents whether a tree is truncated when fetching a tree + // If truncated is true in the response when fetching a tree, then the number of items in the tree array exceeded the maximum limit + Truncated bool `json:"truncated"` +} diff --git a/stash/client_repository_file.go b/stash/client_repository_file.go index 72405a48..66572d64 100644 --- a/stash/client_repository_file.go +++ b/stash/client_repository_file.go @@ -33,6 +33,6 @@ type FileClient struct { } // Get fetches and returns the contents of a file from a given branch and path -func (c *FileClient) Get(_ context.Context, path, branch string) ([]*gitprovider.CommitFile, error) { +func (c *FileClient) Get(_ context.Context, path, branch string, optFns ...gitprovider.FilesGetOption) ([]*gitprovider.CommitFile, error) { return nil, fmt.Errorf("error getting file %s@%s. not implemented in stash yet", path, branch) } diff --git a/stash/client_repository_tree.go b/stash/client_repository_tree.go new file mode 100644 index 00000000..1fec807a --- /dev/null +++ b/stash/client_repository_tree.go @@ -0,0 +1,44 @@ +/* +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 +} + +// 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, path 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) }