From a7f11126facabb6fda86d85a83f930443071dd25 Mon Sep 17 00:00:00 2001 From: Marko Gacesa Date: Mon, 11 Dec 2023 10:15:15 +0000 Subject: [PATCH] remove gogit (#887) --- app/api/controller/pullreq/codeowner.go | 2 +- app/api/controller/repo/content_get.go | 14 ++ app/api/controller/repo/raw.go | 2 +- app/api/handler/repo/raw.go | 8 + app/pipeline/file/gitness.go | 8 + app/services/codeowners/service.go | 6 + cmd/gitness/wire_gen.go | 3 +- git/adapter/adapter.go | 3 - git/adapter/blob.go | 65 +++---- git/adapter/gogit.go | 132 -------------- git/adapter/mapping.go | 18 -- git/adapter/match_files.go | 87 ++++----- git/adapter/setup_test.go | 2 - git/adapter/tree.go | 233 +++++++++++++++--------- git/adapter/wire.go | 6 - git/blob.go | 10 +- git/wire.go | 3 +- 17 files changed, 265 insertions(+), 337 deletions(-) delete mode 100644 git/adapter/gogit.go diff --git a/app/api/controller/pullreq/codeowner.go b/app/api/controller/pullreq/codeowner.go index 0cdbd0044..9f19a1e50 100644 --- a/app/api/controller/pullreq/codeowner.go +++ b/app/api/controller/pullreq/codeowner.go @@ -47,7 +47,7 @@ func (c *Controller) CodeOwners( } ownerEvaluation, err := c.codeOwners.Evaluate(ctx, repo, pr, reviewers) - if errors.Is(codeowners.ErrNotFound, err) { + if errors.Is(err, codeowners.ErrNotFound) { return types.CodeOwnerEvaluation{}, usererror.ErrNotFound } if codeowners.IsTooLargeError(err) { diff --git a/app/api/controller/repo/content_get.go b/app/api/controller/repo/content_get.go index f09b781b6..cb3f4005b 100644 --- a/app/api/controller/repo/content_get.go +++ b/app/api/controller/repo/content_get.go @@ -26,6 +26,8 @@ import ( "github.com/harness/gitness/git" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" + + "github.com/rs/zerolog/log" ) const ( @@ -186,6 +188,12 @@ func (c *Controller) getFileContent(ctx context.Context, return nil, fmt.Errorf("failed to get file content: %w", err) } + defer func() { + if err := output.Content.Close(); err != nil { + log.Ctx(ctx).Warn().Err(err).Msgf("failed to close blob content reader.") + } + }() + content, err := io.ReadAll(output.Content) if err != nil { return nil, fmt.Errorf("failed to read blob content: %w", err) @@ -213,6 +221,12 @@ func (c *Controller) getSymlinkContent(ctx context.Context, return nil, fmt.Errorf("failed to get symlink: %w", err) } + defer func() { + if err := output.Content.Close(); err != nil { + log.Ctx(ctx).Warn().Err(err).Msgf("failed to close blob content reader.") + } + }() + content, err := io.ReadAll(output.Content) if err != nil { return nil, fmt.Errorf("failed to read blob content: %w", err) diff --git a/app/api/controller/repo/raw.go b/app/api/controller/repo/raw.go index c7e3adf10..51f76f7a0 100644 --- a/app/api/controller/repo/raw.go +++ b/app/api/controller/repo/raw.go @@ -32,7 +32,7 @@ func (c *Controller) Raw(ctx context.Context, repoRef string, gitRef string, repoPath string, -) (io.Reader, int64, error) { +) (io.ReadCloser, int64, error) { repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView, true) if err != nil { return nil, 0, err diff --git a/app/api/handler/repo/raw.go b/app/api/handler/repo/raw.go index 20937bf39..663a23ba5 100644 --- a/app/api/handler/repo/raw.go +++ b/app/api/handler/repo/raw.go @@ -21,6 +21,8 @@ import ( "github.com/harness/gitness/app/api/controller/repo" "github.com/harness/gitness/app/api/render" "github.com/harness/gitness/app/api/request" + + "github.com/rs/zerolog/log" ) // HandleRaw returns the raw content of a file. @@ -45,6 +47,12 @@ func HandleRaw(repoCtrl *repo.Controller) http.HandlerFunc { return } + defer func() { + if err := dataReader.Close(); err != nil { + log.Ctx(ctx).Warn().Err(err).Msgf("failed to close blob content reader.") + } + }() + w.Header().Add("Content-Length", fmt.Sprint(dataLength)) render.Reader(ctx, w, http.StatusOK, dataReader) diff --git a/app/pipeline/file/gitness.go b/app/pipeline/file/gitness.go index 4abffae2f..8fa90b4c2 100644 --- a/app/pipeline/file/gitness.go +++ b/app/pipeline/file/gitness.go @@ -21,6 +21,8 @@ import ( "github.com/harness/gitness/git" "github.com/harness/gitness/types" + + "github.com/rs/zerolog/log" ) type service struct { @@ -63,6 +65,12 @@ func (f *service) Get( return nil, fmt.Errorf("failed to read blob: %w", err) } + defer func() { + if err := blobReader.Content.Close(); err != nil { + log.Ctx(ctx).Warn().Err(err).Msgf("failed to close blob content reader.") + } + }() + buf, err := io.ReadAll(blobReader.Content) if err != nil { return nil, fmt.Errorf("could not read blob content from file: %w", err) diff --git a/app/services/codeowners/service.go b/app/services/codeowners/service.go index 8ca5078c1..dfb791e4d 100644 --- a/app/services/codeowners/service.go +++ b/app/services/codeowners/service.go @@ -223,6 +223,12 @@ func (s *Service) getCodeOwnerFile( return nil, fmt.Errorf("failed to get file content: %w", err) } + defer func() { + if err := output.Content.Close(); err != nil { + log.Ctx(ctx).Warn().Err(err).Msgf("failed to close blob content reader.") + } + }() + content, err := io.ReadAll(output.Content) if err != nil { return nil, fmt.Errorf("failed to read blob content: %w", err) diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 9fb25e3f1..d5d032639 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -127,7 +127,6 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro return nil, err } typesConfig := server.ProvideGitConfig(config) - goGitRepoProvider := adapter.ProvideGoGitRepoProvider() universalClient, err := server.ProvideRedis(config) if err != nil { return nil, err @@ -136,7 +135,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro if err != nil { return nil, err } - gitAdapter, err := git.ProvideGITAdapter(typesConfig, goGitRepoProvider, cacheCache) + gitAdapter, err := git.ProvideGITAdapter(typesConfig, cacheCache) if err != nil { return nil, err } diff --git a/git/adapter/adapter.go b/git/adapter/adapter.go index 25e6e2363..c11d500e1 100644 --- a/git/adapter/adapter.go +++ b/git/adapter/adapter.go @@ -26,13 +26,11 @@ import ( type Adapter struct { traceGit bool - repoProvider *GoGitRepoProvider lastCommitCache cache.Cache[CommitEntryKey, *types.Commit] } func New( config types.Config, - repoProvider *GoGitRepoProvider, lastCommitCache cache.Cache[CommitEntryKey, *types.Commit], ) (Adapter, error) { // TODO: should be subdir of gitRoot? What is it being used for? @@ -45,7 +43,6 @@ func New( return Adapter{ traceGit: config.Trace, - repoProvider: repoProvider, lastCommitCache: lastCommitCache, }, nil } diff --git a/git/adapter/blob.go b/git/adapter/blob.go index 78773fb47..49190eb34 100644 --- a/git/adapter/blob.go +++ b/git/adapter/blob.go @@ -16,12 +16,13 @@ package adapter import ( "context" + "fmt" "io" "github.com/harness/gitness/errors" "github.com/harness/gitness/git/types" - gogitplumbing "github.com/go-git/go-git/v5/plumbing" + "code.gitea.io/gitea/modules/git" ) // GetBlob returns the blob for the given object sha. @@ -31,58 +32,60 @@ func (a Adapter) GetBlob( sha string, sizeLimit int64, ) (*types.BlobReader, error) { - if repoPath == "" { - return nil, ErrRepositoryPathEmpty - } - repo, err := a.repoProvider.Get(ctx, repoPath) + stdIn, stdOut, cancel := git.CatFileBatch(ctx, repoPath) + + _, err := stdIn.Write([]byte(sha + "\n")) if err != nil { - return nil, errors.Internal("failed to open repository", err) + cancel() + return nil, fmt.Errorf("failed to write blob sha to git stdin: %w", err) } - blob, err := repo.BlobObject(gogitplumbing.NewHash(sha)) + objectSHA, objectType, objectSize, err := git.ReadBatchLine(stdOut) if err != nil { - if errors.Is(err, gogitplumbing.ErrObjectNotFound) { - return nil, errors.NotFound("blob sha %s not found", sha) - } - return nil, errors.Internal("failed to get blob object for sha '%s'", sha, err) + cancel() + return nil, processGiteaErrorf(err, "failed to read cat-file batch line") + } + + if string(objectSHA) != sha { + cancel() + return nil, fmt.Errorf("cat-file returned object sha '%s' but expected '%s'", objectSHA, sha) + } + if objectType != string(git.ObjectBlob) { + cancel() + return nil, errors.InvalidArgument( + "cat-file returned object type '%s' but expected '%s'", objectType, git.ObjectBlob) } - objectSize := blob.Size contentSize := objectSize - if sizeLimit > 0 && contentSize > sizeLimit { + if sizeLimit > 0 && sizeLimit < contentSize { contentSize = sizeLimit } - reader, err := blob.Reader() - if err != nil { - return nil, errors.Internal("failed to open blob object for sha '%s'", sha, err) - } - return &types.BlobReader{ SHA: sha, Size: objectSize, ContentSize: contentSize, - Content: LimitReadCloser(reader, contentSize), + Content: newLimitReaderCloser(stdOut, contentSize, cancel), }, nil } -func LimitReadCloser(r io.ReadCloser, n int64) io.ReadCloser { - return limitReadCloser{ - r: io.LimitReader(r, n), - c: r, +func newLimitReaderCloser(reader io.Reader, limit int64, stop func()) limitReaderCloser { + return limitReaderCloser{ + reader: io.LimitReader(reader, limit), + stop: stop, } } -// limitReadCloser implements io.ReadCloser interface. -type limitReadCloser struct { - r io.Reader - c io.Closer +type limitReaderCloser struct { + reader io.Reader + stop func() } -func (l limitReadCloser) Read(p []byte) (n int, err error) { - return l.r.Read(p) +func (l limitReaderCloser) Read(p []byte) (n int, err error) { + return l.reader.Read(p) } -func (l limitReadCloser) Close() error { - return l.c.Close() +func (l limitReaderCloser) Close() error { + l.stop() + return nil } diff --git a/git/adapter/gogit.go b/git/adapter/gogit.go deleted file mode 100644 index 15d23c9fe..000000000 --- a/git/adapter/gogit.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2023 Harness, Inc. -// -// 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 adapter - -import ( - "context" - "fmt" - "os" - "time" - - "github.com/harness/gitness/cache" - "github.com/harness/gitness/errors" - "github.com/harness/gitness/git/types" - - gogitosfs "github.com/go-git/go-billy/v5/osfs" - gogit "github.com/go-git/go-git/v5" - gogitplumbing "github.com/go-git/go-git/v5/plumbing" - gogitcache "github.com/go-git/go-git/v5/plumbing/cache" - gogitobject "github.com/go-git/go-git/v5/plumbing/object" - gogitfilesystem "github.com/go-git/go-git/v5/storage/filesystem" -) - -type GoGitRepoProvider struct { - gitObjectCache cache.Cache[string, *gogitcache.ObjectLRU] -} - -func NewGoGitRepoProvider( - objectCacheMax int, - cacheDuration time.Duration, -) *GoGitRepoProvider { - c := cache.New[string, *gogitcache.ObjectLRU](gitObjectCacheGetter{ - maxSize: objectCacheMax, - }, cacheDuration) - return &GoGitRepoProvider{ - gitObjectCache: c, - } -} - -func (gr *GoGitRepoProvider) Get( - ctx context.Context, - path string, -) (*gogit.Repository, error) { - fs := gogitosfs.New(path) - stat, err := fs.Stat("") - if err != nil { - if os.IsNotExist(err) { - return nil, types.ErrRepositoryNotFound - } - - return nil, fmt.Errorf("failed to check repository existence: %w", err) - } - if !stat.IsDir() { - return nil, types.ErrRepositoryCorrupted - } - - gitObjectCache, err := gr.gitObjectCache.Get(ctx, path) - if err != nil { - return nil, fmt.Errorf("failed to get repository cache: %w", err) - } - - s := gogitfilesystem.NewStorage(fs, gitObjectCache) - - repo, err := gogit.Open(s, nil) - if err != nil { - return nil, err - } - - return repo, nil -} - -type gitObjectCacheGetter struct { - maxSize int -} - -func (r gitObjectCacheGetter) Find( - _ context.Context, - _ string, -) (*gogitcache.ObjectLRU, error) { - return gogitcache.NewObjectLRU(gogitcache.FileSize(r.maxSize)), nil -} - -func (a Adapter) getGoGitCommit( - ctx context.Context, - repoPath string, - rev string, -) (*gogit.Repository, *gogitobject.Commit, error) { - if repoPath == "" { - return nil, nil, ErrRepositoryPathEmpty - } - repo, err := a.repoProvider.Get(ctx, repoPath) - if err != nil { - return nil, nil, fmt.Errorf("failed to open repository: %w", err) - } - - var refSHA *gogitplumbing.Hash - if rev == "" { - var head *gogitplumbing.Reference - head, err = repo.Head() - if err != nil { - return nil, nil, errors.Internal("failed to get head: %w", err) - } - - headHash := head.Hash() - refSHA = &headHash - } else { - refSHA, err = repo.ResolveRevision(gogitplumbing.Revision(rev)) - if errors.Is(err, gogitplumbing.ErrReferenceNotFound) { - return nil, nil, errors.NotFound("reference not found '%s'", rev) - } else if err != nil { - return nil, nil, errors.Internal("failed to resolve revision '%s'", rev, err) - } - } - - refCommit, err := repo.CommitObject(*refSHA) - if err != nil { - return nil, nil, fmt.Errorf("failed to load commit data: %w", err) - } - - return repo, refCommit, nil -} diff --git a/git/adapter/mapping.go b/git/adapter/mapping.go index 604e37d4a..a608dc55b 100644 --- a/git/adapter/mapping.go +++ b/git/adapter/mapping.go @@ -82,24 +82,6 @@ func mapGiteaCommit(giteaCommit *gitea.Commit) (*types.Commit, error) { }, nil } -func mapGiteaNodeToTreeNodeModeAndType(giteaMode gitea.EntryMode) (types.TreeNodeType, types.TreeNodeMode, error) { - switch giteaMode { - case gitea.EntryModeBlob: - return types.TreeNodeTypeBlob, types.TreeNodeModeFile, nil - case gitea.EntryModeSymlink: - return types.TreeNodeTypeBlob, types.TreeNodeModeSymlink, nil - case gitea.EntryModeExec: - return types.TreeNodeTypeBlob, types.TreeNodeModeExec, nil - case gitea.EntryModeCommit: - return types.TreeNodeTypeCommit, types.TreeNodeModeCommit, nil - case gitea.EntryModeTree: - return types.TreeNodeTypeTree, types.TreeNodeModeTree, nil - default: - return types.TreeNodeTypeBlob, types.TreeNodeModeFile, - fmt.Errorf("received unknown tree node mode from gitea: '%s'", giteaMode.String()) - } -} - func mapGiteaSignature( giteaSignature *gitea.Signature, ) (types.Signature, error) { diff --git a/git/adapter/match_files.go b/git/adapter/match_files.go index 846c428e6..4367f21df 100644 --- a/git/adapter/match_files.go +++ b/git/adapter/match_files.go @@ -16,93 +16,86 @@ package adapter import ( "context" - "errors" "fmt" "io" "path" "github.com/harness/gitness/git/types" - gogitobject "github.com/go-git/go-git/v5/plumbing/object" + gitea "code.gitea.io/gitea/modules/git" ) -// nolint:gocognit +//nolint:gocognit func (a Adapter) MatchFiles( ctx context.Context, repoPath string, - ref string, - dirPath string, + rev string, + treePath string, pattern string, maxSize int, ) ([]types.FileContent, error) { - if repoPath == "" { - return nil, ErrRepositoryPathEmpty - } - _, refCommit, err := a.getGoGitCommit(ctx, repoPath, ref) + nodes, err := lsDirectory(ctx, repoPath, rev, treePath) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to list files in match files: %w", err) } - tree, err := refCommit.Tree() - if err != nil { - return nil, fmt.Errorf("failed to get tree for the commit: %w", err) - } - - if dirPath != "" { - tree, err = tree.Tree(dirPath) - if errors.Is(err, gogitobject.ErrDirectoryNotFound) { - return nil, &types.PathNotFoundError{Path: dirPath} - } - if err != nil { - return nil, fmt.Errorf("failed to navigate to %s directory: %w", dirPath, err) - } - } + catFileWriter, catFileReader, catFileStop := gitea.CatFileBatch(ctx, repoPath) + defer catFileStop() var files []types.FileContent - for i := range tree.Entries { - fileEntry := tree.Entries[i] - ok, err := path.Match(pattern, fileEntry.Name) + for i := range nodes { + if nodes[i].NodeType != types.TreeNodeTypeBlob { + continue + } + + fileName := nodes[i].Name + ok, err := path.Match(pattern, fileName) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to match file name against pattern: %w", err) } if !ok { continue } - name := fileEntry.Name - - f, err := tree.TreeEntryFile(&fileEntry) + _, err = catFileWriter.Write([]byte(nodes[i].Sha + "\n")) if err != nil { - return nil, fmt.Errorf("failed to get tree entry file %s: %w", name, err) + return nil, fmt.Errorf("failed to ask for file content from cat file batch: %w", err) } - reader, err := f.Reader() + _, _, size, err := gitea.ReadBatchLine(catFileReader) if err != nil { - return nil, fmt.Errorf("failed to open tree entry file %s: %w", name, err) + return nil, fmt.Errorf("failed to read cat-file batch header: %w", err) } - filePath := path.Join(dirPath, name) + reader := io.LimitReader(catFileReader, size+1) // plus eol - content, err := func(r io.ReadCloser) ([]byte, error) { - defer func() { - _ = r.Close() - }() - return io.ReadAll(io.LimitReader(reader, int64(maxSize))) - }(reader) - if err != nil { - return nil, fmt.Errorf("failed to read file content %s: %w", name, err) + if size > int64(maxSize) { + _, err = io.Copy(io.Discard, reader) + if err != nil { + return nil, fmt.Errorf("failed to discard a large file: %w", err) + } } - if len(content) == maxSize { - // skip truncated files + data, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("failed to read cat-file content: %w", err) + } + + if len(data) > 0 { + data = data[:len(data)-1] + } + + if len(data) == 0 { continue } files = append(files, types.FileContent{ - Path: filePath, - Content: content, + Path: nodes[i].Path, + Content: data, }) } + _ = catFileWriter.Close() + return files, nil } diff --git a/git/adapter/setup_test.go b/git/adapter/setup_test.go index abd92324c..0cd2fbe51 100644 --- a/git/adapter/setup_test.go +++ b/git/adapter/setup_test.go @@ -44,10 +44,8 @@ var ( func setupGit(t *testing.T) adapter.Adapter { t.Helper() - gogitProvider := adapter.ProvideGoGitRepoProvider() git, err := adapter.New( types.Config{Trace: true}, - gogitProvider, adapter.NewInMemoryLastCommitCache(5*time.Minute), ) if err != nil { diff --git a/git/adapter/tree.go b/git/adapter/tree.go index 6efe825e8..7b8f84436 100644 --- a/git/adapter/tree.go +++ b/git/adapter/tree.go @@ -15,14 +15,16 @@ package adapter import ( + "bufio" "bytes" "context" "fmt" "io" "path" - "path/filepath" + "regexp" "strings" + "github.com/harness/gitness/errors" "github.com/harness/gitness/git/types" gitea "code.gitea.io/gitea/modules/git" @@ -32,108 +34,173 @@ func cleanTreePath(treePath string) string { return strings.Trim(path.Clean("/"+treePath), "/") } -// GetTreeNode returns the tree node at the given path as found for the provided reference. -// Note: ref can be Branch / Tag / CommitSHA. -func (a Adapter) GetTreeNode( - ctx context.Context, - repoPath string, - ref string, - treePath string, -) (*types.TreeNode, error) { - treePath = cleanTreePath(treePath) - - giteaRepo, err := gitea.OpenRepository(ctx, repoPath) - if err != nil { - return nil, processGiteaErrorf(err, "failed to open repository") +func parseTreeNodeMode(s string) (types.TreeNodeType, types.TreeNodeMode, error) { + switch s { + case "100644": + return types.TreeNodeTypeBlob, types.TreeNodeModeFile, nil + case "120000": + return types.TreeNodeTypeBlob, types.TreeNodeModeSymlink, nil + case "100775": + return types.TreeNodeTypeBlob, types.TreeNodeModeExec, nil + case "160000": + return types.TreeNodeTypeCommit, types.TreeNodeModeCommit, nil + case "040000": + return types.TreeNodeTypeTree, types.TreeNodeModeTree, nil + default: + return types.TreeNodeTypeBlob, types.TreeNodeModeFile, + fmt.Errorf("unknown git tree node mode: '%s'", s) } - defer giteaRepo.Close() - - // Get the giteaCommit object for the ref - giteaCommit, err := giteaRepo.GetCommit(ref) - if err != nil { - return nil, processGiteaErrorf(err, "error getting commit for ref '%s'", ref) - } - - // TODO: handle ErrNotExist :) - giteaTreeEntry, err := giteaCommit.GetTreeEntryByPath(treePath) - if err != nil { - return nil, processGiteaErrorf(err, "failed to get tree entry for commit '%s' at path '%s'", - giteaCommit.ID.String(), treePath) - } - - nodeType, mode, err := mapGiteaNodeToTreeNodeModeAndType(giteaTreeEntry.Mode()) - if err != nil { - return nil, err - } - - return &types.TreeNode{ - Mode: mode, - NodeType: nodeType, - Sha: giteaTreeEntry.ID.String(), - Name: giteaTreeEntry.Name(), - Path: treePath, - }, nil } -// ListTreeNodes lists the child nodes of a tree reachable from ref via the specified path -// and includes the latest commit for all nodes if requested. -// Note: ref can be Branch / Tag / CommitSHA. -func (a Adapter) ListTreeNodes( +func scanZeroSeparated(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil // Return nothing if at end of file and no data passed + } + if i := strings.IndexByte(string(data), 0); i >= 0 { + return i + 1, data[0:i], nil // Split at zero byte + } + if atEOF { + return len(data), data, nil // at the end of file return the data + } + return +} + +var regexpLsTreeLongColumns = regexp.MustCompile(`^(\d{6})\s+(\w+)\s+(\w+)\t(.+)$`) + +func lsTree( ctx context.Context, repoPath string, - ref string, + rev string, treePath string, ) ([]types.TreeNode, error) { + if repoPath == "" { + return nil, ErrRepositoryPathEmpty + } + + args := []string{"ls-tree", "-z", rev, treePath} + output, stderr, err := gitea.NewCommand(ctx, args...).RunStdString(&gitea.RunOpts{Dir: repoPath}) + if strings.Contains(stderr, "fatal: Not a valid object name") { + return nil, errors.InvalidArgument("revision %q not found", rev) + } + if err != nil { + return nil, fmt.Errorf("failed to run git ls-tree: %w", err) + } + + if output == "" { + return nil, errors.Format(errors.StatusPathNotFound, "path %q not found", treePath) + } + + n := strings.Count(output, "\x00") + + list := make([]types.TreeNode, 0, n) + scan := bufio.NewScanner(strings.NewReader(output)) + scan.Split(scanZeroSeparated) + for scan.Scan() { + columns := regexpLsTreeLongColumns.FindStringSubmatch(scan.Text()) + if columns == nil { + return nil, errors.New("unrecognized format of git directory listing") + } + + nodeType, nodeMode, err := parseTreeNodeMode(columns[1]) + if err != nil { + return nil, fmt.Errorf("failed to parse git mode: %w", err) + } + + nodeSha := columns[3] + nodePath := columns[4] + nodeName := path.Base(nodePath) + + list = append(list, types.TreeNode{ + NodeType: nodeType, + Mode: nodeMode, + Sha: nodeSha, + Name: nodeName, + Path: nodePath, + }) + } + + return list, nil +} + +// lsFile returns all tree node entries in the requested directory. +func lsDirectory( + ctx context.Context, + repoPath string, + rev string, + treePath string, +) ([]types.TreeNode, error) { + treePath = path.Clean(treePath) + if treePath == "" { + treePath = "." + } else { + treePath += "/" + } + + return lsTree(ctx, repoPath, rev, treePath) +} + +// lsFile returns one tree node entry. +func lsFile( + ctx context.Context, + repoPath string, + rev string, + treePath string, +) (types.TreeNode, error) { treePath = cleanTreePath(treePath) - giteaRepo, err := gitea.OpenRepository(ctx, repoPath) + list, err := lsTree(ctx, repoPath, rev, treePath) if err != nil { - return nil, processGiteaErrorf(err, "failed to open repository") + return types.TreeNode{}, fmt.Errorf("failed to ls file: %w", err) } - defer giteaRepo.Close() - - // Get the giteaCommit object for the ref - giteaCommit, err := giteaRepo.GetCommit(ref) - if err != nil { - return nil, processGiteaErrorf(err, "error getting commit for ref '%s'", ref) + if len(list) != 1 { + return types.TreeNode{}, fmt.Errorf("ls file list contains more than one element, len=%d", len(list)) } - // Get the giteaTree object for the ref - giteaTree, err := giteaCommit.SubTree(treePath) - if err != nil { - return nil, processGiteaErrorf(err, "error getting tree for '%s'", treePath) - } + return list[0], nil +} - giteaEntries, err := giteaTree.ListEntries() - if err != nil { - return nil, processGiteaErrorf(err, "failed to list entries for tree '%s'", treePath) - } +// GetTreeNode returns the tree node at the given path as found for the provided reference. +func (a Adapter) GetTreeNode(ctx context.Context, repoPath, rev, treePath string) (*types.TreeNode, error) { + // root path (empty path) is a special case + if treePath == "" { + if repoPath == "" { + return nil, ErrRepositoryPathEmpty + } - nodes := make([]types.TreeNode, len(giteaEntries)) - for i := range giteaEntries { - giteaEntry := giteaEntries[i] - - var nodeType types.TreeNodeType - var mode types.TreeNodeMode - nodeType, mode, err = mapGiteaNodeToTreeNodeModeAndType(giteaEntry.Mode()) + args := []string{"show", "--no-patch", "--format=" + fmtTreeHash, rev} + treeSHA, stderr, err := gitea.NewCommand(ctx, args...).RunStdString(&gitea.RunOpts{Dir: repoPath}) + if strings.Contains(stderr, "ambiguous argument") { + return nil, errors.InvalidArgument("could not resolve git revision: %s", rev) + } if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get root tree node: %w", err) } - // giteaNode.Name() returns the path of the node relative to the tree. - relPath := giteaEntry.Name() - name := filepath.Base(relPath) - - nodes[i] = types.TreeNode{ - NodeType: nodeType, - Mode: mode, - Sha: giteaEntry.ID.String(), - Name: name, - Path: filepath.Join(treePath, relPath), - } + return &types.TreeNode{ + NodeType: types.TreeNodeTypeTree, + Mode: types.TreeNodeModeTree, + Sha: treeSHA, + Name: "", + Path: "", + }, err } - return nodes, nil + treeNode, err := lsFile(ctx, repoPath, rev, treePath) + if err != nil { + return nil, fmt.Errorf("failed to get tree node: %w", err) + } + + return &treeNode, nil +} + +// ListTreeNodes lists the child nodes of a tree reachable from ref via the specified path. +func (a Adapter) ListTreeNodes(ctx context.Context, repoPath, rev, treePath string) ([]types.TreeNode, error) { + list, err := lsDirectory(ctx, repoPath, rev, treePath) + if err != nil { + return nil, fmt.Errorf("failed to list tree nodes: %w", err) + } + + return list, nil } func (a Adapter) ReadTree( diff --git a/git/adapter/wire.go b/git/adapter/wire.go index e6298a4f6..39e942e2a 100644 --- a/git/adapter/wire.go +++ b/git/adapter/wire.go @@ -27,15 +27,9 @@ import ( ) var WireSet = wire.NewSet( - ProvideGoGitRepoProvider, ProvideLastCommitCache, ) -func ProvideGoGitRepoProvider() *GoGitRepoProvider { - const objectCacheSize = 16 << 20 // 16MiB - return NewGoGitRepoProvider(objectCacheSize, 15*time.Minute) -} - func ProvideLastCommitCache( config types.Config, redisClient redis.UniversalClient, diff --git a/git/blob.go b/git/blob.go index 6e65b4390..db621f0b9 100644 --- a/git/blob.go +++ b/git/blob.go @@ -17,8 +17,6 @@ package git import ( "context" "io" - - "github.com/rs/zerolog/log" ) type GetBlobParams struct { @@ -34,7 +32,7 @@ type GetBlobOutput struct { // ContentSize is the total number of bytes returned by the Content Reader. ContentSize int64 // Content contains the (partial) content of the blob. - Content io.Reader + Content io.ReadCloser } func (s *Service) GetBlob(ctx context.Context, params *GetBlobParams) (*GetBlobOutput, error) { @@ -49,12 +47,6 @@ func (s *Service) GetBlob(ctx context.Context, params *GetBlobParams) (*GetBlobO if err != nil { return nil, err } - defer func() { - dErr := reader.Content.Close() - if dErr != nil { - log.Ctx(ctx).Warn().Err(err).Msgf("failed to close blob content reader.") - } - }() return &GetBlobOutput{ SHA: reader.SHA, diff --git a/git/wire.go b/git/wire.go index 19efab794..2da5eff7f 100644 --- a/git/wire.go +++ b/git/wire.go @@ -31,10 +31,9 @@ var WireSet = wire.NewSet( func ProvideGITAdapter( config types.Config, - repoProvider *adapter.GoGitRepoProvider, lastCommitCache cache.Cache[adapter.CommitEntryKey, *types.Commit], ) (Adapter, error) { - return adapter.New(config, repoProvider, lastCommitCache) + return adapter.New(config, lastCommitCache) } func ProvideService(config types.Config, adapter Adapter, storage storage.Store) (Interface, error) {