detached 7559d0f

feat(app): add the ListPaths use case and reader port

commit
7559d0fb77f4fb265c5725f08b0cea3195eac519
author
brice · · 2026-10-11 18:30 UTC
parent
39ca311
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +169 -7
M go.mod +1 -1
@@ -12,7 +12,7 @@ tool (
1212 )
1313
1414 require (
15- bnema.dev/ermite-api v0.0.0-20261011052945-e9394b717bc2
15+ bnema.dev/ermite-api v0.0.0-20261011182719-cb5699451db6
1616 bnema.dev/ermite-git v0.1.0
1717 bnema.dev/ermite-kv v0.0.0-20261010031554-5e09b48f81c3
1818 connectrpc.com/connect v1.21.0
@@ -12,7 +12,7 @@ tool (
12 )12 )
13 13
14 require (14 require (
15- bnema.dev/ermite-api v0.0.0-20261011052945-e9394b717bc215+ bnema.dev/ermite-api v0.0.0-20261011182719-cb5699451db6
16 bnema.dev/ermite-git v0.1.016 bnema.dev/ermite-git v0.1.0
17 bnema.dev/ermite-kv v0.0.0-20261010031554-5e09b48f81c317 bnema.dev/ermite-kv v0.0.0-20261010031554-5e09b48f81c3
18 connectrpc.com/connect v1.21.018 connectrpc.com/connect v1.21.0
M go.sum +2 -2 generated file: collapsed
M internal/adapters/ermitegit/ermitegit.go +5 -0
@@ -315,6 +315,11 @@ func (a *Adapter) ResolveRefPath(ctx context.Context, repo domain.RepoName, path
315315 }, cli)
316316 }
317317
318+// ListPaths is not implemented yet.
319+func (a *Adapter) ListPaths(_ context.Context, _ domain.RepoName, _ domain.Revision, _, _ int) (domain.PathList, error) {
320+ return domain.PathList{}, errors.ErrUnsupported
321+}
322+
318323 // GetTree lists a directory.
319324 func (a *Adapter) GetTree(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath, includeLastCommit bool) (domain.Tree, error) {
320325 cli := func() (domain.Tree, error) { return a.cli.GetTree(ctx, repo, rev, path, includeLastCommit) }
@@ -315,6 +315,11 @@ func (a *Adapter) ResolveRefPath(ctx context.Context, repo domain.RepoName, path
315 }, cli)315 }, cli)
316 }316 }
317 317
318+// ListPaths is not implemented yet.
319+func (a *Adapter) ListPaths(_ context.Context, _ domain.RepoName, _ domain.Revision, _, _ int) (domain.PathList, error) {
320+ return domain.PathList{}, errors.ErrUnsupported
321+}
322+
318 // GetTree lists a directory.323 // GetTree lists a directory.
319 func (a *Adapter) GetTree(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath, includeLastCommit bool) (domain.Tree, error) {324 func (a *Adapter) GetTree(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath, includeLastCommit bool) (domain.Tree, error) {
320 cli := func() (domain.Tree, error) { return a.cli.GetTree(ctx, repo, rev, path, includeLastCommit) }325 cli := func() (domain.Tree, error) { return a.cli.GetTree(ctx, repo, rev, path, includeLastCommit) }
M internal/adapters/gitcli/read.go +5 -0
@@ -288,6 +288,11 @@ func (g *Git) lookup(ctx context.Context, dir string, rev domain.Revision, path
288288 return obj, err
289289 }
290290
291+// ListPaths is not implemented yet.
292+func (g *Git) ListPaths(_ context.Context, _ domain.RepoName, _ domain.Revision, _, _ int) (domain.PathList, error) {
293+ return domain.PathList{}, errors.ErrUnsupported
294+}
295+
291296 // GetTree lists a directory with `git ls-tree -z -l`.
292297 //
293298 //hexcheck:ignore suspicious-business-logic-in-adapter parses git output into domain values; limits and rules come from the port
@@ -288,6 +288,11 @@ func (g *Git) lookup(ctx context.Context, dir string, rev domain.Revision, path
288 return obj, err288 return obj, err
289 }289 }
290 290
291+// ListPaths is not implemented yet.
292+func (g *Git) ListPaths(_ context.Context, _ domain.RepoName, _ domain.Revision, _, _ int) (domain.PathList, error) {
293+ return domain.PathList{}, errors.ErrUnsupported
294+}
295+
291 // GetTree lists a directory with `git ls-tree -z -l`.296 // GetTree lists a directory with `git ls-tree -z -l`.
292 //297 //
293 //hexcheck:ignore suspicious-business-logic-in-adapter parses git output into domain values; limits and rules come from the port298 //hexcheck:ignore suspicious-business-logic-in-adapter parses git output into domain values; limits and rules come from the port
M internal/adapters/rpc/git.go +5 -0
@@ -85,6 +85,11 @@ func (s *gitService) GetTree(ctx context.Context, req *connect.Request[pb.GetTre
8585 return connect.NewResponse(out), nil
8686 }
8787
88+// ListPaths is not served yet.
89+func (s *gitService) ListPaths(context.Context, *connect.Request[pb.ListPathsRequest]) (*connect.Response[pb.ListPathsResponse], error) {
90+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ListPaths is not implemented"))
91+}
92+
8893 // GetBlob streams the blob in chunks of BlobChunkSize. The first message
8994 // carries the metadata and the first chunk.
9095 func (s *gitService) GetBlob(ctx context.Context, req *connect.Request[pb.GetBlobRequest], stream *connect.ServerStream[pb.GetBlobResponse]) error {
@@ -85,6 +85,11 @@ func (s *gitService) GetTree(ctx context.Context, req *connect.Request[pb.GetTre
85 return connect.NewResponse(out), nil85 return connect.NewResponse(out), nil
86 }86 }
87 87
88+// ListPaths is not served yet.
89+func (s *gitService) ListPaths(context.Context, *connect.Request[pb.ListPathsRequest]) (*connect.Response[pb.ListPathsResponse], error) {
90+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ListPaths is not implemented"))
91+}
92+
88 // GetBlob streams the blob in chunks of BlobChunkSize. The first message93 // GetBlob streams the blob in chunks of BlobChunkSize. The first message
89 // carries the metadata and the first chunk.94 // carries the metadata and the first chunk.
90 func (s *gitService) GetBlob(ctx context.Context, req *connect.Request[pb.GetBlobRequest], stream *connect.ServerStream[pb.GetBlobResponse]) error {95 func (s *gitService) GetBlob(ctx context.Context, req *connect.Request[pb.GetBlobRequest], stream *connect.ServerStream[pb.GetBlobResponse]) error {
M internal/app/browse.go +9 -0
@@ -174,6 +174,15 @@ func (b *Browse) GetTree(ctx context.Context, p domain.Principal, repo domain.Re
174174 return b.reader.GetTree(ctx, repo, rev, path, includeLastCommit)
175175 }
176176
177+// ListPaths lists the paths of every file and symbolic link of a revision, at
178+// most domain.MaxPathEntries paths and domain.MaxPathBytes bytes of paths.
179+func (b *Browse) ListPaths(ctx context.Context, p domain.Principal, repo domain.RepoName, rev domain.Revision) (domain.PathList, error) {
180+ if err := b.authorize(ctx, p, repo); err != nil {
181+ return domain.PathList{}, err
182+ }
183+ return b.reader.ListPaths(ctx, repo, rev, domain.MaxPathEntries, domain.MaxPathBytes)
184+}
185+
177186 // OpenBlob returns the description of a file and a reader of its content. The
178187 // caller closes the reader.
179188 func (b *Browse) OpenBlob(ctx context.Context, p domain.Principal, repo domain.RepoName, rev domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error) {
@@ -174,6 +174,15 @@ func (b *Browse) GetTree(ctx context.Context, p domain.Principal, repo domain.Re
174 return b.reader.GetTree(ctx, repo, rev, path, includeLastCommit)174 return b.reader.GetTree(ctx, repo, rev, path, includeLastCommit)
175 }175 }
176 176
177+// ListPaths lists the paths of every file and symbolic link of a revision, at
178+// most domain.MaxPathEntries paths and domain.MaxPathBytes bytes of paths.
179+func (b *Browse) ListPaths(ctx context.Context, p domain.Principal, repo domain.RepoName, rev domain.Revision) (domain.PathList, error) {
180+ if err := b.authorize(ctx, p, repo); err != nil {
181+ return domain.PathList{}, err
182+ }
183+ return b.reader.ListPaths(ctx, repo, rev, domain.MaxPathEntries, domain.MaxPathBytes)
184+}
185+
177 // OpenBlob returns the description of a file and a reader of its content. The186 // OpenBlob returns the description of a file and a reader of its content. The
178 // caller closes the reader.187 // caller closes the reader.
179 func (b *Browse) OpenBlob(ctx context.Context, p domain.Principal, repo domain.RepoName, rev domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error) {188 func (b *Browse) OpenBlob(ctx context.Context, p domain.Principal, repo domain.RepoName, rev domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error) {
M internal/app/browse_test.go +6 -0
@@ -76,6 +76,12 @@ func browseOps() []browseOp {
7676 }, func(r *mocks.RepoReader) {
7777 r.EXPECT().GetTree(mock.Anything, mock.Anything, mock.Anything, mock.Anything, true).Return(domain.Tree{}, nil)
7878 }},
79+ {"ListPaths", func(b *app.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
80+ _, err := b.ListPaths(ctx, p, repo, domain.Revision{})
81+ return err
82+ }, func(r *mocks.RepoReader) {
83+ r.EXPECT().ListPaths(mock.Anything, mock.Anything, mock.Anything, domain.MaxPathEntries, domain.MaxPathBytes).Return(domain.PathList{}, nil)
84+ }},
7985 {"OpenBlob", func(b *app.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
8086 _, rc, err := b.OpenBlob(ctx, p, repo, domain.Revision{}, root)
8187 if rc != nil {
@@ -76,6 +76,12 @@ func browseOps() []browseOp {
76 }, func(r *mocks.RepoReader) {76 }, func(r *mocks.RepoReader) {
77 r.EXPECT().GetTree(mock.Anything, mock.Anything, mock.Anything, mock.Anything, true).Return(domain.Tree{}, nil)77 r.EXPECT().GetTree(mock.Anything, mock.Anything, mock.Anything, mock.Anything, true).Return(domain.Tree{}, nil)
78 }},78 }},
79+ {"ListPaths", func(b *app.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
80+ _, err := b.ListPaths(ctx, p, repo, domain.Revision{})
81+ return err
82+ }, func(r *mocks.RepoReader) {
83+ r.EXPECT().ListPaths(mock.Anything, mock.Anything, mock.Anything, domain.MaxPathEntries, domain.MaxPathBytes).Return(domain.PathList{}, nil)
84+ }},
79 {"OpenBlob", func(b *app.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {85 {"OpenBlob", func(b *app.Browse, ctx context.Context, p domain.Principal, repo domain.RepoName) error {
80 _, rc, err := b.OpenBlob(ctx, p, repo, domain.Revision{}, root)86 _, rc, err := b.OpenBlob(ctx, p, repo, domain.Revision{}, root)
81 if rc != nil {87 if rc != nil {
M internal/domain/git.go +37 -4
@@ -155,20 +155,34 @@ func ParseTreePath(s string) (TreePath, error) {
155155 if s == "" {
156156 return TreePath{}, nil
157157 }
158+ if why := treePathProblem(s); why != "" {
159+ return TreePath{}, fmt.Errorf("%w: %q: %s", ErrInvalidPath, truncate(s), why)
160+ }
161+ return TreePath{s: s}, nil
162+}
163+
164+// IsAddressablePath reports whether s is a non-root path that ParseTreePath
165+// accepts as it is (no trailing slash). It allocates nothing.
166+func IsAddressablePath(s string) bool {
167+ return s != "" && treePathProblem(s) == ""
168+}
169+
170+// treePathProblem returns why s cannot be a non-root tree path, or "".
171+func treePathProblem(s string) string {
158172 if len(s) > maxTreePathLen || !utf8.ValidString(s) {
159- return TreePath{}, fmt.Errorf("%w: %q: too long or not valid UTF-8", ErrInvalidPath, truncate(s))
173+ return "too long or not valid UTF-8"
160174 }
161175 for _, c := range s {
162176 if c < 0x20 || c == 0x7f {
163- return TreePath{}, fmt.Errorf("%w: %q: control character", ErrInvalidPath, truncate(s))
177+ return "control character"
164178 }
165179 }
166180 for part := range strings.SplitSeq(s, "/") {
167181 if part == "" || part == "." || part == ".." {
168- return TreePath{}, fmt.Errorf("%w: %q: empty, . or .. component", ErrInvalidPath, truncate(s))
182+ return "empty, . or .. component"
169183 }
170184 }
171- return TreePath{s: s}, nil
185+ return ""
172186 }
173187
174188 func truncate(s string) string {
@@ -325,6 +339,25 @@ type Tree struct {
325339 // MaxTreeEntries bounds the entries of a listed directory.
326340 const MaxTreeEntries = 5000
327341
342+// MaxPathEntries bounds the paths ListPaths returns.
343+const MaxPathEntries = 100_000
344+
345+// MaxPathBytes bounds the total bytes of the paths ListPaths returns.
346+const MaxPathBytes = 8 << 20
347+
348+// PathList is the recursive list of the file paths of a revision.
349+type PathList struct {
350+ // CommitID is the commit the revision resolved to.
351+ CommitID ObjectID
352+ // TreeID is the root tree of that commit.
353+ TreeID ObjectID
354+ // Paths are the paths of the files and symbolic links, in tree order (the
355+ // order of `git ls-tree -r`).
356+ Paths []string
357+ // Truncated is true when more paths exist than the ones in Paths.
358+ Truncated bool
359+}
360+
328361 // MaxLastCommitEntries bounds the entries of one directory that get a last
329362 // commit: each costs a history lookup.
330363 const MaxLastCommitEntries = 200
@@ -155,20 +155,34 @@ func ParseTreePath(s string) (TreePath, error) {
155 if s == "" {155 if s == "" {
156 return TreePath{}, nil156 return TreePath{}, nil
157 }157 }
158+ if why := treePathProblem(s); why != "" {
159+ return TreePath{}, fmt.Errorf("%w: %q: %s", ErrInvalidPath, truncate(s), why)
160+ }
161+ return TreePath{s: s}, nil
162+}
163+
164+// IsAddressablePath reports whether s is a non-root path that ParseTreePath
165+// accepts as it is (no trailing slash). It allocates nothing.
166+func IsAddressablePath(s string) bool {
167+ return s != "" && treePathProblem(s) == ""
168+}
169+
170+// treePathProblem returns why s cannot be a non-root tree path, or "".
171+func treePathProblem(s string) string {
158 if len(s) > maxTreePathLen || !utf8.ValidString(s) {172 if len(s) > maxTreePathLen || !utf8.ValidString(s) {
159- return TreePath{}, fmt.Errorf("%w: %q: too long or not valid UTF-8", ErrInvalidPath, truncate(s))173+ return "too long or not valid UTF-8"
160 }174 }
161 for _, c := range s {175 for _, c := range s {
162 if c < 0x20 || c == 0x7f {176 if c < 0x20 || c == 0x7f {
163- return TreePath{}, fmt.Errorf("%w: %q: control character", ErrInvalidPath, truncate(s))177+ return "control character"
164 }178 }
165 }179 }
166 for part := range strings.SplitSeq(s, "/") {180 for part := range strings.SplitSeq(s, "/") {
167 if part == "" || part == "." || part == ".." {181 if part == "" || part == "." || part == ".." {
168- return TreePath{}, fmt.Errorf("%w: %q: empty, . or .. component", ErrInvalidPath, truncate(s))182+ return "empty, . or .. component"
169 }183 }
170 }184 }
171- return TreePath{s: s}, nil185+ return ""
172 }186 }
173 187
174 func truncate(s string) string {188 func truncate(s string) string {
@@ -325,6 +339,25 @@ type Tree struct {
325 // MaxTreeEntries bounds the entries of a listed directory.339 // MaxTreeEntries bounds the entries of a listed directory.
326 const MaxTreeEntries = 5000340 const MaxTreeEntries = 5000
327 341
342+// MaxPathEntries bounds the paths ListPaths returns.
343+const MaxPathEntries = 100_000
344+
345+// MaxPathBytes bounds the total bytes of the paths ListPaths returns.
346+const MaxPathBytes = 8 << 20
347+
348+// PathList is the recursive list of the file paths of a revision.
349+type PathList struct {
350+ // CommitID is the commit the revision resolved to.
351+ CommitID ObjectID
352+ // TreeID is the root tree of that commit.
353+ TreeID ObjectID
354+ // Paths are the paths of the files and symbolic links, in tree order (the
355+ // order of `git ls-tree -r`).
356+ Paths []string
357+ // Truncated is true when more paths exist than the ones in Paths.
358+ Truncated bool
359+}
360+
328 // MaxLastCommitEntries bounds the entries of one directory that get a last361 // MaxLastCommitEntries bounds the entries of one directory that get a last
329 // commit: each costs a history lookup.362 // commit: each costs a history lookup.
330 const MaxLastCommitEntries = 200363 const MaxLastCommitEntries = 200
M internal/domain/git_test.go +10 -0
@@ -80,6 +80,16 @@ func TestParseTreePath(t *testing.T) {
8080 }
8181 }
8282
83+func TestIsAddressablePath(t *testing.T) {
84+ for _, ok := range []string{"a", "a/b.txt", "dir/ünï.go", "a b/c", ".github/ci.yml"} {
85+ assert.True(t, domain.IsAddressablePath(ok), "%q", ok)
86+ }
87+ for _, bad := range []string{"", "/", "a/", "/a", "a//b", "a/../b", "./a", "..", "tab\tname", "a\nb", "a\x00b", "a\x7fb", "\xff", "a/\xfe/b", strings.Repeat("a", 4097)} {
88+ assert.False(t, domain.IsAddressablePath(bad), "%q", bad)
89+ }
90+ assert.True(t, domain.IsAddressablePath(strings.Repeat("a", 4096)))
91+}
92+
8393 func TestEntryKindOf(t *testing.T) {
8494 tests := map[uint32]domain.EntryKind{
8595 0o100644: domain.EntryFile,
@@ -80,6 +80,16 @@ func TestParseTreePath(t *testing.T) {
80 }80 }
81 }81 }
82 82
83+func TestIsAddressablePath(t *testing.T) {
84+ for _, ok := range []string{"a", "a/b.txt", "dir/ünï.go", "a b/c", ".github/ci.yml"} {
85+ assert.True(t, domain.IsAddressablePath(ok), "%q", ok)
86+ }
87+ for _, bad := range []string{"", "/", "a/", "/a", "a//b", "a/../b", "./a", "..", "tab\tname", "a\nb", "a\x00b", "a\x7fb", "\xff", "a/\xfe/b", strings.Repeat("a", 4097)} {
88+ assert.False(t, domain.IsAddressablePath(bad), "%q", bad)
89+ }
90+ assert.True(t, domain.IsAddressablePath(strings.Repeat("a", 4096)))
91+}
92+
83 func TestEntryKindOf(t *testing.T) {93 func TestEntryKindOf(t *testing.T) {
84 tests := map[uint32]domain.EntryKind{94 tests := map[uint32]domain.EntryKind{
85 0o100644: domain.EntryFile,95 0o100644: domain.EntryFile,
M internal/ports/mocks/RepoReader.go +84 -0
@@ -283,6 +283,90 @@ func (_c *RepoReader_ListCommits_Call) RunAndReturn(run func(ctx context.Context
283283 return _c
284284 }
285285
286+// ListPaths provides a mock function for the type RepoReader
287+func (_mock *RepoReader) ListPaths(ctx context.Context, repo domain.RepoName, rev domain.Revision, maxPaths int, maxBytes int) (domain.PathList, error) {
288+ ret := _mock.Called(ctx, repo, rev, maxPaths, maxBytes)
289+
290+ if len(ret) == 0 {
291+ panic("no return value specified for ListPaths")
292+ }
293+
294+ var r0 domain.PathList
295+ var r1 error
296+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.Revision, int, int) (domain.PathList, error)); ok {
297+ return returnFunc(ctx, repo, rev, maxPaths, maxBytes)
298+ }
299+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.Revision, int, int) domain.PathList); ok {
300+ r0 = returnFunc(ctx, repo, rev, maxPaths, maxBytes)
301+ } else {
302+ r0 = ret.Get(0).(domain.PathList)
303+ }
304+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.Revision, int, int) error); ok {
305+ r1 = returnFunc(ctx, repo, rev, maxPaths, maxBytes)
306+ } else {
307+ r1 = ret.Error(1)
308+ }
309+ return r0, r1
310+}
311+
312+// RepoReader_ListPaths_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPaths'
313+type RepoReader_ListPaths_Call struct {
314+ *mock.Call
315+}
316+
317+// ListPaths is a helper method to define mock.On call
318+// - ctx context.Context
319+// - repo domain.RepoName
320+// - rev domain.Revision
321+// - maxPaths int
322+// - maxBytes int
323+func (_e *RepoReader_Expecter) ListPaths(ctx any, repo any, rev any, maxPaths any, maxBytes any) *RepoReader_ListPaths_Call {
324+ return &RepoReader_ListPaths_Call{Call: _e.mock.On("ListPaths", ctx, repo, rev, maxPaths, maxBytes)}
325+}
326+
327+func (_c *RepoReader_ListPaths_Call) Run(run func(ctx context.Context, repo domain.RepoName, rev domain.Revision, maxPaths int, maxBytes int)) *RepoReader_ListPaths_Call {
328+ _c.Call.Run(func(args mock.Arguments) {
329+ var arg0 context.Context
330+ if args[0] != nil {
331+ arg0 = args[0].(context.Context)
332+ }
333+ var arg1 domain.RepoName
334+ if args[1] != nil {
335+ arg1 = args[1].(domain.RepoName)
336+ }
337+ var arg2 domain.Revision
338+ if args[2] != nil {
339+ arg2 = args[2].(domain.Revision)
340+ }
341+ var arg3 int
342+ if args[3] != nil {
343+ arg3 = args[3].(int)
344+ }
345+ var arg4 int
346+ if args[4] != nil {
347+ arg4 = args[4].(int)
348+ }
349+ run(
350+ arg0,
351+ arg1,
352+ arg2,
353+ arg3,
354+ arg4,
355+ )
356+ })
357+ return _c
358+}
359+
360+func (_c *RepoReader_ListPaths_Call) Return(pathList domain.PathList, err error) *RepoReader_ListPaths_Call {
361+ _c.Call.Return(pathList, err)
362+ return _c
363+}
364+
365+func (_c *RepoReader_ListPaths_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, rev domain.Revision, maxPaths int, maxBytes int) (domain.PathList, error)) *RepoReader_ListPaths_Call {
366+ _c.Call.Return(run)
367+ return _c
368+}
369+
286370 // ListRefs provides a mock function for the type RepoReader
287371 func (_mock *RepoReader) ListRefs(ctx context.Context, repo domain.RepoName) ([]domain.Ref, error) {
288372 ret := _mock.Called(ctx, repo)
@@ -283,6 +283,90 @@ func (_c *RepoReader_ListCommits_Call) RunAndReturn(run func(ctx context.Context
283 return _c283 return _c
284 }284 }
285 285
286+// ListPaths provides a mock function for the type RepoReader
287+func (_mock *RepoReader) ListPaths(ctx context.Context, repo domain.RepoName, rev domain.Revision, maxPaths int, maxBytes int) (domain.PathList, error) {
288+ ret := _mock.Called(ctx, repo, rev, maxPaths, maxBytes)
289+
290+ if len(ret) == 0 {
291+ panic("no return value specified for ListPaths")
292+ }
293+
294+ var r0 domain.PathList
295+ var r1 error
296+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.Revision, int, int) (domain.PathList, error)); ok {
297+ return returnFunc(ctx, repo, rev, maxPaths, maxBytes)
298+ }
299+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.Revision, int, int) domain.PathList); ok {
300+ r0 = returnFunc(ctx, repo, rev, maxPaths, maxBytes)
301+ } else {
302+ r0 = ret.Get(0).(domain.PathList)
303+ }
304+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.Revision, int, int) error); ok {
305+ r1 = returnFunc(ctx, repo, rev, maxPaths, maxBytes)
306+ } else {
307+ r1 = ret.Error(1)
308+ }
309+ return r0, r1
310+}
311+
312+// RepoReader_ListPaths_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPaths'
313+type RepoReader_ListPaths_Call struct {
314+ *mock.Call
315+}
316+
317+// ListPaths is a helper method to define mock.On call
318+// - ctx context.Context
319+// - repo domain.RepoName
320+// - rev domain.Revision
321+// - maxPaths int
322+// - maxBytes int
323+func (_e *RepoReader_Expecter) ListPaths(ctx any, repo any, rev any, maxPaths any, maxBytes any) *RepoReader_ListPaths_Call {
324+ return &RepoReader_ListPaths_Call{Call: _e.mock.On("ListPaths", ctx, repo, rev, maxPaths, maxBytes)}
325+}
326+
327+func (_c *RepoReader_ListPaths_Call) Run(run func(ctx context.Context, repo domain.RepoName, rev domain.Revision, maxPaths int, maxBytes int)) *RepoReader_ListPaths_Call {
328+ _c.Call.Run(func(args mock.Arguments) {
329+ var arg0 context.Context
330+ if args[0] != nil {
331+ arg0 = args[0].(context.Context)
332+ }
333+ var arg1 domain.RepoName
334+ if args[1] != nil {
335+ arg1 = args[1].(domain.RepoName)
336+ }
337+ var arg2 domain.Revision
338+ if args[2] != nil {
339+ arg2 = args[2].(domain.Revision)
340+ }
341+ var arg3 int
342+ if args[3] != nil {
343+ arg3 = args[3].(int)
344+ }
345+ var arg4 int
346+ if args[4] != nil {
347+ arg4 = args[4].(int)
348+ }
349+ run(
350+ arg0,
351+ arg1,
352+ arg2,
353+ arg3,
354+ arg4,
355+ )
356+ })
357+ return _c
358+}
359+
360+func (_c *RepoReader_ListPaths_Call) Return(pathList domain.PathList, err error) *RepoReader_ListPaths_Call {
361+ _c.Call.Return(pathList, err)
362+ return _c
363+}
364+
365+func (_c *RepoReader_ListPaths_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, rev domain.Revision, maxPaths int, maxBytes int) (domain.PathList, error)) *RepoReader_ListPaths_Call {
366+ _c.Call.Return(run)
367+ return _c
368+}
369+
286 // ListRefs provides a mock function for the type RepoReader370 // ListRefs provides a mock function for the type RepoReader
287 func (_mock *RepoReader) ListRefs(ctx context.Context, repo domain.RepoName) ([]domain.Ref, error) {371 func (_mock *RepoReader) ListRefs(ctx context.Context, repo domain.RepoName) ([]domain.Ref, error) {
288 ret := _mock.Called(ctx, repo)372 ret := _mock.Called(ctx, repo)
M internal/ports/ports.go +5 -0
@@ -156,6 +156,11 @@ type RepoReader interface {
156156 // domain.MaxLastCommitEntries entries get their last commit. It returns
157157 // domain.ErrNotDirectory if path names something else.
158158 GetTree(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath, includeLastCommit bool) (domain.Tree, error)
159+ // ListPaths lists the file and symlink paths of rev recursively in tree
160+ // order, at most maxPaths paths and maxBytes bytes of paths (PathList.Truncated
161+ // tells), skipping submodules and paths that are not addressable
162+ // (domain.IsAddressablePath).
163+ ListPaths(ctx context.Context, repo domain.RepoName, rev domain.Revision, maxPaths, maxBytes int) (domain.PathList, error)
159164 // OpenBlob returns the description of the file at path and a reader of its
160165 // content, which streams without holding the whole file in memory. The
161166 // caller must close the reader. It returns domain.ErrNotFile if path names
@@ -156,6 +156,11 @@ type RepoReader interface {
156 // domain.MaxLastCommitEntries entries get their last commit. It returns156 // domain.MaxLastCommitEntries entries get their last commit. It returns
157 // domain.ErrNotDirectory if path names something else.157 // domain.ErrNotDirectory if path names something else.
158 GetTree(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath, includeLastCommit bool) (domain.Tree, error)158 GetTree(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath, includeLastCommit bool) (domain.Tree, error)
159+ // ListPaths lists the file and symlink paths of rev recursively in tree
160+ // order, at most maxPaths paths and maxBytes bytes of paths (PathList.Truncated
161+ // tells), skipping submodules and paths that are not addressable
162+ // (domain.IsAddressablePath).
163+ ListPaths(ctx context.Context, repo domain.RepoName, rev domain.Revision, maxPaths, maxBytes int) (domain.PathList, error)
159 // OpenBlob returns the description of the file at path and a reader of its164 // OpenBlob returns the description of the file at path and a reader of its
160 // content, which streams without holding the whole file in memory. The165 // content, which streams without holding the whole file in memory. The
161 // caller must close the reader. It returns domain.ErrNotFile if path names166 // caller must close the reader. It returns domain.ErrNotFile if path names

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 41

↑↓ moveenter openesc close