Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlejandro Rodríguez <alejorro70@gmail.com>2017-07-14 19:01:12 +0300
committerAlejandro Rodríguez <alejorro70@gmail.com>2017-07-14 19:01:12 +0300
commitd2978e6eff30332b11b5ec47e0f01915dd3eeb52 (patch)
tree79208e9498bba08bf145bb245183194e1f51cf8e
parente9a16b20f9b190012c4c7895c17fd54ac36b1e99 (diff)
parenta8f32a8111ac8b8aa7776e55b9a40d8190494fe2 (diff)
Merge branch '359-commitservice-gettreeentries-server-implementation' into 'master'
CommitService::GetTreeEntries server implementation Closes #359 See merge request !208
-rw-r--r--CHANGELOG.md2
-rw-r--r--internal/git/catfile/catfile.go29
-rw-r--r--internal/service/commit/server.go4
-rw-r--r--internal/service/commit/testhelper_test.go6
-rw-r--r--internal/service/commit/tree_entries.go69
-rw-r--r--internal/service/commit/tree_entries_helper.go102
-rw-r--r--internal/service/commit/tree_entries_test.go281
-rw-r--r--internal/service/commit/tree_entry.go269
-rw-r--r--internal/service/commit/util.go32
9 files changed, 607 insertions, 187 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d7d442f8..bc4565b16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ UNRELEASED
https://gitlab.com/gitlab-org/gitaly/merge_requests/216
- Streamio chunk size optimizations
https://gitlab.com/gitlab-org/gitaly/merge_requests/206
+- Implement CommitService.GetTreeEntries
+ https://gitlab.com/gitlab-org/gitaly/merge_requests/208
v0.18.0
diff --git a/internal/git/catfile/catfile.go b/internal/git/catfile/catfile.go
index 880637acd..504373dfb 100644
--- a/internal/git/catfile/catfile.go
+++ b/internal/git/catfile/catfile.go
@@ -3,8 +3,14 @@ package catfile
import (
"bufio"
"fmt"
+ "io"
+ "os/exec"
"strconv"
"strings"
+
+ "gitlab.com/gitlab-org/gitaly/internal/helper"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
)
// ObjectInfo represents a header returned by `git cat-file --batch`
@@ -14,6 +20,29 @@ type ObjectInfo struct {
Size int64
}
+// Handler takes care of writing to stdin and reading from stdout of
+// `git cat-file --batch`
+type Handler func(io.Writer, *bufio.Reader) error
+
+// CatFile fetches the tree entries information using git cat-file. It
+// calls the handler with the TreeEntry slice, and an stdin reader and a stdout
+// writer in case the handler wants to perform addition cat-file operations.
+func CatFile(repoPath string, handler Handler) error {
+ stdinReader, stdinWriter := io.Pipe()
+ cmdArgs := []string{"--git-dir", repoPath, "cat-file", "--batch"}
+ cmd, err := helper.NewCommand(exec.Command(helper.GitPath(), cmdArgs...), stdinReader, nil, nil)
+ if err != nil {
+ return grpc.Errorf(codes.Internal, "CatFile: cmd: %v", err)
+ }
+ defer cmd.Kill()
+ defer stdinWriter.Close()
+ defer stdinReader.Close()
+
+ stdout := bufio.NewReader(cmd)
+
+ return handler(stdinWriter, stdout)
+}
+
// ParseObjectInfo reads and parses one header line from `git cat-file --batch`
func ParseObjectInfo(stdout *bufio.Reader) (*ObjectInfo, error) {
infoLine, err := stdout.ReadString('\n')
diff --git a/internal/service/commit/server.go b/internal/service/commit/server.go
index 142e109fd..e887fcd4d 100644
--- a/internal/service/commit/server.go
+++ b/internal/service/commit/server.go
@@ -17,10 +17,6 @@ func (server) FindCommit(ctx context.Context, in *pb.FindCommitRequest) (*pb.Fin
return nil, nil
}
-func (server) GetTreeEntries(*pb.GetTreeEntriesRequest, pb.CommitService_GetTreeEntriesServer) error {
- return nil
-}
-
func (server) ListFiles(*pb.ListFilesRequest, pb.CommitService_ListFilesServer) error {
return nil
}
diff --git a/internal/service/commit/testhelper_test.go b/internal/service/commit/testhelper_test.go
index 0dc2f0cea..264e6cecc 100644
--- a/internal/service/commit/testhelper_test.go
+++ b/internal/service/commit/testhelper_test.go
@@ -1,6 +1,7 @@
package commit
import (
+ "bytes"
"net"
"os"
"path"
@@ -111,3 +112,8 @@ func newCommitServiceClient(t *testing.T) pb.CommitServiceClient {
return pb.NewCommitServiceClient(conn)
}
+
+func treeEntriesEqual(a, b *pb.TreeEntry) bool {
+ return a.CommitOid == b.CommitOid && a.Oid == b.Oid && a.Mode == b.Mode &&
+ bytes.Equal(a.Path, b.Path) && a.RootOid == b.RootOid && a.Type == b.Type
+}
diff --git a/internal/service/commit/tree_entries.go b/internal/service/commit/tree_entries.go
new file mode 100644
index 000000000..735c15355
--- /dev/null
+++ b/internal/service/commit/tree_entries.go
@@ -0,0 +1,69 @@
+package commit
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+
+ pb "gitlab.com/gitlab-org/gitaly-proto/go"
+ "gitlab.com/gitlab-org/gitaly/internal/git/catfile"
+ "gitlab.com/gitlab-org/gitaly/internal/helper"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+)
+
+var maxTreeEntries = 1000
+
+func validateGetTreeEntriesRequest(in *pb.GetTreeEntriesRequest) error {
+ if len(in.GetRevision()) == 0 {
+ return fmt.Errorf("empty Revision")
+ }
+
+ if len(in.GetPath()) == 0 {
+ return fmt.Errorf("empty Path")
+ }
+
+ return nil
+}
+
+func getTreeEntriesHandler(stream pb.CommitService_GetTreeEntriesServer, revision, path string) catfile.Handler {
+ return func(stdin io.Writer, stdout *bufio.Reader) error {
+ entries, err := treeEntries(revision, path, stdin, stdout)
+ if err != nil {
+ return err
+ }
+
+ for len(entries) > maxTreeEntries {
+ chunk := &pb.GetTreeEntriesResponse{
+ Entries: entries[:maxTreeEntries],
+ }
+ if err := stream.Send(chunk); err != nil {
+ return err
+ }
+ entries = entries[maxTreeEntries:]
+ }
+
+ if len(entries) > 0 {
+ return stream.Send(&pb.GetTreeEntriesResponse{Entries: entries})
+ }
+
+ return nil
+ }
+}
+
+func (s *server) GetTreeEntries(in *pb.GetTreeEntriesRequest, stream pb.CommitService_GetTreeEntriesServer) error {
+ if err := validateGetTreeEntriesRequest(in); err != nil {
+ return grpc.Errorf(codes.InvalidArgument, "TreeEntry: %v", err)
+ }
+
+ repoPath, err := helper.GetRepoPath(in.Repository)
+ if err != nil {
+ return err
+ }
+
+ revision := string(in.GetRevision())
+ path := string(in.GetPath())
+ handler := getTreeEntriesHandler(stream, revision, path)
+
+ return catfile.CatFile(repoPath, handler)
+}
diff --git a/internal/service/commit/tree_entries_helper.go b/internal/service/commit/tree_entries_helper.go
new file mode 100644
index 000000000..326d0956a
--- /dev/null
+++ b/internal/service/commit/tree_entries_helper.go
@@ -0,0 +1,102 @@
+package commit
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+
+ pb "gitlab.com/gitlab-org/gitaly-proto/go"
+ "gitlab.com/gitlab-org/gitaly/internal/git/catfile"
+)
+
+func getTreeInfo(revision, path string, stdin io.Writer, stdout *bufio.Reader) (*catfile.ObjectInfo, error) {
+ if _, err := fmt.Fprintf(stdin, "%s^{tree}:%s\n", revision, path); err != nil {
+ return nil, grpc.Errorf(codes.Internal, "TreeEntry: stdin write: %v", err)
+ }
+
+ treeInfo, err := catfile.ParseObjectInfo(stdout)
+ if err != nil {
+ return nil, grpc.Errorf(codes.Internal, "TreeEntry: %v", err)
+ }
+ return treeInfo, nil
+}
+
+func extractEntryInfoFromTreeData(stdout *bufio.Reader, commitOid, rootOid string, treeInfo *catfile.ObjectInfo) ([]*pb.TreeEntry, error) {
+ var entries []*pb.TreeEntry
+ var modeBytes, path []byte
+ var err error
+
+ // Non-existing tree, return empty entry list
+ if len(treeInfo.Oid) == 0 {
+ return entries, nil
+ }
+
+ oidBytes := make([]byte, 20)
+ bytesLeft := treeInfo.Size
+
+ for bytesLeft > 0 {
+ modeBytes, err = stdout.ReadBytes(' ')
+ if err != nil || len(modeBytes) <= 1 {
+ return nil, fmt.Errorf("read entry mode: %v", err)
+ }
+ bytesLeft -= int64(len(modeBytes))
+ modeBytes = modeBytes[:len(modeBytes)-1]
+
+ path, err = stdout.ReadBytes('\x00')
+ if err != nil || len(path) <= 1 {
+ return nil, fmt.Errorf("read entry path: %v", err)
+ }
+ bytesLeft -= int64(len(path))
+ path = path[:len(path)-1]
+
+ if n, _ := stdout.Read(oidBytes); n != 20 {
+ return nil, fmt.Errorf("read entry oid: %v", err)
+ }
+
+ bytesLeft -= int64(len(oidBytes))
+
+ treeEntry, err := newTreeEntry(commitOid, rootOid, path, oidBytes, modeBytes)
+ if err != nil {
+ return nil, fmt.Errorf("new entry info: %v", err)
+ }
+
+ entries = append(entries, treeEntry)
+ }
+
+ // Extra byte for a linefeed at the end
+ if _, err := stdout.Discard(int(bytesLeft + 1)); err != nil {
+ return nil, fmt.Errorf("stdout discard: %v", err)
+ }
+
+ return entries, nil
+}
+
+func treeEntries(revision, path string, stdin io.Writer, stdout *bufio.Reader) ([]*pb.TreeEntry, error) {
+ if path == "." {
+ path = ""
+ }
+
+ // We always need to process the root path to get the rootTreeInfo.Oid
+ rootTreeInfo, err := getTreeInfo(revision, "", stdin, stdout)
+ if err != nil {
+ return nil, err
+ }
+ entries, err := extractEntryInfoFromTreeData(stdout, revision, rootTreeInfo.Oid, rootTreeInfo)
+ if err != nil {
+ return nil, err
+ }
+
+ // If we were asked for the root path, good luck! We're done
+ if path == "" {
+ return entries, nil
+ }
+
+ treeInfo, err := getTreeInfo(revision, path, stdin, stdout)
+ if err != nil {
+ return nil, err
+ }
+ return extractEntryInfoFromTreeData(stdout, revision, rootTreeInfo.Oid, treeInfo)
+}
diff --git a/internal/service/commit/tree_entries_test.go b/internal/service/commit/tree_entries_test.go
new file mode 100644
index 000000000..dcebf5004
--- /dev/null
+++ b/internal/service/commit/tree_entries_test.go
@@ -0,0 +1,281 @@
+package commit
+
+import (
+ "io"
+ "testing"
+
+ "google.golang.org/grpc/codes"
+
+ "golang.org/x/net/context"
+
+ pb "gitlab.com/gitlab-org/gitaly-proto/go"
+ "gitlab.com/gitlab-org/gitaly/internal/testhelper"
+)
+
+func TestSuccessfulGetTreeEntries(t *testing.T) {
+ // Force entries to be sliced to test that behaviour
+ oldMaxTreeEntries := maxTreeEntries
+ maxTreeEntries = 1
+ defer func() {
+ maxTreeEntries = oldMaxTreeEntries
+ }()
+
+ commitID := "913c66a37b4a45b9769037c55c2d238bd0942d2e"
+ rootOid := "faafbe7fe23fb83c664c78aaded9566c8f934412"
+ client := newCommitServiceClient(t)
+ rootEntries := []*pb.TreeEntry{
+ {
+ Oid: "fd90a3d2d21d6b4f9bec2c33fb7f49780c55f0d2",
+ RootOid: rootOid,
+ Path: []byte(".DS_Store"),
+ Type: pb.TreeEntry_BLOB,
+ Mode: 0100644,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "470ad2fcf1e33798f1afc5781d08e60c40f51e7a",
+ RootOid: rootOid,
+ Path: []byte(".gitignore"),
+ Type: pb.TreeEntry_BLOB,
+ Mode: 0100644,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "fdaada1754989978413d618ee1fb1c0469d6a664",
+ RootOid: rootOid,
+ Path: []byte(".gitmodules"),
+ Type: pb.TreeEntry_BLOB,
+ Mode: 0100644,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "c74175afd117781cbc983664339a0f599b5bb34e",
+ RootOid: rootOid,
+ Path: []byte("CHANGELOG"),
+ Type: pb.TreeEntry_BLOB,
+ Mode: 0100644,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "c1788657b95998a2f177a4f86d68a60f2a80117f",
+ RootOid: rootOid,
+ Path: []byte("CONTRIBUTING.md"),
+ Type: pb.TreeEntry_BLOB,
+ Mode: 0100644,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "50b27c6518be44c42c4d87966ae2481ce895624c",
+ RootOid: rootOid,
+ Path: []byte("LICENSE"),
+ Type: pb.TreeEntry_BLOB,
+ Mode: 0100644,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "95d9f0a5e7bb054e9dd3975589b8dfc689e20e88",
+ RootOid: rootOid,
+ Path: []byte("MAINTENANCE.md"),
+ Type: pb.TreeEntry_BLOB,
+ Mode: 0100644,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "bf757025c40c62e6ffa6f11d3819c769a76dbe09",
+ RootOid: rootOid,
+ Path: []byte("PROCESS.md"),
+ Type: pb.TreeEntry_BLOB,
+ Mode: 0100644,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "faaf198af3a36dbf41961466703cc1d47c61d051",
+ RootOid: rootOid,
+ Path: []byte("README.md"),
+ Type: pb.TreeEntry_BLOB,
+ Mode: 0100644,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "998707b421c89bd9a3063333f9f728ef3e43d101",
+ RootOid: rootOid,
+ Path: []byte("VERSION"),
+ Type: pb.TreeEntry_BLOB,
+ Mode: 0100644,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "3c122d2b7830eca25235131070602575cf8b41a1",
+ RootOid: rootOid,
+ Path: []byte("encoding"),
+ Type: pb.TreeEntry_TREE,
+ Mode: 040000,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "ab746f8ad0b84b147290041dc13cc9c7adc52930",
+ RootOid: rootOid,
+ Path: []byte("files"),
+ Type: pb.TreeEntry_TREE,
+ Mode: 040000,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "409f37c4f05865e4fb208c771485f211a22c4c2d",
+ RootOid: rootOid,
+ Path: []byte("six"),
+ Type: pb.TreeEntry_COMMIT,
+ Mode: 0160000,
+ CommitOid: commitID,
+ },
+ }
+ filesDirEntries := []*pb.TreeEntry{
+ {
+ Oid: "60d7a906c2fd9e4509aeb1187b98d0ea7ce827c9",
+ RootOid: rootOid,
+ Path: []byte(".DS_Store"),
+ Type: pb.TreeEntry_BLOB,
+ Mode: 0100644,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba",
+ RootOid: rootOid,
+ Path: []byte("html"),
+ Type: pb.TreeEntry_TREE,
+ Mode: 040000,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "5e147e3af6740ee83103ec2ecdf846cae696edd1",
+ RootOid: rootOid,
+ Path: []byte("images"),
+ Type: pb.TreeEntry_TREE,
+ Mode: 040000,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "7853101769f3421725ddc41439c2cd4610e37ad9",
+ RootOid: rootOid,
+ Path: []byte("js"),
+ Type: pb.TreeEntry_TREE,
+ Mode: 040000,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "fd581c619bf59cfdfa9c8282377bb09c2f897520",
+ RootOid: rootOid,
+ Path: []byte("markdown"),
+ Type: pb.TreeEntry_TREE,
+ Mode: 040000,
+ CommitOid: commitID,
+ },
+ {
+ Oid: "b59dbe4a27371d53e61bf3cb8bef66be53572db0",
+ RootOid: rootOid,
+ Path: []byte("ruby"),
+ Type: pb.TreeEntry_TREE,
+ Mode: 040000,
+ CommitOid: commitID,
+ },
+ }
+
+ testCases := []struct {
+ description string
+ revision []byte
+ path []byte
+ entries []*pb.TreeEntry
+ }{
+ {
+ description: "with root path",
+ revision: []byte(commitID),
+ path: []byte("."),
+ entries: rootEntries,
+ },
+ {
+ description: "with a folder",
+ revision: []byte(commitID),
+ path: []byte("files"),
+ entries: filesDirEntries,
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Logf("test case: %s", testCase.description)
+
+ request := &pb.GetTreeEntriesRequest{
+ Repository: testRepo,
+ Revision: testCase.revision,
+ Path: testCase.path,
+ }
+
+ c, err := client.GetTreeEntries(context.Background(), request)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertTreeEntriesReceived(t, c, testCase.entries)
+ }
+}
+
+func assertTreeEntriesReceived(t *testing.T, client pb.CommitService_GetTreeEntriesClient, entries []*pb.TreeEntry) {
+ fetchedEntries := getTreeEntriesFromTreeEntryClient(t, client)
+
+ if len(fetchedEntries) != len(entries) {
+ t.Fatalf("Expected %d entries, got %d instead", len(entries), len(fetchedEntries))
+ }
+
+ for i, entry := range fetchedEntries {
+ if !treeEntriesEqual(entry, entries[i]) {
+ t.Fatalf("Expected tree entry %v, got %v instead", entries[i], entry)
+ }
+ }
+}
+
+func getTreeEntriesFromTreeEntryClient(t *testing.T, client pb.CommitService_GetTreeEntriesClient) []*pb.TreeEntry {
+ var entries []*pb.TreeEntry
+ for {
+ resp, err := client.Recv()
+ if err == io.EOF {
+ break
+ } else if err != nil {
+ t.Fatal(err)
+ }
+
+ entries = append(entries, resp.Entries...)
+ }
+ return entries
+}
+
+func TestFailedGetTreeEntriesRequestDueToValidationError(t *testing.T) {
+ client := newCommitServiceClient(t)
+ revision := []byte("d42783470dc29fde2cf459eb3199ee1d7e3f3a72")
+ path := []byte("a/b/c")
+
+ rpcRequests := []pb.GetTreeEntriesRequest{
+ {Repository: &pb.Repository{StorageName: "fake", RelativePath: "path"}, Revision: revision, Path: path}, // Repository doesn't exist
+ {Repository: nil, Revision: revision, Path: path}, // Repository is nil
+ {Repository: testRepo, Revision: nil, Path: path}, // Revision is empty
+ {Repository: testRepo, Revision: revision}, // Path is empty
+ }
+
+ for _, rpcRequest := range rpcRequests {
+ t.Logf("test case: %v", rpcRequest)
+
+ c, err := client.GetTreeEntries(context.Background(), &rpcRequest)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = drainTreeEntriesResponse(c)
+ testhelper.AssertGrpcError(t, err, codes.InvalidArgument, "")
+ }
+}
+
+func drainTreeEntriesResponse(c pb.CommitService_GetTreeEntriesClient) error {
+ var err error
+ for err == nil {
+ _, err = c.Recv()
+ }
+ return err
+}
diff --git a/internal/service/commit/tree_entry.go b/internal/service/commit/tree_entry.go
index 6cbd6610b..3b25c74b9 100644
--- a/internal/service/commit/tree_entry.go
+++ b/internal/service/commit/tree_entry.go
@@ -4,152 +4,128 @@ import (
"bufio"
"fmt"
"io"
- "os/exec"
"path"
- "strconv"
+ "strings"
"gitlab.com/gitlab-org/gitaly/internal/git/catfile"
"gitlab.com/gitlab-org/gitaly/internal/helper"
+ "gitlab.com/gitlab-org/gitaly/streamio"
pb "gitlab.com/gitlab-org/gitaly-proto/go"
- "gitlab.com/gitlab-org/gitaly/streamio"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
)
-type entryInfo struct {
- objectType string
- oid string
- mode int32
-}
+func treeEntryHandler(stream pb.Commit_TreeEntryServer, revision, path, baseName string, limit int64) catfile.Handler {
+ return func(stdin io.Writer, stdout *bufio.Reader) error {
+ var treeEntry *pb.TreeEntry
-func (s *server) TreeEntry(in *pb.TreeEntryRequest, stream pb.CommitService_TreeEntryServer) error {
- if err := validateRequest(in); err != nil {
- return grpc.Errorf(codes.InvalidArgument, "TreeEntry: %v", err)
- }
-
- repoPath, err := helper.GetRepoPath(in.Repository)
- if err != nil {
- return err
- }
-
- stdinReader, stdinWriter := io.Pipe()
- cmdArgs := []string{
- "--git-dir", repoPath,
- "cat-file",
- "--batch",
- }
- cmd, err := helper.NewCommand(exec.Command(helper.GitPath(), cmdArgs...), stdinReader, nil, nil)
- if err != nil {
- return grpc.Errorf(codes.Internal, "TreeEntry: cmd: %v", err)
- }
- defer cmd.Kill()
- defer stdinWriter.Close()
- defer stdinReader.Close()
-
- dirName := path.Dir(string(in.GetPath()))
- if dirName == "." {
- dirName = ""
- }
- baseName := path.Base(string(in.GetPath()))
+ entries, err := treeEntries(revision, path, stdin, stdout)
+ if err != nil {
+ return err
+ }
- treeObject := fmt.Sprintf("%s^{tree}:%s\n", in.GetRevision(), dirName)
- if _, err := stdinWriter.Write([]byte(treeObject)); err != nil {
- return grpc.Errorf(codes.Internal, "TreeEntry: stdin write: %v", err)
- }
+ for _, entry := range entries {
+ if string(entry.Path) == baseName {
+ treeEntry = entry
+ break
+ }
+ }
- stdout := bufio.NewReader(cmd)
+ if treeEntry == nil || len(treeEntry.Oid) == 0 {
+ // Not found, send nothing
+ return nil
+ }
- treeInfo, err := catfile.ParseObjectInfo(stdout)
- if err != nil {
- return grpc.Errorf(codes.Internal, "TreeEntry: %v", err)
- }
- if treeInfo.Oid == "" {
- return sendNotFoundResponse(stream)
- }
+ if treeEntry.Type == pb.TreeEntry_COMMIT {
+ response := &pb.TreeEntryResponse{
+ Type: pb.TreeEntryResponse_COMMIT,
+ Mode: treeEntry.Mode,
+ Oid: treeEntry.Oid,
+ }
+ if err := stream.Send(response); err != nil {
+ return grpc.Errorf(codes.Unavailable, "TreeEntry: send: %v", err)
+ }
+
+ return nil
+ }
- treeEntryInfo, err := extractEntryInfoFromTreeData(stdout, treeInfo.Size, baseName)
- if err != nil {
- return grpc.Errorf(codes.Internal, "TreeEntry: %v", err)
- }
- if treeEntryInfo.oid == "" {
- return sendNotFoundResponse(stream)
- }
+ stdin.Write([]byte(treeEntry.Oid + "\n"))
- if treeEntryInfo.objectType == "commit" {
- response := &pb.TreeEntryResponse{
- Type: pb.TreeEntryResponse_COMMIT,
- Mode: treeEntryInfo.mode,
- Oid: treeEntryInfo.oid,
- }
- if err := stream.Send(response); err != nil {
- return grpc.Errorf(codes.Unavailable, "TreeEntry: send: %v", err)
+ objectInfo, err := catfile.ParseObjectInfo(stdout)
+ if err != nil {
+ return grpc.Errorf(codes.Internal, "TreeEntry: %v", err)
}
- return nil
- }
-
- stdinWriter.Write([]byte(treeEntryInfo.oid))
- stdinWriter.Close()
+ if strings.ToLower(treeEntry.Type.String()) != objectInfo.Type {
+ return grpc.Errorf(
+ codes.Internal,
+ "TreeEntry: mismatched object type: tree-oid=%s object-oid=%s entry-type=%s object-type=%s",
+ treeEntry.Oid, objectInfo.Oid, treeEntry.Type.String, objectInfo.Type,
+ )
+ }
- objectInfo, err := catfile.ParseObjectInfo(stdout)
- if err != nil {
- return grpc.Errorf(codes.Internal, "TreeEntry: %v", err)
- }
+ if objectInfo.Type == "tree" {
+ response := &pb.TreeEntryResponse{
+ Type: pb.TreeEntryResponse_TREE,
+ Oid: objectInfo.Oid,
+ Size: objectInfo.Size,
+ Mode: treeEntry.Mode,
+ }
+ return helper.DecorateError(codes.Unavailable, stream.Send(response))
+ }
- if treeEntryInfo.objectType != objectInfo.Type {
- return grpc.Errorf(
- codes.Internal,
- "TreeEntry: mismatched object type: tree-oid=%s object-oid=%s entry-type=%s object-type=%s",
- treeEntryInfo.oid, objectInfo.Oid, treeEntryInfo.objectType, objectInfo.Type,
- )
- }
+ dataLength := objectInfo.Size
+ if limit > 0 && dataLength > limit {
+ dataLength = limit
+ }
- if objectInfo.Type == "tree" {
response := &pb.TreeEntryResponse{
- Type: pb.TreeEntryResponse_TREE,
+ Type: pb.TreeEntryResponse_BLOB,
Oid: objectInfo.Oid,
Size: objectInfo.Size,
- Mode: treeEntryInfo.mode,
+ Mode: treeEntry.Mode,
+ }
+ if dataLength == 0 {
+ return helper.DecorateError(codes.Unavailable, stream.Send(response))
}
- return helper.DecorateError(codes.Unavailable, stream.Send(response))
- }
- dataLength := objectInfo.Size
- if in.Limit > 0 && dataLength > in.Limit {
- dataLength = in.Limit
- }
+ sw := streamio.NewWriter(func(p []byte) error {
+ response.Data = p
- response := &pb.TreeEntryResponse{
- Type: pb.TreeEntryResponse_BLOB,
- Oid: objectInfo.Oid,
- Size: objectInfo.Size,
- Mode: treeEntryInfo.mode,
- }
- if dataLength == 0 {
- return helper.DecorateError(codes.Unavailable, stream.Send(response))
- }
+ if err := stream.Send(response); err != nil {
+ return grpc.Errorf(codes.Unavailable, "TreeEntry: send: %v", err)
+ }
- sw := streamio.NewWriter(func(p []byte) error {
- response.Data = p
+ // Use a new response so we don't send other fields (Size, ...) over and over
+ response = &pb.TreeEntryResponse{}
- if err := stream.Send(response); err != nil {
- return grpc.Errorf(codes.Unavailable, "TreeEntry: send: %v", err)
+ return nil
+ })
+
+ n, err := io.Copy(sw, io.LimitReader(stdout, dataLength))
+ if n < dataLength && err == nil {
+ return grpc.Errorf(codes.Internal, "TreeEntry: Incomplete copy")
}
- // Use a new response so we don't send other fields (Size, ...) over and over
- response = &pb.TreeEntryResponse{}
+ return err
+ }
+}
- return nil
- })
+func (s *server) TreeEntry(in *pb.TreeEntryRequest, stream pb.CommitService_TreeEntryServer) error {
+ if err := validateRequest(in); err != nil {
+ return grpc.Errorf(codes.InvalidArgument, "TreeEntry: %v", err)
+ }
- n, err := io.Copy(sw, io.LimitReader(stdout, dataLength))
- if n < dataLength && err == nil {
- return grpc.Errorf(codes.Internal, "TreeEntry: Incomplete copy")
+ repoPath, err := helper.GetRepoPath(in.Repository)
+ if err != nil {
+ return err
}
- return err
+ requestPath := string(in.GetPath())
+ handler := treeEntryHandler(stream, string(in.GetRevision()), path.Dir(requestPath), path.Base(requestPath), in.GetLimit())
+ return catfile.CatFile(repoPath, handler)
}
func validateRequest(in *pb.TreeEntryRequest) error {
@@ -163,76 +139,3 @@ func validateRequest(in *pb.TreeEntryRequest) error {
return nil
}
-
-func extractEntryInfoFromTreeData(stdout *bufio.Reader, treeSize int64, baseName string) (*entryInfo, error) {
- var modeBytes, path []byte
- var objectType string
- var err error
-
- oidBytes := make([]byte, 20)
- entryFound := false
- bytesLeft := treeSize
-
- for bytesLeft > 0 {
- modeBytes, err = stdout.ReadBytes(' ')
- if err != nil || len(modeBytes) <= 1 {
- return nil, fmt.Errorf("read entry mode: %v", err)
- }
- bytesLeft -= int64(len(modeBytes))
- modeBytes = modeBytes[:len(modeBytes)-1]
-
- path, err = stdout.ReadBytes('\x00')
- if err != nil || len(path) <= 1 {
- return nil, fmt.Errorf("read entry path: %v", err)
- }
- bytesLeft -= int64(len(path))
- path = path[:len(path)-1]
-
- if n, _ := stdout.Read(oidBytes); n != 20 {
- return nil, fmt.Errorf("read entry oid: %v", err)
- }
-
- bytesLeft -= int64(len(oidBytes))
-
- if string(path) == baseName {
- entryFound = true
- break
- }
- }
-
- // Extra byte for a linefeed at the end
- if _, err := stdout.Discard(int(bytesLeft + 1)); err != nil {
- return nil, fmt.Errorf("stdout discard: %v", err)
- }
-
- if !entryFound {
- return &entryInfo{}, nil
- }
-
- mode, err := strconv.ParseInt(string(modeBytes), 8, 32)
- if err != nil {
- return nil, fmt.Errorf("parse mode: %v", err)
- }
-
- oid := fmt.Sprintf("%02x", oidBytes)
-
- // Based on https://github.com/git/git/blob/v2.13.1/builtin/ls-tree.c#L67-L87
- switch mode & 0xf000 {
- case 0160000:
- objectType = "commit"
- case 040000:
- objectType = "tree"
- default:
- objectType = "blob"
- }
-
- return &entryInfo{
- objectType: objectType,
- mode: int32(mode),
- oid: oid,
- }, nil
-}
-
-func sendNotFoundResponse(stream pb.Commit_TreeEntryServer) error {
- return helper.DecorateError(codes.Unavailable, stream.Send(&pb.TreeEntryResponse{}))
-}
diff --git a/internal/service/commit/util.go b/internal/service/commit/util.go
index 5b0ef9333..9897afc91 100644
--- a/internal/service/commit/util.go
+++ b/internal/service/commit/util.go
@@ -2,6 +2,8 @@ package commit
import (
"bytes"
+ "fmt"
+ "strconv"
"strings"
pb "gitlab.com/gitlab-org/gitaly-proto/go"
@@ -34,3 +36,33 @@ func newCommitsBetweenWriter(stream pb.CommitService_CommitsBetweenServer) lines
return stream.Send(&pb.CommitsBetweenResponse{Commits: commits})
}
}
+
+func newTreeEntry(commitOid, rootOid string, path, oidBytes, modeBytes []byte) (*pb.TreeEntry, error) {
+ var objectType pb.TreeEntry_EntryType
+
+ mode, err := strconv.ParseInt(string(modeBytes), 8, 32)
+ if err != nil {
+ return nil, fmt.Errorf("parse mode: %v", err)
+ }
+
+ oid := fmt.Sprintf("%02x", oidBytes)
+
+ // Based on https://github.com/git/git/blob/v2.13.1/builtin/ls-tree.c#L67-L87
+ switch mode & 0xf000 {
+ case 0160000:
+ objectType = pb.TreeEntry_COMMIT
+ case 040000:
+ objectType = pb.TreeEntry_TREE
+ default:
+ objectType = pb.TreeEntry_BLOB
+ }
+
+ return &pb.TreeEntry{
+ CommitOid: commitOid,
+ RootOid: rootOid,
+ Oid: oid,
+ Path: path,
+ Type: objectType,
+ Mode: int32(mode),
+ }, nil
+}