detached e17a0e5

feat(kvstore): list open merge requests and issues across repositories

commit
e17a0e59e503b085f11e3ef2ba82b39e646a580a
author
brice · · 2026-10-19 06:19 UTC
parent
68460c1
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +278 -0
M internal/adapters/kvstore/issues.go +44 -0
@@ -185,6 +185,50 @@ func (i *Issues) ListOpen(ctx context.Context, repo domain.RepoName) ([]domain.I
185185 return out, nil
186186 }
187187
188+// ListOpenAll returns the open issues of every repository through the isopen/
189+// index: the issues it names are read, no other.
190+//
191+//hexcheck:ignore suspicious-business-logic-in-adapter reads the open issues of all repositories through their index: a storage concern, no policy
192+func (i *Issues) ListOpenAll(ctx context.Context) ([]domain.Issue, error) {
193+ type ref struct {
194+ repo domain.RepoName
195+ n int64
196+ }
197+ var refs []ref
198+ for key := range i.db.Snapshot(ctx).Prefix(ctx, []byte(issueOpenPrefix)) {
199+ if err := ctx.Err(); err != nil {
200+ return nil, err
201+ }
202+ repoName, num, ok := strings.Cut(strings.TrimPrefix(string(key), issueOpenPrefix), "/")
203+ if !ok {
204+ return nil, fmt.Errorf("index key %q", key)
205+ }
206+ repo, err := domain.ParseRepoName(repoName)
207+ if err != nil {
208+ return nil, fmt.Errorf("index key %q: %w", key, err)
209+ }
210+ n, err := strconv.ParseInt(num, 10, 64)
211+ if err != nil {
212+ return nil, fmt.Errorf("index key %q: %w", key, err)
213+ }
214+ refs = append(refs, ref{repo, n})
215+ }
216+ var out []domain.Issue
217+ for _, r := range refs {
218+ issue, err := i.Get(ctx, r.repo, r.n)
219+ if errors.Is(err, domain.ErrNotFound) {
220+ continue // closed or deleted since the snapshot of the index
221+ }
222+ if err != nil {
223+ return nil, err
224+ }
225+ if issue.IsOpen() {
226+ out = append(out, issue)
227+ }
228+ }
229+ return out, nil
230+}
231+
188232 // Update replaces the issue with a version precondition and moves its index key
189233 // in the same batch. The repository must exist (see guardRepoKey), so that a
190234 // concurrent delete of the repository leaves no index key behind.
@@ -185,6 +185,50 @@ func (i *Issues) ListOpen(ctx context.Context, repo domain.RepoName) ([]domain.I
185 return out, nil185 return out, nil
186 }186 }
187 187
188+// ListOpenAll returns the open issues of every repository through the isopen/
189+// index: the issues it names are read, no other.
190+//
191+//hexcheck:ignore suspicious-business-logic-in-adapter reads the open issues of all repositories through their index: a storage concern, no policy
192+func (i *Issues) ListOpenAll(ctx context.Context) ([]domain.Issue, error) {
193+ type ref struct {
194+ repo domain.RepoName
195+ n int64
196+ }
197+ var refs []ref
198+ for key := range i.db.Snapshot(ctx).Prefix(ctx, []byte(issueOpenPrefix)) {
199+ if err := ctx.Err(); err != nil {
200+ return nil, err
201+ }
202+ repoName, num, ok := strings.Cut(strings.TrimPrefix(string(key), issueOpenPrefix), "/")
203+ if !ok {
204+ return nil, fmt.Errorf("index key %q", key)
205+ }
206+ repo, err := domain.ParseRepoName(repoName)
207+ if err != nil {
208+ return nil, fmt.Errorf("index key %q: %w", key, err)
209+ }
210+ n, err := strconv.ParseInt(num, 10, 64)
211+ if err != nil {
212+ return nil, fmt.Errorf("index key %q: %w", key, err)
213+ }
214+ refs = append(refs, ref{repo, n})
215+ }
216+ var out []domain.Issue
217+ for _, r := range refs {
218+ issue, err := i.Get(ctx, r.repo, r.n)
219+ if errors.Is(err, domain.ErrNotFound) {
220+ continue // closed or deleted since the snapshot of the index
221+ }
222+ if err != nil {
223+ return nil, err
224+ }
225+ if issue.IsOpen() {
226+ out = append(out, issue)
227+ }
228+ }
229+ return out, nil
230+}
231+
188 // Update replaces the issue with a version precondition and moves its index key232 // Update replaces the issue with a version precondition and moves its index key
189 // in the same batch. The repository must exist (see guardRepoKey), so that a233 // in the same batch. The repository must exist (see guardRepoKey), so that a
190 // concurrent delete of the repository leaves no index key behind.234 // concurrent delete of the repository leaves no index key behind.
M internal/adapters/kvstore/issues_test.go +57 -0
@@ -4,6 +4,7 @@ package kvstore_test
44
55 import (
66 "context"
7+ "strconv"
78 "sync"
89 "testing"
910
@@ -368,3 +369,59 @@ func TestDeleteVersusReopenLeavesNoIssueKeys(t *testing.T) {
368369 assert.ErrorIs(t, is.Update(ctx, closed), domain.ErrNotFound)
369370 }
370371 }
372+
373+func TestListOpenAllSpansRepositoriesThroughTheIndex(t *testing.T) {
374+ ctx := context.Background()
375+ s, mrs, n := repoWithMRs(t)
376+ other := name(t, "other")
377+ require.NoError(t, s.Create(ctx, domain.Repo{Name: other, CreatedAt: created, Visibility: domain.VisibilityPublic}))
378+ is := s.Issues()
379+
380+ i1, err := is.Create(ctx, newIssue(t, n, "one"))
381+ require.NoError(t, err)
382+ i2, err := is.Create(ctx, newIssue(t, other, "two"))
383+ require.NoError(t, err)
384+ i3, err := is.Create(ctx, newIssue(t, other, "three"))
385+ require.NoError(t, err)
386+ i3.State = domain.IssueStateClosed
387+ require.NoError(t, is.Update(ctx, i3))
388+ _, err = mrs.Create(ctx, newMR(t, n, "mr1"))
389+ require.NoError(t, err)
390+ m2, err := mrs.Create(ctx, newMR(t, other, "mr2"))
391+ require.NoError(t, err)
392+ m3, err := mrs.Create(ctx, newMR(t, other, "mr3"))
393+ require.NoError(t, err)
394+ m3.State = domain.MRStateClosed
395+ require.NoError(t, mrs.Update(ctx, m3))
396+
397+ issues, err := is.ListOpenAll(ctx)
398+ require.NoError(t, err)
399+ var got []string
400+ for _, i := range issues {
401+ got = append(got, i.Repo.String()+"#"+strconv.FormatInt(i.Number, 10))
402+ }
403+ assert.ElementsMatch(t, []string{
404+ i1.Repo.String() + "#" + strconv.FormatInt(i1.Number, 10),
405+ i2.Repo.String() + "#" + strconv.FormatInt(i2.Number, 10),
406+ }, got)
407+
408+ open, err := mrs.ListOpenAll(ctx)
409+ require.NoError(t, err)
410+ got = nil
411+ for _, m := range open {
412+ got = append(got, m.Repo.String()+"#"+strconv.FormatInt(m.Number, 10))
413+ }
414+ assert.ElementsMatch(t, []string{"proj#2", m2.Repo.String() + "#" + strconv.FormatInt(m2.Number, 10)}, got)
415+ assert.Len(t, open, 2)
416+}
417+
418+func TestListOpenAllIsEmptyWithoutOpenItems(t *testing.T) {
419+ ctx := context.Background()
420+ s, mrs, _ := repoWithMRs(t)
421+ issues, err := s.Issues().ListOpenAll(ctx)
422+ require.NoError(t, err)
423+ assert.Empty(t, issues)
424+ open, err := mrs.ListOpenAll(ctx)
425+ require.NoError(t, err)
426+ assert.Empty(t, open)
427+}
@@ -4,6 +4,7 @@ package kvstore_test
4 4
5 import (5 import (
6 "context"6 "context"
7+ "strconv"
7 "sync"8 "sync"
8 "testing"9 "testing"
9 10
@@ -368,3 +369,59 @@ func TestDeleteVersusReopenLeavesNoIssueKeys(t *testing.T) {
368 assert.ErrorIs(t, is.Update(ctx, closed), domain.ErrNotFound)369 assert.ErrorIs(t, is.Update(ctx, closed), domain.ErrNotFound)
369 }370 }
370 }371 }
372+
373+func TestListOpenAllSpansRepositoriesThroughTheIndex(t *testing.T) {
374+ ctx := context.Background()
375+ s, mrs, n := repoWithMRs(t)
376+ other := name(t, "other")
377+ require.NoError(t, s.Create(ctx, domain.Repo{Name: other, CreatedAt: created, Visibility: domain.VisibilityPublic}))
378+ is := s.Issues()
379+
380+ i1, err := is.Create(ctx, newIssue(t, n, "one"))
381+ require.NoError(t, err)
382+ i2, err := is.Create(ctx, newIssue(t, other, "two"))
383+ require.NoError(t, err)
384+ i3, err := is.Create(ctx, newIssue(t, other, "three"))
385+ require.NoError(t, err)
386+ i3.State = domain.IssueStateClosed
387+ require.NoError(t, is.Update(ctx, i3))
388+ _, err = mrs.Create(ctx, newMR(t, n, "mr1"))
389+ require.NoError(t, err)
390+ m2, err := mrs.Create(ctx, newMR(t, other, "mr2"))
391+ require.NoError(t, err)
392+ m3, err := mrs.Create(ctx, newMR(t, other, "mr3"))
393+ require.NoError(t, err)
394+ m3.State = domain.MRStateClosed
395+ require.NoError(t, mrs.Update(ctx, m3))
396+
397+ issues, err := is.ListOpenAll(ctx)
398+ require.NoError(t, err)
399+ var got []string
400+ for _, i := range issues {
401+ got = append(got, i.Repo.String()+"#"+strconv.FormatInt(i.Number, 10))
402+ }
403+ assert.ElementsMatch(t, []string{
404+ i1.Repo.String() + "#" + strconv.FormatInt(i1.Number, 10),
405+ i2.Repo.String() + "#" + strconv.FormatInt(i2.Number, 10),
406+ }, got)
407+
408+ open, err := mrs.ListOpenAll(ctx)
409+ require.NoError(t, err)
410+ got = nil
411+ for _, m := range open {
412+ got = append(got, m.Repo.String()+"#"+strconv.FormatInt(m.Number, 10))
413+ }
414+ assert.ElementsMatch(t, []string{"proj#2", m2.Repo.String() + "#" + strconv.FormatInt(m2.Number, 10)}, got)
415+ assert.Len(t, open, 2)
416+}
417+
418+func TestListOpenAllIsEmptyWithoutOpenItems(t *testing.T) {
419+ ctx := context.Background()
420+ s, mrs, _ := repoWithMRs(t)
421+ issues, err := s.Issues().ListOpenAll(ctx)
422+ require.NoError(t, err)
423+ assert.Empty(t, issues)
424+ open, err := mrs.ListOpenAll(ctx)
425+ require.NoError(t, err)
426+ assert.Empty(t, open)
427+}
M internal/adapters/kvstore/mergerequests.go +46 -0
@@ -380,6 +380,52 @@ func (m *MergeRequests) ListOpen(ctx context.Context, repo domain.RepoName) ([]d
380380 return out, nil
381381 }
382382
383+// ListOpenAll returns the open merge requests of every repository, repository
384+// by repository and highest number first, through the mropen/ index: the
385+// requests it names are read, no other.
386+//
387+//hexcheck:ignore suspicious-business-logic-in-adapter reads the open merge requests of all repositories through their index: a storage concern, no policy
388+func (m *MergeRequests) ListOpenAll(ctx context.Context) ([]domain.MergeRequest, error) {
389+ type ref struct {
390+ repo domain.RepoName
391+ n int64
392+ }
393+ var refs []ref
394+ for key := range m.db.Snapshot(ctx).Prefix(ctx, []byte(mrOpenPrefix)) {
395+ if err := ctx.Err(); err != nil {
396+ return nil, err
397+ }
398+ rest := strings.TrimPrefix(string(key), mrOpenPrefix)
399+ repoName, _, ok := strings.Cut(rest, "/")
400+ if !ok {
401+ return nil, fmt.Errorf("index key %q", key)
402+ }
403+ repo, err := domain.ParseRepoName(repoName)
404+ if err != nil {
405+ return nil, fmt.Errorf("index key %q: %w", key, err)
406+ }
407+ n, err := strconv.ParseInt(rest[strings.LastIndexByte(rest, '/')+1:], 10, 64)
408+ if err != nil {
409+ return nil, fmt.Errorf("index key %q: %w", key, err)
410+ }
411+ refs = append(refs, ref{repo, n})
412+ }
413+ var out []domain.MergeRequest
414+ for _, r := range refs {
415+ mr, err := m.Get(ctx, r.repo, r.n)
416+ if errors.Is(err, domain.ErrNotFound) {
417+ continue // closed or deleted since the snapshot of the index
418+ }
419+ if err != nil {
420+ return nil, err
421+ }
422+ if mr.IsOpen() {
423+ out = append(out, mr)
424+ }
425+ }
426+ return out, nil
427+}
428+
383429 // CountOpen counts the open merge requests of the repository from the keys of
384430 // the index: no request is decoded.
385431 func (m *MergeRequests) CountOpen(ctx context.Context, repo domain.RepoName) (int, error) {
@@ -380,6 +380,52 @@ func (m *MergeRequests) ListOpen(ctx context.Context, repo domain.RepoName) ([]d
380 return out, nil380 return out, nil
381 }381 }
382 382
383+// ListOpenAll returns the open merge requests of every repository, repository
384+// by repository and highest number first, through the mropen/ index: the
385+// requests it names are read, no other.
386+//
387+//hexcheck:ignore suspicious-business-logic-in-adapter reads the open merge requests of all repositories through their index: a storage concern, no policy
388+func (m *MergeRequests) ListOpenAll(ctx context.Context) ([]domain.MergeRequest, error) {
389+ type ref struct {
390+ repo domain.RepoName
391+ n int64
392+ }
393+ var refs []ref
394+ for key := range m.db.Snapshot(ctx).Prefix(ctx, []byte(mrOpenPrefix)) {
395+ if err := ctx.Err(); err != nil {
396+ return nil, err
397+ }
398+ rest := strings.TrimPrefix(string(key), mrOpenPrefix)
399+ repoName, _, ok := strings.Cut(rest, "/")
400+ if !ok {
401+ return nil, fmt.Errorf("index key %q", key)
402+ }
403+ repo, err := domain.ParseRepoName(repoName)
404+ if err != nil {
405+ return nil, fmt.Errorf("index key %q: %w", key, err)
406+ }
407+ n, err := strconv.ParseInt(rest[strings.LastIndexByte(rest, '/')+1:], 10, 64)
408+ if err != nil {
409+ return nil, fmt.Errorf("index key %q: %w", key, err)
410+ }
411+ refs = append(refs, ref{repo, n})
412+ }
413+ var out []domain.MergeRequest
414+ for _, r := range refs {
415+ mr, err := m.Get(ctx, r.repo, r.n)
416+ if errors.Is(err, domain.ErrNotFound) {
417+ continue // closed or deleted since the snapshot of the index
418+ }
419+ if err != nil {
420+ return nil, err
421+ }
422+ if mr.IsOpen() {
423+ out = append(out, mr)
424+ }
425+ }
426+ return out, nil
427+}
428+
383 // CountOpen counts the open merge requests of the repository from the keys of429 // CountOpen counts the open merge requests of the repository from the keys of
384 // the index: no request is decoded.430 // the index: no request is decoded.
385 func (m *MergeRequests) CountOpen(ctx context.Context, repo domain.RepoName) (int, error) {431 func (m *MergeRequests) CountOpen(ctx context.Context, repo domain.RepoName) (int, error) {
M internal/ports/mocks/IssueStore.go +62 -0
@@ -400,6 +400,68 @@ func (_c *IssueStore_ListOpen_Call) RunAndReturn(run func(ctx context.Context, r
400400 return _c
401401 }
402402
403+// ListOpenAll provides a mock function for the type IssueStore
404+func (_mock *IssueStore) ListOpenAll(ctx context.Context) ([]domain.Issue, error) {
405+ ret := _mock.Called(ctx)
406+
407+ if len(ret) == 0 {
408+ panic("no return value specified for ListOpenAll")
409+ }
410+
411+ var r0 []domain.Issue
412+ var r1 error
413+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]domain.Issue, error)); ok {
414+ return returnFunc(ctx)
415+ }
416+ if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.Issue); ok {
417+ r0 = returnFunc(ctx)
418+ } else {
419+ if ret.Get(0) != nil {
420+ r0 = ret.Get(0).([]domain.Issue)
421+ }
422+ }
423+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
424+ r1 = returnFunc(ctx)
425+ } else {
426+ r1 = ret.Error(1)
427+ }
428+ return r0, r1
429+}
430+
431+// IssueStore_ListOpenAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListOpenAll'
432+type IssueStore_ListOpenAll_Call struct {
433+ *mock.Call
434+}
435+
436+// ListOpenAll is a helper method to define mock.On call
437+// - ctx context.Context
438+func (_e *IssueStore_Expecter) ListOpenAll(ctx any) *IssueStore_ListOpenAll_Call {
439+ return &IssueStore_ListOpenAll_Call{Call: _e.mock.On("ListOpenAll", ctx)}
440+}
441+
442+func (_c *IssueStore_ListOpenAll_Call) Run(run func(ctx context.Context)) *IssueStore_ListOpenAll_Call {
443+ _c.Call.Run(func(args mock.Arguments) {
444+ var arg0 context.Context
445+ if args[0] != nil {
446+ arg0 = args[0].(context.Context)
447+ }
448+ run(
449+ arg0,
450+ )
451+ })
452+ return _c
453+}
454+
455+func (_c *IssueStore_ListOpenAll_Call) Return(issues []domain.Issue, err error) *IssueStore_ListOpenAll_Call {
456+ _c.Call.Return(issues, err)
457+ return _c
458+}
459+
460+func (_c *IssueStore_ListOpenAll_Call) RunAndReturn(run func(ctx context.Context) ([]domain.Issue, error)) *IssueStore_ListOpenAll_Call {
461+ _c.Call.Return(run)
462+ return _c
463+}
464+
403465 // SaveComment provides a mock function for the type IssueStore
404466 func (_mock *IssueStore) SaveComment(ctx context.Context, issue domain.Issue, c domain.IssueComment) error {
405467 ret := _mock.Called(ctx, issue, c)
@@ -400,6 +400,68 @@ func (_c *IssueStore_ListOpen_Call) RunAndReturn(run func(ctx context.Context, r
400 return _c400 return _c
401 }401 }
402 402
403+// ListOpenAll provides a mock function for the type IssueStore
404+func (_mock *IssueStore) ListOpenAll(ctx context.Context) ([]domain.Issue, error) {
405+ ret := _mock.Called(ctx)
406+
407+ if len(ret) == 0 {
408+ panic("no return value specified for ListOpenAll")
409+ }
410+
411+ var r0 []domain.Issue
412+ var r1 error
413+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]domain.Issue, error)); ok {
414+ return returnFunc(ctx)
415+ }
416+ if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.Issue); ok {
417+ r0 = returnFunc(ctx)
418+ } else {
419+ if ret.Get(0) != nil {
420+ r0 = ret.Get(0).([]domain.Issue)
421+ }
422+ }
423+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
424+ r1 = returnFunc(ctx)
425+ } else {
426+ r1 = ret.Error(1)
427+ }
428+ return r0, r1
429+}
430+
431+// IssueStore_ListOpenAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListOpenAll'
432+type IssueStore_ListOpenAll_Call struct {
433+ *mock.Call
434+}
435+
436+// ListOpenAll is a helper method to define mock.On call
437+// - ctx context.Context
438+func (_e *IssueStore_Expecter) ListOpenAll(ctx any) *IssueStore_ListOpenAll_Call {
439+ return &IssueStore_ListOpenAll_Call{Call: _e.mock.On("ListOpenAll", ctx)}
440+}
441+
442+func (_c *IssueStore_ListOpenAll_Call) Run(run func(ctx context.Context)) *IssueStore_ListOpenAll_Call {
443+ _c.Call.Run(func(args mock.Arguments) {
444+ var arg0 context.Context
445+ if args[0] != nil {
446+ arg0 = args[0].(context.Context)
447+ }
448+ run(
449+ arg0,
450+ )
451+ })
452+ return _c
453+}
454+
455+func (_c *IssueStore_ListOpenAll_Call) Return(issues []domain.Issue, err error) *IssueStore_ListOpenAll_Call {
456+ _c.Call.Return(issues, err)
457+ return _c
458+}
459+
460+func (_c *IssueStore_ListOpenAll_Call) RunAndReturn(run func(ctx context.Context) ([]domain.Issue, error)) *IssueStore_ListOpenAll_Call {
461+ _c.Call.Return(run)
462+ return _c
463+}
464+
403 // SaveComment provides a mock function for the type IssueStore465 // SaveComment provides a mock function for the type IssueStore
404 func (_mock *IssueStore) SaveComment(ctx context.Context, issue domain.Issue, c domain.IssueComment) error {466 func (_mock *IssueStore) SaveComment(ctx context.Context, issue domain.Issue, c domain.IssueComment) error {
405 ret := _mock.Called(ctx, issue, c)467 ret := _mock.Called(ctx, issue, c)
M internal/ports/mocks/MergeRequestStore.go +62 -0
@@ -611,6 +611,68 @@ func (_c *MergeRequestStore_ListOpen_Call) RunAndReturn(run func(ctx context.Con
611611 return _c
612612 }
613613
614+// ListOpenAll provides a mock function for the type MergeRequestStore
615+func (_mock *MergeRequestStore) ListOpenAll(ctx context.Context) ([]domain.MergeRequest, error) {
616+ ret := _mock.Called(ctx)
617+
618+ if len(ret) == 0 {
619+ panic("no return value specified for ListOpenAll")
620+ }
621+
622+ var r0 []domain.MergeRequest
623+ var r1 error
624+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]domain.MergeRequest, error)); ok {
625+ return returnFunc(ctx)
626+ }
627+ if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.MergeRequest); ok {
628+ r0 = returnFunc(ctx)
629+ } else {
630+ if ret.Get(0) != nil {
631+ r0 = ret.Get(0).([]domain.MergeRequest)
632+ }
633+ }
634+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
635+ r1 = returnFunc(ctx)
636+ } else {
637+ r1 = ret.Error(1)
638+ }
639+ return r0, r1
640+}
641+
642+// MergeRequestStore_ListOpenAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListOpenAll'
643+type MergeRequestStore_ListOpenAll_Call struct {
644+ *mock.Call
645+}
646+
647+// ListOpenAll is a helper method to define mock.On call
648+// - ctx context.Context
649+func (_e *MergeRequestStore_Expecter) ListOpenAll(ctx any) *MergeRequestStore_ListOpenAll_Call {
650+ return &MergeRequestStore_ListOpenAll_Call{Call: _e.mock.On("ListOpenAll", ctx)}
651+}
652+
653+func (_c *MergeRequestStore_ListOpenAll_Call) Run(run func(ctx context.Context)) *MergeRequestStore_ListOpenAll_Call {
654+ _c.Call.Run(func(args mock.Arguments) {
655+ var arg0 context.Context
656+ if args[0] != nil {
657+ arg0 = args[0].(context.Context)
658+ }
659+ run(
660+ arg0,
661+ )
662+ })
663+ return _c
664+}
665+
666+func (_c *MergeRequestStore_ListOpenAll_Call) Return(mergeRequests []domain.MergeRequest, err error) *MergeRequestStore_ListOpenAll_Call {
667+ _c.Call.Return(mergeRequests, err)
668+ return _c
669+}
670+
671+func (_c *MergeRequestStore_ListOpenAll_Call) RunAndReturn(run func(ctx context.Context) ([]domain.MergeRequest, error)) *MergeRequestStore_ListOpenAll_Call {
672+ _c.Call.Return(run)
673+ return _c
674+}
675+
614676 // OpenBySource provides a mock function for the type MergeRequestStore
615677 func (_mock *MergeRequestStore) OpenBySource(ctx context.Context, repo domain.RepoName, branches []string, limit int) ([]int64, bool, error) {
616678 ret := _mock.Called(ctx, repo, branches, limit)
@@ -611,6 +611,68 @@ func (_c *MergeRequestStore_ListOpen_Call) RunAndReturn(run func(ctx context.Con
611 return _c611 return _c
612 }612 }
613 613
614+// ListOpenAll provides a mock function for the type MergeRequestStore
615+func (_mock *MergeRequestStore) ListOpenAll(ctx context.Context) ([]domain.MergeRequest, error) {
616+ ret := _mock.Called(ctx)
617+
618+ if len(ret) == 0 {
619+ panic("no return value specified for ListOpenAll")
620+ }
621+
622+ var r0 []domain.MergeRequest
623+ var r1 error
624+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]domain.MergeRequest, error)); ok {
625+ return returnFunc(ctx)
626+ }
627+ if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.MergeRequest); ok {
628+ r0 = returnFunc(ctx)
629+ } else {
630+ if ret.Get(0) != nil {
631+ r0 = ret.Get(0).([]domain.MergeRequest)
632+ }
633+ }
634+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
635+ r1 = returnFunc(ctx)
636+ } else {
637+ r1 = ret.Error(1)
638+ }
639+ return r0, r1
640+}
641+
642+// MergeRequestStore_ListOpenAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListOpenAll'
643+type MergeRequestStore_ListOpenAll_Call struct {
644+ *mock.Call
645+}
646+
647+// ListOpenAll is a helper method to define mock.On call
648+// - ctx context.Context
649+func (_e *MergeRequestStore_Expecter) ListOpenAll(ctx any) *MergeRequestStore_ListOpenAll_Call {
650+ return &MergeRequestStore_ListOpenAll_Call{Call: _e.mock.On("ListOpenAll", ctx)}
651+}
652+
653+func (_c *MergeRequestStore_ListOpenAll_Call) Run(run func(ctx context.Context)) *MergeRequestStore_ListOpenAll_Call {
654+ _c.Call.Run(func(args mock.Arguments) {
655+ var arg0 context.Context
656+ if args[0] != nil {
657+ arg0 = args[0].(context.Context)
658+ }
659+ run(
660+ arg0,
661+ )
662+ })
663+ return _c
664+}
665+
666+func (_c *MergeRequestStore_ListOpenAll_Call) Return(mergeRequests []domain.MergeRequest, err error) *MergeRequestStore_ListOpenAll_Call {
667+ _c.Call.Return(mergeRequests, err)
668+ return _c
669+}
670+
671+func (_c *MergeRequestStore_ListOpenAll_Call) RunAndReturn(run func(ctx context.Context) ([]domain.MergeRequest, error)) *MergeRequestStore_ListOpenAll_Call {
672+ _c.Call.Return(run)
673+ return _c
674+}
675+
614 // OpenBySource provides a mock function for the type MergeRequestStore676 // OpenBySource provides a mock function for the type MergeRequestStore
615 func (_mock *MergeRequestStore) OpenBySource(ctx context.Context, repo domain.RepoName, branches []string, limit int) ([]int64, bool, error) {677 func (_mock *MergeRequestStore) OpenBySource(ctx context.Context, repo domain.RepoName, branches []string, limit int) ([]int64, bool, error) {
616 ret := _mock.Called(ctx, repo, branches, limit)678 ret := _mock.Called(ctx, repo, branches, limit)
M internal/ports/ports.go +7 -0
@@ -261,6 +261,9 @@ type IssueStore interface {
261261 // ListOpen returns the open issues of the repository, highest number
262262 // first, reading only the open ones (through an index).
263263 ListOpen(ctx context.Context, repo domain.RepoName) ([]domain.Issue, error)
264+ // ListOpenAll returns the open issues of every repository, reading only
265+ // the open ones (through the same index). The order is unspecified.
266+ ListOpenAll(ctx context.Context) ([]domain.Issue, error)
264267 // Update replaces the stored issue and keeps the index of the open issues
265268 // in step. It returns domain.ErrNotFound if there is none and
266269 // domain.ErrConflict if another write changed it since issue was read.
@@ -294,6 +297,10 @@ type MergeRequestStore interface {
294297 // ListOpen returns the open merge requests of the repository, highest
295298 // number first, reading only the open ones (through an index).
296299 ListOpen(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)
300+ // ListOpenAll returns the open merge requests of every repository,
301+ // reading only the open ones (through the same index). The order is
302+ // unspecified.
303+ ListOpenAll(ctx context.Context) ([]domain.MergeRequest, error)
297304 // CountOpen returns the number of open merge requests of the repository
298305 // from the index of keys: no request is read.
299306 CountOpen(ctx context.Context, repo domain.RepoName) (int, error)
@@ -261,6 +261,9 @@ type IssueStore interface {
261 // ListOpen returns the open issues of the repository, highest number261 // ListOpen returns the open issues of the repository, highest number
262 // first, reading only the open ones (through an index).262 // first, reading only the open ones (through an index).
263 ListOpen(ctx context.Context, repo domain.RepoName) ([]domain.Issue, error)263 ListOpen(ctx context.Context, repo domain.RepoName) ([]domain.Issue, error)
264+ // ListOpenAll returns the open issues of every repository, reading only
265+ // the open ones (through the same index). The order is unspecified.
266+ ListOpenAll(ctx context.Context) ([]domain.Issue, error)
264 // Update replaces the stored issue and keeps the index of the open issues267 // Update replaces the stored issue and keeps the index of the open issues
265 // in step. It returns domain.ErrNotFound if there is none and268 // in step. It returns domain.ErrNotFound if there is none and
266 // domain.ErrConflict if another write changed it since issue was read.269 // domain.ErrConflict if another write changed it since issue was read.
@@ -294,6 +297,10 @@ type MergeRequestStore interface {
294 // ListOpen returns the open merge requests of the repository, highest297 // ListOpen returns the open merge requests of the repository, highest
295 // number first, reading only the open ones (through an index).298 // number first, reading only the open ones (through an index).
296 ListOpen(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)299 ListOpen(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)
300+ // ListOpenAll returns the open merge requests of every repository,
301+ // reading only the open ones (through the same index). The order is
302+ // unspecified.
303+ ListOpenAll(ctx context.Context) ([]domain.MergeRequest, error)
297 // CountOpen returns the number of open merge requests of the repository304 // CountOpen returns the number of open merge requests of the repository
298 // from the index of keys: no request is read.305 // from the index of keys: no request is read.
299 CountOpen(ctx context.Context, repo domain.RepoName) (int, error)306 CountOpen(ctx context.Context, repo domain.RepoName) (int, error)

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