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:
authorPatrick Steinhardt <psteinhardt@gitlab.com>2023-05-11 09:17:16 +0300
committerPatrick Steinhardt <psteinhardt@gitlab.com>2023-05-11 09:18:23 +0300
commit9a371a3711f2a46f9d942a6c8dc170afdc8d4a2b (patch)
tree957e094fa96ac191fd60e6c6deccbf26a75c17c9
parentff96c9cf394813bb54b9d20ccffd65f99ef18e48 (diff)
localrepo: Revert refactorings of tree entries helpers
This reverts 319f05939 (Merge branch 'jc/tree-improvements' into 'master', 2023-05-09), which has introduced a bunch of refactorings for our tree entries helpers in the localrepo package. These refactorings caused failures in Rails: 1) API::Submodules PUT /projects/:id/repository/submodule/:submodule when authenticated as a developer returns the commit Failure/Error: expect(response).to have_gitlab_http_status(:ok) expected the response to have status code :ok but it was 400. The response was: {"message":"13:submodule subcommand: write tree: writing tree: exit status 128."} # ./spec/requests/api/submodules_spec.rb:72:in `block (4 levels) in <main>' # ./spec/spec_helper.rb:423:in `block (3 levels) in <top (required)>' # ./spec/support/sidekiq_middleware.rb:18:in `with_sidekiq_server_middleware' # ./spec/spec_helper.rb:415:in `block (2 levels) in <top (required)>' # ./spec/spec_helper.rb:411:in `block (3 levels) in <top (required)>' # ./lib/gitlab/application_context.rb:61:in `with_raw_context' # ./spec/spec_helper.rb:411:in `block (2 levels) in <top (required)>' # ./spec/spec_helper.rb:242:in `block (2 levels) in <top (required)>' # ./spec/support/system_exit_detected.rb:7:in `block (2 levels) in <main>' # ./spec/support/fast_quarantine.rb:39:in `block (2 levels) in <main>' # ./spec/support/database/prevent_cross_joins.rb:108:in `block (3 levels) in <main>' # ./spec/support/database/prevent_cross_joins.rb:62:in `with_cross_joins_prevented' # ./spec/support/database/prevent_cross_joins.rb:108:in `block (2 levels) in <main>' 2) API::Submodules PUT /projects/:id/repository/submodule/:submodule when authenticated as a developer when the submodule name is urlencoded returns the commit Failure/Error: expect(response).to have_gitlab_http_status(:ok) expected the response to have status code :ok but it was 400. The response was: {"message":"13:submodule subcommand: write tree: writing tree: exit status 128."} # ./spec/requests/api/submodules_spec.rb:92:in `block (5 levels) in <main>' # ./spec/spec_helper.rb:423:in `block (3 levels) in <top (required)>' # ./spec/support/sidekiq_middleware.rb:18:in `with_sidekiq_server_middleware' # ./spec/spec_helper.rb:415:in `block (2 levels) in <top (required)>' # ./spec/spec_helper.rb:411:in `block (3 levels) in <top (required)>' # ./lib/gitlab/application_context.rb:61:in `with_raw_context' # ./spec/spec_helper.rb:411:in `block (2 levels) in <top (required)>' # ./spec/spec_helper.rb:242:in `block (2 levels) in <top (required)>' # ./spec/support/system_exit_detected.rb:7:in `block (2 levels) in <main>' # ./spec/support/fast_quarantine.rb:39:in `block (2 levels) in <main>' # ./spec/support/database/prevent_cross_joins.rb:108:in `block (3 levels) in <main>' # ./spec/support/database/prevent_cross_joins.rb:62:in `with_cross_joins_prevented' # ./spec/support/database/prevent_cross_joins.rb:108:in `block (2 levels) in <main>'
-rw-r--r--internal/git/localrepo/commit_test.go57
-rw-r--r--internal/git/localrepo/list_entries.go98
-rw-r--r--internal/git/localrepo/list_entries_test.go159
-rw-r--r--internal/git/localrepo/objects.go19
-rw-r--r--internal/git/localrepo/objects_test.go43
-rw-r--r--internal/git/localrepo/tree.go380
-rw-r--r--internal/git/localrepo/tree_test.go810
-rw-r--r--internal/git/version.go8
-rw-r--r--internal/gitaly/service/commit/tree_entries.go32
-rw-r--r--internal/gitaly/service/operations/submodules.go54
10 files changed, 365 insertions, 1295 deletions
diff --git a/internal/git/localrepo/commit_test.go b/internal/git/localrepo/commit_test.go
index 9fe1a19dc..a95e41273 100644
--- a/internal/git/localrepo/commit_test.go
+++ b/internal/git/localrepo/commit_test.go
@@ -32,23 +32,13 @@ func TestWriteCommit(t *testing.T) {
require.NoError(t, err)
treeEntryA := TreeEntry{Path: "file", Mode: "100644", OID: blobID}
- treeA := &TreeEntry{
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{&treeEntryA},
- }
- require.NoError(t, treeA.Write(
- ctx,
- repo))
+ treeA, err := repo.WriteTree(ctx, []TreeEntry{treeEntryA})
+ require.NoError(t, err)
- treeB := &TreeEntry{
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{
- {Path: "file", Mode: "100644", OID: changedBlobID},
- },
- }
- require.NoError(t, treeB.Write(ctx, repo))
+ treeB, err := repo.WriteTree(ctx, []TreeEntry{
+ {Path: "file", Mode: "100644", OID: changedBlobID},
+ })
+ require.NoError(t, err)
commitA, err := repo.WriteCommit(
ctx,
WriteCommitConfig{
@@ -57,7 +47,7 @@ func TestWriteCommit(t *testing.T) {
CommitterName: "Tazmanian Devil",
CommitterEmail: "taz@devils.org",
Message: "I ❤️ Tazmania",
- TreeID: treeA.OID,
+ TreeID: treeA,
},
)
require.NoError(t, err)
@@ -69,7 +59,7 @@ func TestWriteCommit(t *testing.T) {
CommitterName: "Daffy Duck",
CommitterEmail: "daffy@ducks.org",
Message: "Big beak",
- TreeID: treeB.OID,
+ TreeID: treeB,
},
)
require.NoError(t, err)
@@ -90,7 +80,7 @@ func TestWriteCommit(t *testing.T) {
{
desc: "with commit message",
cfg: WriteCommitConfig{
- TreeID: treeA.OID,
+ TreeID: treeA,
AuthorName: "Scrooge Mcduck",
AuthorEmail: "chief@ducks.org",
CommitterName: "Mickey Mouse",
@@ -100,7 +90,7 @@ func TestWriteCommit(t *testing.T) {
Message: "my custom message\n\ntrailer\n",
},
expectedCommit: strings.Join([]string{
- "tree " + string(treeA.OID),
+ "tree " + string(treeA),
fmt.Sprintf(
"author Scrooge Mcduck <chief@ducks.org> %d %s",
commitDate.Unix(),
@@ -120,7 +110,7 @@ func TestWriteCommit(t *testing.T) {
{
desc: "with multiple parents",
cfg: WriteCommitConfig{
- TreeID: treeA.OID,
+ TreeID: treeA,
Parents: []git.ObjectID{commitA, commitB},
AuthorName: "Scrooge Mcduck",
AuthorEmail: "chief@ducks.org",
@@ -131,7 +121,7 @@ func TestWriteCommit(t *testing.T) {
Message: "my custom message",
},
expectedCommit: strings.Join([]string{
- "tree " + treeA.OID.String(),
+ "tree " + treeA.String(),
"parent " + commitA.String(),
"parent " + commitB.String(),
fmt.Sprintf(
@@ -151,7 +141,7 @@ func TestWriteCommit(t *testing.T) {
{
desc: "with reference",
cfg: WriteCommitConfig{
- TreeID: treeA.OID,
+ TreeID: treeA,
Parents: []git.ObjectID{commitA, commitB},
AuthorName: "Scrooge Mcduck",
AuthorEmail: "chief@ducks.org",
@@ -163,7 +153,7 @@ func TestWriteCommit(t *testing.T) {
Reference: "refs/heads/foo",
},
expectedCommit: strings.Join([]string{
- "tree " + treeA.OID.String(),
+ "tree " + treeA.String(),
"parent " + commitA.String(),
"parent " + commitB.String(),
fmt.Sprintf(
@@ -213,19 +203,14 @@ func TestWriteCommit_validation(t *testing.T) {
blobID, err := repo.WriteBlob(ctx, "", strings.NewReader("foo"))
require.NoError(t, err)
- tree := &TreeEntry{
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{
- {
- OID: blobID,
- Mode: "100644",
- Path: "file1",
- },
+ treeID, err := repo.WriteTree(ctx, []TreeEntry{
+ {
+ OID: blobID,
+ Mode: "100644",
+ Path: "file1",
},
- }
- require.NoError(t, tree.Write(ctx, repo))
- treeID := tree.OID
+ })
+ require.NoError(t, err)
testCases := []struct {
desc string
diff --git a/internal/git/localrepo/list_entries.go b/internal/git/localrepo/list_entries.go
new file mode 100644
index 000000000..464fbcef6
--- /dev/null
+++ b/internal/git/localrepo/list_entries.go
@@ -0,0 +1,98 @@
+package localrepo
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "gitlab.com/gitlab-org/gitaly/v16/internal/git"
+)
+
+var (
+ // ErrNotExist indicates that the requested tree does not exist, either because the revision
+ // is invalid or because the path is not valid.
+ ErrNotExist = errors.New("invalid object name")
+ // ErrNotTreeish indicates that the requested revision or path does not resolve to a tree
+ // object.
+ ErrNotTreeish = errors.New("not treeish")
+)
+
+// ListEntriesConfig is configuration that can be passed to ListEntries.
+type ListEntriesConfig struct {
+ // Recursive indicates whether the listing shall be recursive or not.
+ Recursive bool
+ // RelativePath is the relative path at which listing of entries should be started.
+ RelativePath string
+}
+
+// ListEntries lists tree entries for the given treeish. By default, this will do a non-recursive
+// listing starting from the root of the given treeish. This behaviour can be changed by passing a
+// config.
+func (repo *Repo) ListEntries(
+ ctx context.Context,
+ treeish git.Revision,
+ cfg *ListEntriesConfig,
+) ([]*TreeEntry, error) {
+ if cfg == nil {
+ cfg = &ListEntriesConfig{}
+ }
+
+ flags := []git.Option{git.Flag{Name: "-z"}}
+ if cfg.Recursive {
+ flags = append(flags,
+ git.Flag{Name: "-r"},
+ git.Flag{Name: "-t"},
+ )
+ }
+
+ relativePath := cfg.RelativePath
+ if relativePath == "." {
+ relativePath = ""
+ }
+
+ var stderr bytes.Buffer
+ cmd, err := repo.Exec(ctx, git.Command{
+ Name: "ls-tree",
+ Args: []string{fmt.Sprintf("%s:%s", treeish, relativePath)},
+ Flags: flags,
+ }, git.WithStderr(&stderr))
+ if err != nil {
+ return nil, fmt.Errorf("spawning git-ls-tree: %w", err)
+ }
+
+ objectHash, err := repo.ObjectHash(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("detecting object hash: %w", err)
+ }
+
+ parser := NewParser(cmd, objectHash)
+ var entries []*TreeEntry
+ for {
+ entry, err := parser.NextEntry()
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+
+ return nil, fmt.Errorf("parsing tree entry: %w", err)
+ }
+
+ entries = append(entries, entry)
+ }
+
+ if err := cmd.Wait(); err != nil {
+ errorMessage := stderr.String()
+ if strings.HasPrefix(errorMessage, "fatal: not a tree object") {
+ return nil, ErrNotTreeish
+ } else if strings.HasPrefix(errorMessage, "fatal: Not a valid object name") {
+ return nil, ErrNotExist
+ }
+
+ return nil, fmt.Errorf("waiting for git-ls-tree: %w, stderr: %q", err, errorMessage)
+ }
+
+ return entries, nil
+}
diff --git a/internal/git/localrepo/list_entries_test.go b/internal/git/localrepo/list_entries_test.go
new file mode 100644
index 000000000..45292f97a
--- /dev/null
+++ b/internal/git/localrepo/list_entries_test.go
@@ -0,0 +1,159 @@
+package localrepo
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/git"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/git/gittest"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper"
+ "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper/testcfg"
+)
+
+func TestListEntries(t *testing.T) {
+ t.Parallel()
+
+ cfg := testcfg.Build(t)
+ ctx := testhelper.Context(t)
+
+ repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{
+ SkipCreationViaService: true,
+ })
+ repo := NewTestRepo(t, cfg, repoProto)
+
+ blobID := gittest.WriteBlob(t, cfg, repoPath, []byte("blob contents"))
+ emptyTreeID := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{})
+ treeWithBlob := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {OID: blobID, Mode: "100644", Path: "nonexecutable"},
+ {OID: blobID, Mode: "100755", Path: "executable"},
+ })
+ treeWithSubtree := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {OID: emptyTreeID, Mode: "040000", Path: "subdir"},
+ })
+ treeWithNestedSubtrees := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {OID: treeWithSubtree, Mode: "040000", Path: "nested-subdir"},
+ })
+ treeWithSubtreeAndBlob := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {OID: treeWithSubtree, Mode: "040000", Path: "subdir"},
+ {OID: blobID, Mode: "100644", Path: "blob"},
+ })
+ treeWithSubtreeContainingBlob := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
+ {OID: treeWithSubtreeAndBlob, Mode: "040000", Path: "subdir"},
+ })
+
+ for _, tc := range []struct {
+ desc string
+ treeish git.Revision
+ cfg *ListEntriesConfig
+ expectedResults []*TreeEntry
+ expectedErr error
+ }{
+ {
+ desc: "empty tree",
+ treeish: emptyTreeID.Revision(),
+ },
+ {
+ desc: "tree with blob",
+ treeish: treeWithBlob.Revision(),
+ expectedResults: []*TreeEntry{
+ {Mode: "100755", Type: Blob, OID: blobID, Path: "executable"},
+ {Mode: "100644", Type: Blob, OID: blobID, Path: "nonexecutable"},
+ },
+ },
+ {
+ desc: "tree with subtree",
+ treeish: treeWithSubtree.Revision(),
+ expectedResults: []*TreeEntry{
+ {Mode: "040000", Type: Tree, OID: emptyTreeID, Path: "subdir"},
+ },
+ },
+ {
+ desc: "nested trees",
+ treeish: treeWithNestedSubtrees.Revision(),
+ expectedResults: []*TreeEntry{
+ {Mode: "040000", Type: Tree, OID: treeWithSubtree, Path: "nested-subdir"},
+ },
+ },
+ {
+ desc: "recursive nested trees",
+ treeish: treeWithNestedSubtrees.Revision(),
+ cfg: &ListEntriesConfig{
+ Recursive: true,
+ },
+ expectedResults: []*TreeEntry{
+ {Mode: "040000", Type: Tree, OID: treeWithSubtree, Path: "nested-subdir"},
+ {Mode: "040000", Type: Tree, OID: emptyTreeID, Path: "nested-subdir/subdir"},
+ },
+ },
+ {
+ desc: "nested subtree",
+ treeish: treeWithNestedSubtrees.Revision(),
+ cfg: &ListEntriesConfig{
+ RelativePath: "nested-subdir",
+ },
+ expectedResults: []*TreeEntry{
+ {Mode: "040000", Type: Tree, OID: emptyTreeID, Path: "subdir"},
+ },
+ },
+ {
+ desc: "nested recursive subtree",
+ treeish: treeWithSubtreeContainingBlob.Revision(),
+ cfg: &ListEntriesConfig{
+ RelativePath: "subdir",
+ Recursive: true,
+ },
+ expectedResults: []*TreeEntry{
+ {Mode: "100644", Type: Blob, OID: blobID, Path: "blob"},
+ {Mode: "040000", Type: Tree, OID: treeWithSubtree, Path: "subdir"},
+ {Mode: "040000", Type: Tree, OID: emptyTreeID, Path: "subdir/subdir"},
+ },
+ },
+ {
+ desc: "recursive nested trees and blobs",
+ treeish: treeWithSubtreeAndBlob.Revision(),
+ cfg: &ListEntriesConfig{
+ Recursive: true,
+ },
+ expectedResults: []*TreeEntry{
+ {Mode: "100644", Type: Blob, OID: blobID, Path: "blob"},
+ {Mode: "040000", Type: Tree, OID: treeWithSubtree, Path: "subdir"},
+ {Mode: "040000", Type: Tree, OID: emptyTreeID, Path: "subdir/subdir"},
+ },
+ },
+ {
+ desc: "listing blob fails",
+ treeish: blobID.Revision(),
+ // We get a NotExist error here because it's invalid to suffix an object ID
+ // which resolves to a blob with a colon (":") given that it's not possible
+ // to resolve a subpath.
+ expectedErr: ErrNotExist,
+ },
+ {
+ desc: "valid revision with invalid path",
+ treeish: treeWithSubtree.Revision(),
+ cfg: &ListEntriesConfig{
+ RelativePath: "does-not-exist",
+ },
+ expectedErr: ErrNotExist,
+ },
+ {
+ desc: "valid revision with path pointing to blob",
+ treeish: treeWithSubtreeAndBlob.Revision(),
+ cfg: &ListEntriesConfig{
+ RelativePath: "blob",
+ },
+ expectedErr: ErrNotTreeish,
+ },
+ {
+ desc: "listing nonexistent object fails",
+ treeish: "does-not-exist",
+ expectedErr: ErrNotExist,
+ },
+ } {
+ t.Run(tc.desc, func(t *testing.T) {
+ results, err := repo.ListEntries(ctx, tc.treeish, tc.cfg)
+ require.Equal(t, tc.expectedErr, err)
+ require.Equal(t, tc.expectedResults, results)
+ })
+ }
+}
diff --git a/internal/git/localrepo/objects.go b/internal/git/localrepo/objects.go
index 53d41d070..7c44bb17c 100644
--- a/internal/git/localrepo/objects.go
+++ b/internal/git/localrepo/objects.go
@@ -185,25 +185,6 @@ type InvalidObjectError string
func (err InvalidObjectError) Error() string { return fmt.Sprintf("invalid object %q", string(err)) }
-// ReadObjectInfo attempts to read the object info based on a revision.
-func (repo *Repo) ReadObjectInfo(ctx context.Context, rev git.Revision) (*catfile.ObjectInfo, error) {
- infoReader, cleanup, err := repo.catfileCache.ObjectInfoReader(ctx, repo)
- if err != nil {
- return nil, fmt.Errorf("getting object info reader: %w", err)
- }
- defer cleanup()
-
- objectInfo, err := infoReader.Info(ctx, rev)
- if err != nil {
- if catfile.IsNotFound(err) {
- return nil, InvalidObjectError(rev)
- }
- return nil, fmt.Errorf("getting object info %w", err)
- }
-
- return objectInfo, nil
-}
-
// ReadObject reads an object from the repository's object database. InvalidObjectError
// is returned if the oid does not refer to a valid object.
func (repo *Repo) ReadObject(ctx context.Context, oid git.ObjectID) ([]byte, error) {
diff --git a/internal/git/localrepo/objects_test.go b/internal/git/localrepo/objects_test.go
index 216bd47ca..258adde45 100644
--- a/internal/git/localrepo/objects_test.go
+++ b/internal/git/localrepo/objects_test.go
@@ -280,49 +280,6 @@ func testRepoReadObject(t *testing.T, ctx context.Context) {
}
}
-func TestRepoReadObjectInfo(t *testing.T) {
- t.Parallel()
-
- ctx := testhelper.Context(t)
- cfg, repo, repoPath := setupRepo(t)
- blobID := gittest.WriteBlob(t, cfg, repoPath, []byte("content"))
- objectHash, err := repo.ObjectHash(ctx)
- require.NoError(t, err)
-
- for _, tc := range []struct {
- desc string
- oid git.ObjectID
- content string
- expectedErr error
- expectedObjectInfo catfile.ObjectInfo
- }{
- {
- desc: "missing object",
- oid: git.ObjectID("abcdefg"),
- expectedErr: InvalidObjectError("abcdefg"),
- },
- {
- desc: "valid object",
- oid: blobID,
- content: "content",
- expectedObjectInfo: catfile.ObjectInfo{
- Oid: blobID,
- Type: "blob",
- Size: 7,
- Format: objectHash.Format,
- },
- },
- } {
- t.Run(tc.desc, func(t *testing.T) {
- info, err := repo.ReadObjectInfo(ctx, git.Revision(tc.oid))
- require.Equal(t, tc.expectedErr, err)
- if tc.expectedErr == nil {
- require.Equal(t, tc.expectedObjectInfo, *info)
- }
- })
- }
-}
-
func TestRepo_ReadObject_catfileCount(t *testing.T) {
t.Parallel()
diff --git a/internal/git/localrepo/tree.go b/internal/git/localrepo/tree.go
index 49ffa910a..3582cadfa 100644
--- a/internal/git/localrepo/tree.go
+++ b/internal/git/localrepo/tree.go
@@ -3,16 +3,10 @@ package localrepo
import (
"bytes"
"context"
- "errors"
"fmt"
- "io"
- "path/filepath"
- "sort"
- "strings"
"gitlab.com/gitlab-org/gitaly/v16/internal/git"
"gitlab.com/gitlab-org/gitaly/v16/internal/helper/text"
- "gitlab.com/gitlab-org/gitaly/v16/internal/structerr"
)
// ObjectType is an Enum for the type of object of
@@ -43,33 +37,6 @@ func (e Entries) Less(i, j int) bool {
return e[i].Type < e[j].Type
}
-// TreeEntriesByPath allows a slice of *TreeEntry to be sorted by Path
-type TreeEntriesByPath []*TreeEntry
-
-func (b TreeEntriesByPath) Len() int {
- return len(b)
-}
-
-func (b TreeEntriesByPath) Swap(i, j int) {
- b[i], b[j] = b[j], b[i]
-}
-
-func (b TreeEntriesByPath) Less(i, j int) bool {
- iPath, jPath := b[i].Path, b[j].Path
-
- // git has an edge case for subtrees where they are always appended with
- // a '/'. See https://github.com/git/git/blob/v2.40.0/read-cache.c#L491
- if b[i].Type == Tree {
- iPath += "/"
- }
-
- if b[j].Type == Tree {
- jPath += "/"
- }
-
- return iPath < jPath
-}
-
// ToEnum translates a string representation of the object type into an
// ObjectType enum.
func ToEnum(s string) ObjectType {
@@ -85,6 +52,19 @@ func ToEnum(s string) ObjectType {
}
}
+func fromEnum(t ObjectType) string {
+ switch t {
+ case Tree:
+ return "tree"
+ case Blob:
+ return "blob"
+ case Submodule:
+ return "commit"
+ default:
+ return "unknown"
+ }
+}
+
// TreeEntry represents an entry of a git tree object.
type TreeEntry struct {
// OID is the object ID the tree entry refers to.
@@ -95,8 +75,6 @@ type TreeEntry struct {
Path string
// Type is the type of the tree entry.
Type ObjectType
- // Entries is a slice of this tree's entries.
- Entries []*TreeEntry
}
// IsBlob returns whether or not the TreeEntry is a blob.
@@ -104,268 +82,52 @@ func (t *TreeEntry) IsBlob() bool {
return t.Type == Blob
}
-var (
- // ErrTreeNotExist indicates that the requested tree does not exist, either because the revision
- // is invalid or because the path is not valid.
- ErrTreeNotExist = errors.New("invalid object name")
- // ErrNotTreeish indicates that the requested revision or path does not resolve to a tree
- // object.
- ErrNotTreeish = errors.New("not treeish")
-)
-
-// listEntries lists tree entries for the given treeish.
-func (repo *Repo) listEntries(
- ctx context.Context,
- treeish git.Revision,
- relativePath string,
- recursive bool,
-) ([]*TreeEntry, error) {
- flags := []git.Option{git.Flag{Name: "-z"}}
- if recursive {
- flags = append(flags,
- git.Flag{Name: "-r"},
- git.Flag{Name: "-t"},
- )
- }
-
- if relativePath == "." {
- relativePath = ""
- }
-
- var stderr bytes.Buffer
- cmd, err := repo.Exec(ctx, git.Command{
- Name: "ls-tree",
- Args: []string{fmt.Sprintf("%s:%s", treeish, relativePath)},
- Flags: flags,
- }, git.WithStderr(&stderr))
- if err != nil {
- return nil, fmt.Errorf("spawning git-ls-tree: %w", err)
- }
-
- objectHash, err := repo.ObjectHash(ctx)
- if err != nil {
- return nil, fmt.Errorf("detecting object hash: %w", err)
- }
-
- parser := NewParser(cmd, objectHash)
- var entries []*TreeEntry
- for {
- entry, err := parser.NextEntry()
- if err != nil {
- if errors.Is(err, io.EOF) {
- break
- }
-
- return nil, fmt.Errorf("parsing tree entry: %w", err)
- }
-
- entries = append(entries, entry)
- }
-
- if err := cmd.Wait(); err != nil {
- errorMessage := stderr.String()
- if strings.HasPrefix(errorMessage, "fatal: not a tree object") {
- return nil, ErrNotTreeish
- } else if strings.HasPrefix(errorMessage, "fatal: Not a valid object name") {
- return nil, ErrTreeNotExist
- }
-
- return nil, structerr.New("waiting for git-ls-tree: %w", err).WithMetadata("stderr", errorMessage)
- }
-
- return entries, nil
-}
-
-// depthByPath must be called only on cleaned paths
-func depthByPath(path string) int {
- return strings.Count(path, "/") + 1
-}
-
-// treeStack is a stack data structure used by walk() to do a breadth-first walk
-// of the outputof ls-tree -r.
-type treeStack []*TreeEntry
-
-func (t treeStack) peek() *TreeEntry {
- if len(t) == 0 {
- return nil
- }
-
- return t[len(t)-1]
-}
-
-func (t *treeStack) push(e *TreeEntry) {
- *t = append(*t, e)
-}
-
-func (t *treeStack) pop() *TreeEntry {
- e := t.peek()
- if e == nil {
- return nil
- }
-
- *t = (*t)[:len(*t)-1]
-
- return e
-}
-
-// populate scans through the output of ls-tree -r, and constructs a TreeEntry
-// object.
-func (t *TreeEntry) populate(
- ctx context.Context,
- repo *Repo,
-) error {
- if t.OID == "" {
- return errors.New("oid is empty")
- }
-
- t.Entries = nil
-
- entries, err := repo.listEntries(
- ctx,
- git.Revision(t.OID),
- "",
- true,
- )
- if err != nil {
- return err
- }
-
- stack := treeStack{t}
-
- // The outpout of ls-tree -r is the following:
- // a1
- // dir1/file2
- // dir2/file3
- // f2
- // f3
- // Whenever we see a tree, push it onto the stack since we will need to
- // start adding to that tree's entries.
- // When we encounter an entry that has a lower depth than the previous
- // entry, we know that we need to pop the tree entry off to get back to the
- // parent tree.
- for _, entry := range entries {
- if levelsToPop := len(stack) - depthByPath(entry.Path); levelsToPop > 0 {
- for i := 0; i < levelsToPop; i++ {
- stack.pop()
- }
- }
-
- entry.Path = filepath.Base(entry.Path)
- stack.peek().Entries = append(
- stack.peek().Entries,
- entry,
- )
-
- if entry.Type == Tree {
- stack.push(entry)
- }
- }
-
- if err != nil {
- return fmt.Errorf("listing entries: %w", err)
- }
-
- return nil
-}
-
-func (t *TreeEntry) walk(dirPath string, callback func(path string, entry *TreeEntry) error) error {
- nextDirPath := filepath.Join(dirPath, t.Path)
- if err := callback(dirPath, t); err != nil {
- return err
- }
-
- for _, entry := range t.Entries {
- if err := entry.walk(nextDirPath, callback); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-// Walk will walk the whole tree structure and call callback on every entry of
-// the tree in a depth-first like fashion.
-func (t *TreeEntry) Walk(callback func(path string, entry *TreeEntry) error) error {
- for _, e := range t.Entries {
- if err := e.walk(t.Path, callback); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-// Write does a depth first walk, writing new trees when the OID of the
-// TreeEntry is empty.
-func (t *TreeEntry) Write(
- ctx context.Context,
- repo *Repo,
-) error {
- var err error
-
- if t.OID != "" {
- return nil
- }
-
- for _, e := range t.Entries {
- if e.Type == Tree && e.OID == "" {
- if err := e.Write(ctx, repo); err != nil {
- return err
- }
- }
- }
-
- treeOID, err := repo.writeEntries(ctx, t.Entries)
- if err != nil {
- return fmt.Errorf("writing tree: %w", err)
- }
-
- t.OID = treeOID
- return nil
-}
-
-// writeTreeEntries writes a new tree object from a slice of entries. This function does not verify whether OIDs
+// WriteTree writes a new tree object to the given path. This function does not verify whether OIDs
// referred to by tree entries actually exist in the repository.
-func (repo *Repo) writeEntries(ctx context.Context, entries []*TreeEntry) (git.ObjectID, error) {
+func (repo *Repo) WriteTree(ctx context.Context, entries []TreeEntry) (git.ObjectID, error) {
var tree bytes.Buffer
-
- sort.Stable(TreeEntriesByPath(entries))
-
for _, entry := range entries {
- mode := strings.TrimPrefix(entry.Mode, "0")
- formattedEntry := fmt.Sprintf("%s %s\000", mode, entry.Path)
-
- oidBytes, err := entry.OID.Bytes()
- if err != nil {
- return "", err
+ entryType := entry.Type
+
+ if entryType == Unknown {
+ switch entry.Mode {
+ case "100644":
+ fallthrough
+ case "100755":
+ fallthrough
+ case "120000":
+ entryType = Blob
+ case "040000":
+ entryType = Tree
+ case "160000":
+ entryType = Submodule
+ }
}
- if _, err := tree.WriteString(formattedEntry); err != nil {
- return "", err
- }
+ oid := entry.OID
- if _, err := tree.Write(oidBytes); err != nil {
+ formattedEntry := fmt.Sprintf("%s %s %s\t%s\000", entry.Mode, fromEnum(entryType), oid.String(), entry.Path)
+ if _, err := tree.WriteString(formattedEntry); err != nil {
return "", err
}
}
options := []git.Option{
- git.ValueFlag{Name: "-t", Value: "tree"},
- git.Flag{Name: "-w"},
- git.Flag{Name: "--stdin"},
+ git.Flag{Name: "-z"},
+ git.Flag{Name: "--missing"},
}
var stdout, stderr bytes.Buffer
if err := repo.ExecAndWait(ctx,
git.Command{
- Name: "hash-object",
+ Name: "mktree",
Flags: options,
},
git.WithStdout(&stdout),
git.WithStderr(&stderr),
git.WithStdin(&tree),
); err != nil {
- return "", structerr.New("%w", err).WithMetadata("stderr", stderr.String())
+ return "", err
}
objectHash, err := repo.ObjectHash(ctx)
@@ -380,71 +142,3 @@ func (repo *Repo) writeEntries(ctx context.Context, entries []*TreeEntry) (git.O
return treeOID, nil
}
-
-// readTreeConfig is configuration that can be passed to ReadTree.
-type readTreeConfig struct {
- recursive bool
- // relativePath is the relative path at which listing of entries should be started.
- relativePath string
-}
-
-// ReadTreeOption is an option that modifies the behavior of ReadTree()
-type ReadTreeOption func(c *readTreeConfig)
-
-// WithRecursive returns all entries recursively, but "flattened" in the sense
-// that all subtrees and their entries get returned as Entries, each entry with
-// its full path.
-func WithRecursive() ReadTreeOption {
- return func(c *readTreeConfig) {
- c.recursive = true
- }
-}
-
-// WithRelativePath will cause ReadTree to return a tree at the relative path.
-func WithRelativePath(relativePath string) ReadTreeOption {
- return func(c *readTreeConfig) {
- c.relativePath = relativePath
- }
-}
-
-// ReadTree gets a tree object with all of the direct children populated.
-// Walk can be called to populate every level of the tree.
-func (repo *Repo) ReadTree(ctx context.Context, treeish git.Revision, options ...ReadTreeOption) (*TreeEntry, error) {
- var c readTreeConfig
-
- for _, opt := range options {
- opt(&c)
- }
-
- if c.relativePath == "." {
- c.relativePath = ""
- }
-
- treeOID, err := repo.ResolveRevision(ctx, git.Revision(string(treeish)+":"+c.relativePath))
- if err != nil {
- return nil, fmt.Errorf("getting revision: %w", err)
- }
-
- rootEntry := &TreeEntry{
- OID: treeOID,
- Type: Tree,
- Mode: "040000",
- }
-
- if c.recursive {
- if err := rootEntry.populate(ctx, repo); err != nil {
- return nil, err
- }
- } else {
- if rootEntry.Entries, err = repo.listEntries(
- ctx,
- treeish,
- c.relativePath,
- c.recursive,
- ); err != nil {
- return nil, fmt.Errorf("listEntries: %w", err)
- }
- }
-
- return rootEntry, nil
-}
diff --git a/internal/git/localrepo/tree_test.go b/internal/git/localrepo/tree_test.go
index e4794f996..c2bb1f897 100644
--- a/internal/git/localrepo/tree_test.go
+++ b/internal/git/localrepo/tree_test.go
@@ -2,29 +2,21 @@ package localrepo
import (
"bytes"
- "context"
"os"
"path/filepath"
- "sort"
- "strings"
"testing"
"github.com/stretchr/testify/require"
- "gitlab.com/gitlab-org/gitaly/v16/internal/git"
"gitlab.com/gitlab-org/gitaly/v16/internal/git/gittest"
"gitlab.com/gitlab-org/gitaly/v16/internal/helper/text"
- "gitlab.com/gitlab-org/gitaly/v16/internal/structerr"
"gitlab.com/gitlab-org/gitaly/v16/internal/testhelper"
"gitlab.com/gitlab-org/gitaly/v16/internal/testhelper/testcfg"
)
-func TestTreeEntry_Write(t *testing.T) {
+func TestWriteTree(t *testing.T) {
cfg := testcfg.Build(t)
ctx := testhelper.Context(t)
- gitVersion, err := gittest.NewCommandFactory(t, cfg).GitVersion(ctx)
- require.NoError(t, err)
-
repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{
SkipCreationViaService: true,
})
@@ -36,19 +28,14 @@ func TestTreeEntry_Write(t *testing.T) {
blobID, err := repo.WriteBlob(ctx, "file", bytes.NewBufferString("foobar\n"))
require.NoError(t, err)
- tree := &TreeEntry{
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{
- {
- OID: blobID,
- Mode: "100644",
- Path: "file",
- },
+ treeID, err := repo.WriteTree(ctx, []TreeEntry{
+ {
+ OID: blobID,
+ Mode: "100644",
+ Path: "file",
},
- }
- require.NoError(t, tree.Write(ctx, repo))
- treeID := tree.OID
+ })
+ require.NoError(t, err)
nonExistentBlobID, err := repo.WriteBlob(ctx, "file1", bytes.NewBufferString("content"))
require.NoError(t, err)
@@ -57,14 +44,13 @@ func TestTreeEntry_Write(t *testing.T) {
require.NoError(t, os.Remove(nonExistentBlobPath))
for _, tc := range []struct {
- desc string
- entries []*TreeEntry
- expectedEntries []TreeEntry
- expectedErrString string
+ desc string
+ entries []TreeEntry
+ expectedEntries []TreeEntry
}{
{
desc: "entry with blob OID",
- entries: []*TreeEntry{
+ entries: []TreeEntry{
{
OID: blobID,
Mode: "100644",
@@ -81,7 +67,7 @@ func TestTreeEntry_Write(t *testing.T) {
},
{
desc: "entry with tree OID",
- entries: []*TreeEntry{
+ entries: []TreeEntry{
{
OID: treeID,
Mode: "040000",
@@ -98,7 +84,7 @@ func TestTreeEntry_Write(t *testing.T) {
},
{
desc: "mixed tree and blob entries",
- entries: []*TreeEntry{
+ entries: []TreeEntry{
{
OID: treeID,
Mode: "040000",
@@ -135,7 +121,7 @@ func TestTreeEntry_Write(t *testing.T) {
},
{
desc: "entry with nonexistent object",
- entries: []*TreeEntry{
+ entries: []TreeEntry{
{
OID: nonExistentBlobID,
Mode: "100644",
@@ -150,69 +136,12 @@ func TestTreeEntry_Write(t *testing.T) {
},
},
},
- {
- desc: "entry with duplicate file",
- entries: []*TreeEntry{
- {
- OID: blobID,
- Mode: "100644",
- Path: "file",
- },
- {
- OID: nonExistentBlobID,
- Mode: "100644",
- Path: "file",
- },
- },
- expectedErrString: "duplicateEntries: contains duplicate file entries",
- },
- {
- desc: "entry with malformed mode",
- entries: []*TreeEntry{
- {
- OID: blobID,
- Mode: "1006442",
- Path: "file",
- },
- },
- expectedErrString: "badFilemode: contains bad file modes",
- },
- {
- desc: "tries to write .git file",
- entries: []*TreeEntry{
- {
- OID: blobID,
- Mode: "040000",
- Path: ".git",
- },
- },
- expectedErrString: "hasDotgit: contains '.git'",
- },
} {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
- tree := &TreeEntry{
- Type: Tree,
- Mode: "040000",
- Entries: tc.entries,
- }
-
- err := tree.Write(ctx, repo)
- oid := tree.OID
- if tc.expectedErrString != "" {
- if gitVersion.HashObjectFsck() {
- switch e := err.(type) {
- case structerr.Error:
- stderr := e.Metadata()["stderr"].(string)
- strings.Contains(stderr, tc.expectedErrString)
- default:
- strings.Contains(err.Error(), tc.expectedErrString)
- }
- }
- return
- }
+ oid, err := repo.WriteTree(ctx, tc.entries)
require.NoError(t, err)
output := text.ChompBytes(gittest.Exec(t, cfg, "-C", repoPath, "ls-tree", "-r", string(oid)))
@@ -250,710 +179,3 @@ func TestTreeEntry_Write(t *testing.T) {
})
}
}
-
-func TestTreeEntryByPath(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- desc string
- input []*TreeEntry
- expected []*TreeEntry
- }{
- {
- desc: "all blobs",
- input: []*TreeEntry{
- {
- Type: Blob,
- Path: "abc",
- },
- {
- Type: Blob,
- Path: "ab",
- },
- {
- Type: Blob,
- Path: "a",
- },
- },
- expected: []*TreeEntry{
- {
- Type: Blob,
- Path: "a",
- },
- {
- Type: Blob,
- Path: "ab",
- },
- {
- Type: Blob,
- Path: "abc",
- },
- },
- },
- {
- desc: "blobs and trees",
- input: []*TreeEntry{
- {
- Type: Blob,
- Path: "abc",
- },
- {
- Type: Tree,
- Path: "ab",
- },
- {
- Type: Blob,
- Path: "a",
- },
- },
- expected: []*TreeEntry{
- {
- Type: Blob,
- Path: "a",
- },
- {
- Type: Tree,
- Path: "ab",
- },
- {
- Type: Blob,
- Path: "abc",
- },
- },
- },
- {
- desc: "trees get sorted with / appended",
- input: []*TreeEntry{
- {
- Type: Tree,
- Path: "a",
- },
- {
- Type: Blob,
- Path: "a+",
- },
- },
- expected: []*TreeEntry{
- {
- Type: Blob,
- Path: "a+",
- },
- {
- Type: Tree,
- Path: "a",
- },
- },
- },
- {
- desc: "blobs get sorted without / appended",
- input: []*TreeEntry{
- {
- Type: Blob,
- Path: "a",
- },
- {
- Type: Blob,
- Path: "a+",
- },
- },
- expected: []*TreeEntry{
- {
- Type: Blob,
- Path: "a",
- },
- {
- Type: Blob,
- Path: "a+",
- },
- },
- },
- }
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- sort.Stable(TreeEntriesByPath(tc.input))
-
- require.Equal(
- t,
- tc.expected,
- tc.input,
- )
- })
- }
-}
-
-func TestReadTree(t *testing.T) {
- t.Parallel()
-
- cfg := testcfg.Build(t)
- ctx := testhelper.Context(t)
-
- repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{
- SkipCreationViaService: true,
- })
- repo := NewTestRepo(t, cfg, repoProto)
-
- blobID := gittest.WriteBlob(t, cfg, repoPath, []byte("blob contents"))
- emptyTreeID := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{})
- treeWithBlob := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
- {OID: blobID, Mode: "100644", Path: "nonexecutable"},
- {OID: blobID, Mode: "100755", Path: "executable"},
- })
- treeWithSubtree := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
- {OID: emptyTreeID, Mode: "040000", Path: "subdir"},
- })
- treeWithNestedSubtrees := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
- {OID: treeWithSubtree, Mode: "040000", Path: "nested-subdir"},
- })
- treeWithSubtreeAndBlob := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
- {OID: treeWithSubtree, Mode: "040000", Path: "subdir"},
- {OID: blobID, Mode: "100644", Path: "blob"},
- })
- treeWithSubtreeContainingBlob := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
- {OID: treeWithSubtreeAndBlob, Mode: "040000", Path: "subdir"},
- })
-
- for _, tc := range []struct {
- desc string
- treeish git.Revision
- options []ReadTreeOption
- cfg *readTreeConfig
- expectedTree TreeEntry
- expectedErr error
- }{
- {
- desc: "empty tree",
- treeish: emptyTreeID.Revision(),
- expectedTree: TreeEntry{
- OID: emptyTreeID,
- Type: Tree,
- Mode: "040000",
- },
- },
- {
- desc: "tree with blob",
- treeish: treeWithBlob.Revision(),
- expectedTree: TreeEntry{
- OID: treeWithBlob,
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{
- {Mode: "100755", Type: Blob, OID: blobID, Path: "executable"},
- {Mode: "100644", Type: Blob, OID: blobID, Path: "nonexecutable"},
- },
- },
- },
- {
- desc: "tree with subtree",
- treeish: treeWithSubtree.Revision(),
- expectedTree: TreeEntry{
- OID: treeWithSubtree,
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{
- {Mode: "040000", Type: Tree, OID: emptyTreeID, Path: "subdir"},
- },
- },
- },
- {
- desc: "nested trees",
- treeish: treeWithNestedSubtrees.Revision(),
- expectedTree: TreeEntry{
- OID: treeWithNestedSubtrees,
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{
- {Mode: "040000", Type: Tree, OID: treeWithSubtree, Path: "nested-subdir"},
- },
- },
- },
- {
- desc: "recursive nested trees",
- treeish: treeWithNestedSubtrees.Revision(),
- options: []ReadTreeOption{WithRecursive()},
- expectedTree: TreeEntry{
- OID: treeWithNestedSubtrees,
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{
- {
- Mode: "040000",
- Type: Tree,
- OID: treeWithSubtree,
- Path: "nested-subdir",
- Entries: []*TreeEntry{
- {
- Mode: "040000",
- Type: Tree,
- OID: emptyTreeID,
- Path: "subdir",
- },
- },
- },
- },
- },
- },
- {
- desc: "nested subtree",
- treeish: treeWithNestedSubtrees.Revision(),
- options: []ReadTreeOption{WithRelativePath("nested-subdir")},
- expectedTree: TreeEntry{
- OID: treeWithSubtree,
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{
- {Mode: "040000", Type: Tree, OID: emptyTreeID, Path: "subdir"},
- },
- },
- },
- {
- desc: "nested recursive subtree",
- treeish: treeWithSubtreeContainingBlob.Revision(),
- options: []ReadTreeOption{
- WithRelativePath("subdir"),
- WithRecursive(),
- },
- expectedTree: TreeEntry{
- OID: treeWithSubtreeAndBlob,
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{
- {Mode: "100644", Type: Blob, OID: blobID, Path: "blob"},
- {
- Mode: "040000",
- Type: Tree,
- OID: treeWithSubtree,
- Path: "subdir",
- Entries: []*TreeEntry{
- {
- Mode: "040000",
- Type: Tree,
- OID: emptyTreeID,
- Path: "subdir",
- },
- },
- },
- },
- },
- },
- {
- desc: "recursive nested trees and blobs",
- treeish: treeWithSubtreeAndBlob.Revision(),
- options: []ReadTreeOption{
- WithRecursive(),
- },
- expectedTree: TreeEntry{
- OID: treeWithSubtreeAndBlob,
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{
- {Mode: "100644", Type: Blob, OID: blobID, Path: "blob"},
- {
- Mode: "040000",
- Type: Tree,
- OID: treeWithSubtree,
- Path: "subdir",
- Entries: []*TreeEntry{
- {
- Mode: "040000",
- Type: Tree,
- OID: emptyTreeID,
- Path: "subdir",
- },
- },
- },
- },
- },
- },
- {
- desc: "listing blob fails",
- treeish: blobID.Revision(),
- // We get a NotExist error here because it's invalid to suffix an object ID
- // which resolves to a blob with a colon (":") given that it's not possible
- // to resolve a subpath.
- expectedErr: git.ErrReferenceNotFound,
- },
- {
- desc: "valid revision with invalid path",
- treeish: treeWithSubtree.Revision(),
- options: []ReadTreeOption{
- WithRelativePath("does-not-exist"),
- },
- expectedErr: git.ErrReferenceNotFound,
- },
- {
- desc: "valid revision with path pointing to blob",
- treeish: treeWithSubtreeAndBlob.Revision(),
- options: []ReadTreeOption{
- WithRelativePath("blob"),
- },
- expectedErr: ErrNotTreeish,
- },
- {
- desc: "listing nonexistent object fails",
- treeish: "does-not-exist",
- expectedErr: git.ErrReferenceNotFound,
- },
- } {
- tc := tc
- t.Run(tc.desc, func(t *testing.T) {
- t.Parallel()
-
- tree, err := repo.ReadTree(ctx, tc.treeish, tc.options...)
- if tc.expectedErr != nil {
- require.ErrorIs(t, err, tc.expectedErr)
- return
- }
- require.Equal(t, tc.expectedTree, *tree)
- })
- }
-}
-
-func TestWriteTreeRecursively(t *testing.T) {
- cfg := testcfg.Build(t)
-
- testCases := []struct {
- desc string
- setupTree func(*testing.T, string) *TreeEntry
- }{
- {
- desc: "every level has a new tree",
- setupTree: func(t *testing.T, repoPath string) *TreeEntry {
- blobA := gittest.WriteBlob(t, cfg, repoPath, []byte("a"))
-
- return &TreeEntry{
- OID: "",
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{
- {
- OID: "",
- Type: Tree,
- Mode: "040000",
- Path: "dirA",
- Entries: []*TreeEntry{
- {
- OID: "",
- Mode: "040000",
- Type: Tree,
- Path: "dirB",
- Entries: []*TreeEntry{
- {
- OID: blobA,
- Type: Blob,
- Mode: "100644",
- Path: "file1",
- },
- },
- },
- },
- },
- },
- }
- },
- },
- {
- desc: "only some new trees",
- setupTree: func(t *testing.T, repoPath string) *TreeEntry {
- blobA := gittest.WriteBlob(t, cfg, repoPath, []byte("a"))
- blobB := gittest.WriteBlob(t, cfg, repoPath, []byte("b"))
- blobC := gittest.WriteBlob(t, cfg, repoPath, []byte("c"))
- blobD := gittest.WriteBlob(t, cfg, repoPath, []byte("d"))
- dirDTree := gittest.WriteTree(
- t,
- cfg,
- repoPath,
- []gittest.TreeEntry{
- {
- OID: blobC,
- Mode: "100644",
- Path: "file3",
- },
- {
- OID: blobD,
- Mode: "100644",
- Path: "file4",
- },
- })
- dirCTree := gittest.WriteTree(
- t,
- cfg,
- repoPath,
- []gittest.TreeEntry{
- {
- OID: dirDTree,
- Mode: "040000",
- Path: "dirD",
- },
- },
- )
-
- return &TreeEntry{
- OID: "",
- Type: Tree,
- Mode: "040000",
- Entries: []*TreeEntry{
- {
- OID: "",
- Type: Tree,
- Mode: "040000",
- Path: "dirA",
- Entries: []*TreeEntry{
- {
- OID: "",
- Mode: "040000",
- Type: Tree,
- Path: "dirB",
- Entries: []*TreeEntry{
- {
- OID: blobA,
- Type: Blob,
- Mode: "100644",
- Path: "file1",
- },
- {
- OID: blobB,
- Type: Blob,
- Mode: "100644",
- Path: "file2",
- },
- },
- },
- },
- },
- {
- OID: dirCTree,
- Type: Tree,
- Mode: "040000",
- Path: "dirC",
- Entries: []*TreeEntry{
- {
- OID: dirDTree,
- Mode: "040000",
- Type: Tree,
- Path: "dirD",
- Entries: []*TreeEntry{
- {
- OID: blobC,
- Type: Blob,
- Mode: "100644",
- Path: "file3",
- },
- {
- OID: blobD,
- Type: Blob,
- Mode: "100644",
- Path: "file4",
- },
- },
- },
- },
- },
- },
- }
- },
- },
- }
-
- for _, tc := range testCases {
- tc := tc
- t.Run(tc.desc, func(t *testing.T) {
- t.Parallel()
-
- ctx := testhelper.Context(t)
- repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{
- SkipCreationViaService: true,
- })
- repo := NewTestRepo(t, cfg, repoProto)
-
- treeEntry := tc.setupTree(t, repoPath)
- require.NoError(t, treeEntry.Write(
- ctx,
- repo,
- ))
-
- requireTreeEquals(t, ctx, repo, treeEntry.OID, treeEntry)
- })
- }
-}
-
-func requireTreeEquals(
- t *testing.T,
- ctx context.Context,
- repo *Repo,
- treeOID git.ObjectID,
- expect *TreeEntry,
-) {
- tree, err := repo.ReadTree(ctx, git.Revision(treeOID))
- require.NoError(t, err)
-
- for i, entry := range tree.Entries {
- if tree.Entries[i].OID != "" {
- require.Equal(t, expect.Entries[i].Mode, entry.Mode)
- require.Equal(t, expect.Entries[i].Type, entry.Type)
- require.Equal(t, expect.Entries[i].OID, entry.OID)
- require.Equal(t, expect.Entries[i].Path, entry.Path)
- } else {
- objectInfo, err := repo.ReadObjectInfo(
- ctx,
- git.Revision(entry.OID),
- )
- require.NoError(t, err)
-
- require.Equal(t, expect.Entries[i].Type, ToEnum(objectInfo.Type))
- }
-
- if entry.Type == Tree {
- requireTreeEquals(t, ctx, repo, entry.OID, expect.Entries[i])
- }
- }
-}
-
-func TestReadTree_WithRecursive(t *testing.T) {
- t.Parallel()
-
- cfg := testcfg.Build(t)
- ctx := testhelper.Context(t)
-
- testCases := []struct {
- desc string
- setupTree func(t *testing.T, repoPath string) (git.ObjectID, TreeEntry)
- expectedErr error
- }{
- {
- desc: "flat tree",
- setupTree: func(t *testing.T, repoPath string) (git.ObjectID, TreeEntry) {
- blobA := gittest.WriteBlob(t, cfg, repoPath, []byte("a"))
- blobB := gittest.WriteBlob(t, cfg, repoPath, []byte("b"))
-
- treeID := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
- {
- Mode: "100644",
- Path: "fileA",
- OID: blobA,
- },
- {
- Mode: "100644",
- Path: "fileB",
- OID: blobB,
- },
- })
- return gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("master"),
- gittest.WithTree(treeID)),
- TreeEntry{
- OID: treeID,
- Mode: "040000",
- Type: Tree,
- Entries: []*TreeEntry{
- {
- Mode: "100644",
- Path: "fileA",
- OID: blobA,
- Type: Blob,
- },
- {
- Mode: "100644",
- Path: "fileB",
- OID: blobB,
- Type: Blob,
- },
- },
- }
- },
- },
- {
- desc: "nested tree",
- setupTree: func(t *testing.T, repoPath string) (git.ObjectID, TreeEntry) {
- blobA := gittest.WriteBlob(t, cfg, repoPath, []byte("a"))
- blobB := gittest.WriteBlob(t, cfg, repoPath, []byte("b"))
- blobC := gittest.WriteBlob(t, cfg, repoPath, []byte("c"))
- dirATree := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
- {
- Mode: "100644",
- Path: "file1InDirA",
- OID: blobA,
- },
- })
-
- treeID := gittest.WriteTree(t, cfg, repoPath, []gittest.TreeEntry{
- {
- Mode: "100644",
- Path: "fileB",
- OID: blobB,
- },
- {
- Mode: "100644",
- Path: "fileC",
- OID: blobC,
- },
- {
- Mode: "040000",
- Path: "dirA",
- OID: dirATree,
- },
- })
-
- return gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("master"),
- gittest.WithTree(treeID)),
- TreeEntry{
- OID: treeID,
- Mode: "040000",
- Type: Tree,
- Entries: []*TreeEntry{
- {
- Mode: "040000",
- Path: "dirA",
- Type: Tree,
- OID: dirATree,
- Entries: []*TreeEntry{
- {
- Mode: "100644",
- Path: "file1InDirA",
- OID: blobA,
- Type: Blob,
- },
- },
- },
- {
- Mode: "100644",
- Path: "fileB",
- OID: blobB,
- Type: Blob,
- },
- {
- Mode: "100644",
- Path: "fileC",
- OID: blobC,
- Type: Blob,
- },
- },
- }
- },
- },
- }
-
- for _, tc := range testCases {
- tc := tc
- t.Run(tc.desc, func(t *testing.T) {
- t.Parallel()
-
- repoProto, repoPath := gittest.CreateRepository(
- t,
- ctx,
- cfg,
- gittest.CreateRepositoryConfig{
- SkipCreationViaService: true,
- })
- repo := NewTestRepo(t, cfg, repoProto)
-
- commitID, expectedTree := tc.setupTree(t, repoPath)
-
- tree, err := repo.ReadTree(
- ctx,
- git.Revision(commitID),
- WithRecursive(),
- )
- require.NoError(t, err)
- require.Equal(t, expectedTree, *tree)
- })
- }
-}
diff --git a/internal/git/version.go b/internal/git/version.go
index 43cff0475..5048007fa 100644
--- a/internal/git/version.go
+++ b/internal/git/version.go
@@ -75,14 +75,6 @@ func (v Version) IsSupported() bool {
return !v.LessThan(minimumVersion)
}
-// HashObjectFsck detects whether or not the given Git version will do fsck
-// checks when git-hash-object writes objects.
-func (v Version) HashObjectFsck() bool {
- return !v.LessThan(Version{
- major: 2, minor: 40, patch: 0,
- })
-}
-
// PatchIDRespectsBinaries detects whether the given Git version correctly handles binary diffs when
// computing a patch ID. Previous to Git v2.39.0, git-patch-id(1) just completely ignored any binary
// diffs and thus would consider two diffs the same even if a binary changed.
diff --git a/internal/gitaly/service/commit/tree_entries.go b/internal/gitaly/service/commit/tree_entries.go
index 8072e49de..cd43e59a2 100644
--- a/internal/gitaly/service/commit/tree_entries.go
+++ b/internal/gitaly/service/commit/tree_entries.go
@@ -6,7 +6,6 @@ import (
"encoding/json"
"errors"
"fmt"
- "path/filepath"
"sort"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus"
@@ -94,12 +93,10 @@ func (s *server) sendTreeEntries(
// git-ls-tree(1) is worse than using a long-lived catfile process. We thus fall back to
// using catfile readers to answer these non-recursive queries.
if recursive {
- tree, err := repo.ReadTree(
- ctx,
- git.Revision(revision),
- localrepo.WithRelativePath(path),
- localrepo.WithRecursive(),
- )
+ treeEntries, err := repo.ListEntries(ctx, git.Revision(revision), &localrepo.ListEntriesConfig{
+ Recursive: recursive,
+ RelativePath: path,
+ })
if err != nil {
// Design wart: we do not return an error if the request does not
// point to a tree object, but just return nothing.
@@ -108,27 +105,24 @@ func (s *server) sendTreeEntries(
}
// Same if we try to list tree entries of a revision which doesn't exist.
- if errors.Is(err, localrepo.ErrTreeNotExist) || errors.Is(err, git.ErrReferenceNotFound) {
+ if errors.Is(err, localrepo.ErrNotExist) {
return nil
}
- return fmt.Errorf("reading tree: %w", err)
+ return fmt.Errorf("listing tree entries: %w", err)
}
- if err := tree.Walk(func(dir string, entry *localrepo.TreeEntry) error {
- if entry.OID == tree.OID {
- return nil
- }
-
+ entries = make([]*gitalypb.TreeEntry, 0, len(treeEntries))
+ for _, entry := range treeEntries {
objectID, err := entry.OID.Bytes()
if err != nil {
return fmt.Errorf("converting tree entry OID: %w", err)
}
- newEntry, err := git.NewTreeEntry(
+ treeEntry, err := git.NewTreeEntry(
revision,
path,
- []byte(filepath.Join(dir, entry.Path)),
+ []byte(entry.Path),
objectID,
[]byte(entry.Mode),
)
@@ -136,11 +130,7 @@ func (s *server) sendTreeEntries(
return fmt.Errorf("converting tree entry: %w", err)
}
- entries = append(entries, newEntry)
-
- return nil
- }); err != nil {
- return fmt.Errorf("listing tree entries: %w", err)
+ entries = append(entries, treeEntry)
}
} else {
var err error
diff --git a/internal/gitaly/service/operations/submodules.go b/internal/gitaly/service/operations/submodules.go
index 28407dcdf..f5daad03c 100644
--- a/internal/gitaly/service/operations/submodules.go
+++ b/internal/gitaly/service/operations/submodules.go
@@ -71,31 +71,30 @@ func (s *Server) updateSubmodule(ctx context.Context, quarantineRepo *localrepo.
// tree with the new tree abcabc. Continue iterating up the tree,
// writing a new tree object each time.
for {
- tree, err := quarantineRepo.ReadTree(
+ entries, err := quarantineRepo.ListEntries(
ctx,
+
git.Revision("refs/heads/"+string(req.GetBranch())),
- localrepo.WithRelativePath(path),
- localrepo.WithRecursive(),
- )
+ &localrepo.ListEntriesConfig{
+ RelativePath: path,
+ })
if err != nil {
- if errors.Is(err, git.ErrReferenceNotFound) {
+ if strings.Contains(err.Error(), "invalid object name") {
return "", fmt.Errorf("submodule: %s", git2go.LegacyErrPrefixInvalidSubmodulePath)
}
return "", fmt.Errorf("error reading tree: %w", err)
}
- var newEntries []*localrepo.TreeEntry
- if err := tree.Walk(func(path string, entry *localrepo.TreeEntry) error {
- if entry.OID == tree.OID {
- return nil
- }
+ var newEntries []localrepo.TreeEntry
+ var newTreeID git.ObjectID
+ for _, entry := range entries {
// If the entry's path does not match, then we simply
// want to retain this tree entry.
if entry.Path != base {
- newEntries = append(newEntries, entry)
- return nil
+ newEntries = append(newEntries, *entry)
+ continue
}
// If we are at the submodule we want to replace, check
@@ -103,42 +102,35 @@ func (s *Server) updateSubmodule(ctx context.Context, quarantineRepo *localrepo.
// if it's not a submodule.
if filepath.Join(path, entry.Path) == string(req.GetSubmodule()) {
if string(entry.OID) == req.GetCommitSha() {
- //nolint:stylecheck
- return fmt.Errorf(
- "The submodule %s is already at %s",
- req.GetSubmodule(),
- replaceWith,
- )
+ return "",
+ //nolint:stylecheck
+ fmt.Errorf(
+ "The submodule %s is already at %s",
+ req.GetSubmodule(),
+ replaceWith,
+ )
}
if entry.Type != localrepo.Submodule {
- return fmt.Errorf("submodule: %s", git2go.LegacyErrPrefixInvalidSubmodulePath)
+ return "", fmt.Errorf("submodule: %s", git2go.LegacyErrPrefixInvalidSubmodulePath)
}
}
// Otherwise, create a new tree entry
submoduleFound = true
- newEntries = append(newEntries, &localrepo.TreeEntry{
+ newEntries = append(newEntries, localrepo.TreeEntry{
Mode: entry.Mode,
Path: entry.Path,
OID: replaceWith,
})
-
- return nil
- }); err != nil {
- return "", err
}
- newTree := &localrepo.TreeEntry{
- Type: localrepo.Tree,
- Mode: "040000",
- Entries: newEntries,
- }
- if err := newTree.Write(ctx, quarantineRepo); err != nil {
+ newTreeID, err = quarantineRepo.WriteTree(ctx, newEntries)
+ if err != nil {
return "", fmt.Errorf("write tree: %w", err)
}
- replaceWith = newTree.OID
+ replaceWith = newTreeID
if path == "." {
break