diff options
author | Alejandro Rodríguez <alejorro70@gmail.com> | 2017-07-14 19:01:12 +0300 |
---|---|---|
committer | Alejandro Rodríguez <alejorro70@gmail.com> | 2017-07-14 19:01:12 +0300 |
commit | d2978e6eff30332b11b5ec47e0f01915dd3eeb52 (patch) | |
tree | 79208e9498bba08bf145bb245183194e1f51cf8e | |
parent | e9a16b20f9b190012c4c7895c17fd54ac36b1e99 (diff) | |
parent | a8f32a8111ac8b8aa7776e55b9a40d8190494fe2 (diff) |
Merge branch '359-commitservice-gettreeentries-server-implementation' into 'master'
CommitService::GetTreeEntries server implementation
Closes #359
See merge request !208
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | internal/git/catfile/catfile.go | 29 | ||||
-rw-r--r-- | internal/service/commit/server.go | 4 | ||||
-rw-r--r-- | internal/service/commit/testhelper_test.go | 6 | ||||
-rw-r--r-- | internal/service/commit/tree_entries.go | 69 | ||||
-rw-r--r-- | internal/service/commit/tree_entries_helper.go | 102 | ||||
-rw-r--r-- | internal/service/commit/tree_entries_test.go | 281 | ||||
-rw-r--r-- | internal/service/commit/tree_entry.go | 269 | ||||
-rw-r--r-- | internal/service/commit/util.go | 32 |
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 +} |