in internal/gitaly/service/commit/get_tree_entries.go [77:260]
func (s *server) sendTreeEntriesUnified(
stream gitalypb.CommitService_GetTreeEntriesServer,
repo *localrepo.Repo,
revision, path string,
recursive bool,
skipFlatPaths bool,
sort gitalypb.GetTreeEntriesRequest_SortBy,
p *gitalypb.PaginationParameter,
) error {
ctx := stream.Context()
// While both repo.ReadTree and catfile.TreeEntries do this internally, in the case
// of non-recursive path, we do repo.ResolveRevision, which could fail because of this.
if path == "." {
path = ""
}
var readTreeOpts []localrepo.ReadTreeOption
if recursive {
readTreeOpts = append(readTreeOpts, localrepo.WithRecursive())
}
var hasPageTokenTreeOID bool
treeRevision := revision
if p != nil && p.GetPageToken() != "" {
// Extract root tree OID from the token, if present.
// The root tree OID is used to ensure that subsequent paginated requests access the same tree
_, tokenTreeOID, _ := decodePageToken(p.GetPageToken())
if tokenTreeOID != "" {
treeRevision = tokenTreeOID
hasPageTokenTreeOID = true
}
}
// When tree OID resolved from the previous request is used instead of the revision,
// the path is no longer relative to the revision. Please refer https://gitlab.com/gitlab-org/gitaly/-/issues/4556#note_2004951285
// for more details.
if !hasPageTokenTreeOID {
readTreeOpts = append(readTreeOpts, localrepo.WithRelativePath(path))
}
tree, err := repo.ReadTree(
ctx,
git.Revision(treeRevision),
readTreeOpts...,
)
if err != nil {
if errors.Is(err, localrepo.ErrNotTreeish) {
return structerr.NewInvalidArgument("path not treeish").WithDetail(&gitalypb.GetTreeEntriesError{
Error: &gitalypb.GetTreeEntriesError_ResolveTree{
ResolveTree: &gitalypb.ResolveRevisionError{
Revision: []byte(revision),
},
},
}).WithMetadataItems(
structerr.MetadataItem{Key: "path", Value: path},
structerr.MetadataItem{Key: "revision", Value: revision},
)
}
if errors.Is(err, localrepo.ErrTreeNotExist) {
return structerr.NewNotFound("revision doesn't exist").WithDetail(&gitalypb.GetTreeEntriesError{
Error: &gitalypb.GetTreeEntriesError_ResolveTree{
ResolveTree: &gitalypb.ResolveRevisionError{
Revision: []byte(revision),
},
},
}).WithMetadataItems(
structerr.MetadataItem{Key: "path", Value: path},
structerr.MetadataItem{Key: "revision", Value: revision},
)
}
if errors.Is(err, git.ErrReferenceNotFound) {
// Since we rely on repo.ResolveRevision, it could either be an invalid revision
// or an invalid path.
var grpcErr structerr.Error
// Return a different gRPC error code for each case to match the old implementation.
// We should probably change this to NewNotFound in a separate MR and FF once the
// UseUnifiedGetTreeEntries FF is fully rolled out.
if recursive {
grpcErr = structerr.NewNotFound("invalid revision or path")
} else {
grpcErr = structerr.NewInvalidArgument("invalid revision or path")
}
return grpcErr.WithDetail(&gitalypb.GetTreeEntriesError{
Error: &gitalypb.GetTreeEntriesError_ResolveTree{
ResolveTree: &gitalypb.ResolveRevisionError{
Revision: []byte(revision),
},
},
}).WithMetadataItems(
structerr.MetadataItem{Key: "path", Value: path},
structerr.MetadataItem{Key: "revision", Value: revision},
)
}
return fmt.Errorf("reading tree: %w", err)
}
var entries []*gitalypb.TreeEntry
if err := tree.Walk(func(dir string, entry *localrepo.TreeEntry) error {
if entry.OID == tree.OID {
return nil
}
objectID, err := entry.OID.Bytes()
if err != nil {
return fmt.Errorf("converting tree entry OID: %w", err)
}
newEntry, err := git.NewTreeEntry(
revision,
path,
[]byte(filepath.Join(dir, entry.Path)),
objectID,
[]byte(entry.Mode),
)
if err != nil {
return fmt.Errorf("converting tree entry: %w", err)
}
entries = append(entries, newEntry)
return nil
}); err != nil {
return fmt.Errorf("listing tree entries: %w", err)
}
// We sort before we paginate to ensure consistent results with ListLastCommitsForTree
entries, err = sortTrees(entries, sort)
if err != nil {
return err
}
cursor := ""
if p != nil {
entries, cursor, err = paginateTreeEntries(ctx, entries, p, tree.OID)
if err != nil {
return err
}
}
treeSender := &treeEntriesSender{stream: stream}
if cursor != "" {
treeSender.SetPaginationCursor(cursor)
}
if !recursive && !skipFlatPaths {
// When we're not doing a recursive request, then we need to populate flat
// paths. A flat path of a tree entry refers to the first subtree of that
// entry which either has at least one blob or more than two subtrees. In
// other terms, it refers to the first "non-empty" subtree such that it's
// easy to skip navigating the intermediate subtrees which wouldn't carry
// any interesting information anyway.
//
// Unfortunately, computing flat paths is _really_ inefficient: for each
// tree entry, we recurse up to 10 levels deep into that subtree. We do so
// by requesting the tree entries via a catfile process, which to the best
// of my knowledge is as good as we can get. Doing this via git-ls-tree(1)
// wouldn't fly: we'd have to spawn a separate process for each of the
// subtrees, which is a lot of overhead.
objectReader, cancel, err := s.catfileCache.ObjectReader(stream.Context(), repo)
if err != nil {
return err
}
defer cancel()
if err := populateFlatPath(ctx, objectReader, entries); err != nil {
return err
}
}
sender := chunk.New(treeSender)
for _, e := range entries {
if err := sender.Send(e); err != nil {
return err
}
}
return sender.Flush()
}