feat(app): tell the reader when a push or the maintenance changed a repository
- commit
- 5925475298b435d8c1382e5afaff6fbff09677cf
- author
- brice · · 2026-10-11 00:46 UTC
- parent
- dedf508
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +176 -2
M .mockery.yml +1 -0
| @@ -19,3 +19,4 @@ packages: | |||
| 19 | 19 | MergeRequestStore: | |
| 20 | 20 | MergeGit: | |
| 21 | 21 | CommitSigner: | |
| 22 | + | RepoFreshener: | |
| @@ -19,3 +19,4 @@ packages: | |||||
| 19 | MergeRequestStore: | 19 | MergeRequestStore: | ||
| 20 | MergeGit: | 20 | MergeGit: | ||
| 21 | CommitSigner: | 21 | CommitSigner: | ||
| 22 | + | RepoFreshener: | |||
M internal/app/maintenance.go +12 -0
| @@ -27,6 +27,7 @@ const MaintenanceDelay = 5 * time.Second | |||
| 27 | 27 | // (gitcli applies its command timeout to a context without deadline). | |
| 28 | 28 | type Maintainer struct { | |
| 29 | 29 | git ports.GitRepos | |
| 30 | + | fresh ports.RepoFreshener // nil: the reader needs no telling | |
| 30 | 31 | delay time.Duration | |
| 31 | 32 | ctx context.Context | |
| 32 | 33 | stop context.CancelFunc | |
| @@ -53,6 +54,14 @@ func NewMaintainer(ctx context.Context, git ports.GitRepos, delay time.Duration) | |||
| 53 | 54 | return &Maintainer{git: git, delay: delay, ctx: run, stop: stop, entries: make(map[domain.RepoName]*maintEntry)} | |
| 54 | 55 | } | |
| 55 | 56 | ||
| 57 | + | // WithFreshener makes every run tell the reader, when it ends, that the | |
| 58 | + | // repository changed: the commit-graph it wrote is for the reader to pick up. | |
| 59 | + | // Call it before the first Schedule; it returns m. | |
| 60 | + | func (m *Maintainer) WithFreshener(f ports.RepoFreshener) *Maintainer { | |
| 61 | + | m.fresh = f | |
| 62 | + | return m | |
| 63 | + | } | |
| 64 | + | ||
| 56 | 65 | // Schedule queues maintenance of the repository. | |
| 57 | 66 | func (m *Maintainer) Schedule(ctx context.Context, name domain.RepoName) { | |
| 58 | 67 | m.mu.Lock() | |
| @@ -98,6 +107,9 @@ func (m *Maintainer) run(name domain.RepoName, e *maintEntry) { | |||
| 98 | 107 | } else { | |
| 99 | 108 | log.Debug().Str("repo", name.String()).Dur("duration", time.Since(start)).Msg("repository maintained") | |
| 100 | 109 | } | |
| 110 | + | if m.fresh != nil { | |
| 111 | + | m.fresh.Refresh(m.ctx, name) // whatever the outcome: git may have written something | |
| 112 | + | } | |
| 101 | 113 | ||
| 102 | 114 | m.mu.Lock() | |
| 103 | 115 | defer m.mu.Unlock() | |
| @@ -27,6 +27,7 @@ const MaintenanceDelay = 5 * time.Second | |||||
| 27 | // (gitcli applies its command timeout to a context without deadline). | 27 | // (gitcli applies its command timeout to a context without deadline). | ||
| 28 | type Maintainer struct { | 28 | type Maintainer struct { | ||
| 29 | git ports.GitRepos | 29 | git ports.GitRepos | ||
| 30 | + | fresh ports.RepoFreshener // nil: the reader needs no telling | |||
| 30 | delay time.Duration | 31 | delay time.Duration | ||
| 31 | ctx context.Context | 32 | ctx context.Context | ||
| 32 | stop context.CancelFunc | 33 | stop context.CancelFunc | ||
| @@ -53,6 +54,14 @@ func NewMaintainer(ctx context.Context, git ports.GitRepos, delay time.Duration) | |||||
| 53 | return &Maintainer{git: git, delay: delay, ctx: run, stop: stop, entries: make(map[domain.RepoName]*maintEntry)} | 54 | return &Maintainer{git: git, delay: delay, ctx: run, stop: stop, entries: make(map[domain.RepoName]*maintEntry)} | ||
| 54 | } | 55 | } | ||
| 55 | 56 | ||||
| 57 | + | // WithFreshener makes every run tell the reader, when it ends, that the | |||
| 58 | + | // repository changed: the commit-graph it wrote is for the reader to pick up. | |||
| 59 | + | // Call it before the first Schedule; it returns m. | |||
| 60 | + | func (m *Maintainer) WithFreshener(f ports.RepoFreshener) *Maintainer { | |||
| 61 | + | m.fresh = f | |||
| 62 | + | return m | |||
| 63 | + | } | |||
| 64 | + | ||||
| 56 | // Schedule queues maintenance of the repository. | 65 | // Schedule queues maintenance of the repository. | ||
| 57 | func (m *Maintainer) Schedule(ctx context.Context, name domain.RepoName) { | 66 | func (m *Maintainer) Schedule(ctx context.Context, name domain.RepoName) { | ||
| 58 | m.mu.Lock() | 67 | m.mu.Lock() | ||
| @@ -98,6 +107,9 @@ func (m *Maintainer) run(name domain.RepoName, e *maintEntry) { | |||||
| 98 | } else { | 107 | } else { | ||
| 99 | log.Debug().Str("repo", name.String()).Dur("duration", time.Since(start)).Msg("repository maintained") | 108 | log.Debug().Str("repo", name.String()).Dur("duration", time.Since(start)).Msg("repository maintained") | ||
| 100 | } | 109 | } | ||
| 110 | + | if m.fresh != nil { | |||
| 111 | + | m.fresh.Refresh(m.ctx, name) // whatever the outcome: git may have written something | |||
| 112 | + | } | |||
| 101 | 113 | ||||
| 102 | m.mu.Lock() | 114 | m.mu.Lock() | ||
| 103 | defer m.mu.Unlock() | 115 | defer m.mu.Unlock() | ||
M internal/app/maintenance_test.go +15 -0
| @@ -97,6 +97,21 @@ func TestMaintainerOneRunAtATimePerRepo(t *testing.T) { | |||
| 97 | 97 | assert.Equal(t, int32(1), maxConcurrent.Load()) | |
| 98 | 98 | } | |
| 99 | 99 | ||
| 100 | + | func TestMaintainerTellsTheReaderAfterARun(t *testing.T) { | |
| 101 | + | ctx := context.Background() | |
| 102 | + | n := name(t, "tools") | |
| 103 | + | for _, failure := range []error{nil, errors.New("boom")} { | |
| 104 | + | git := mocks.NewGitRepos(t) | |
| 105 | + | git.EXPECT().Maintain(mockAny, n).Return(failure) | |
| 106 | + | fresh := &freshener{} | |
| 107 | + | m := app.NewMaintainer(ctx, git, time.Millisecond).WithFreshener(fresh) | |
| 108 | + | m.Schedule(ctx, n) | |
| 109 | + | waitFor(t, func() bool { return len(fresh.seen()) == 1 }, "the reader to be told") | |
| 110 | + | m.Close(ctx) | |
| 111 | + | assert.Equal(t, []domain.RepoName{n}, fresh.seen()) | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | ||
| 100 | 115 | func TestMaintainerFailureIsLoggedNotFatal(t *testing.T) { | |
| 101 | 116 | ctx := context.Background() | |
| 102 | 117 | n := name(t, "tools") | |
| @@ -97,6 +97,21 @@ func TestMaintainerOneRunAtATimePerRepo(t *testing.T) { | |||||
| 97 | assert.Equal(t, int32(1), maxConcurrent.Load()) | 97 | assert.Equal(t, int32(1), maxConcurrent.Load()) | ||
| 98 | } | 98 | } | ||
| 99 | 99 | ||||
| 100 | + | func TestMaintainerTellsTheReaderAfterARun(t *testing.T) { | |||
| 101 | + | ctx := context.Background() | |||
| 102 | + | n := name(t, "tools") | |||
| 103 | + | for _, failure := range []error{nil, errors.New("boom")} { | |||
| 104 | + | git := mocks.NewGitRepos(t) | |||
| 105 | + | git.EXPECT().Maintain(mockAny, n).Return(failure) | |||
| 106 | + | fresh := &freshener{} | |||
| 107 | + | m := app.NewMaintainer(ctx, git, time.Millisecond).WithFreshener(fresh) | |||
| 108 | + | m.Schedule(ctx, n) | |||
| 109 | + | waitFor(t, func() bool { return len(fresh.seen()) == 1 }, "the reader to be told") | |||
| 110 | + | m.Close(ctx) | |||
| 111 | + | assert.Equal(t, []domain.RepoName{n}, fresh.seen()) | |||
| 112 | + | } | |||
| 113 | + | } | |||
| 114 | + | ||||
| 100 | func TestMaintainerFailureIsLoggedNotFatal(t *testing.T) { | 115 | func TestMaintainerFailureIsLoggedNotFatal(t *testing.T) { | ||
| 101 | ctx := context.Background() | 116 | ctx := context.Background() | ||
| 102 | n := name(t, "tools") | 117 | n := name(t, "tools") | ||
M internal/app/pushes.go +13 -2
| @@ -38,6 +38,7 @@ type Pushes struct { | |||
| 38 | 38 | clock ports.Clock | |
| 39 | 39 | maint MaintenanceScheduler | |
| 40 | 40 | auth Revalidator | |
| 41 | + | fresh ports.RepoFreshener // nil: the reader needs no telling | |
| 41 | 42 | } | |
| 42 | 43 | ||
| 43 | 44 | // NewPushes builds the push use cases. Every argument is required: auth | |
| @@ -47,6 +48,13 @@ func NewPushes(meta ports.RepoMetaStore, repos *Repos, git ports.GitRepos, bus p | |||
| 47 | 48 | return &Pushes{meta: meta, repos: repos, git: git, bus: bus, clock: clock, maint: maint, auth: auth} | |
| 48 | 49 | } | |
| 49 | 50 | ||
| 51 | + | // WithFreshener makes Record tell the reader that a repository changed, so that | |
| 52 | + | // it serves the push at once. Call it before the first push; it returns u. | |
| 53 | + | func (u *Pushes) WithFreshener(f ports.RepoFreshener) *Pushes { | |
| 54 | + | u.fresh = f | |
| 55 | + | return u | |
| 56 | + | } | |
| 57 | + | ||
| 50 | 58 | // Open decides whether the principal may push to the repository and, with | |
| 51 | 59 | // create, creates it when it does not exist yet (private, published as | |
| 52 | 60 | // RepoCreated by Repos.Create). create is for the first request of a push only. | |
| @@ -118,8 +126,8 @@ func (u *Pushes) Check(ctx context.Context, p domain.Principal, name domain.Repo | |||
| 118 | 126 | } | |
| 119 | 127 | ||
| 120 | 128 | // Record is the post-receive step of an accepted push: it sets pushed_at, | |
| 121 | - | // repairs HEAD when it points to a missing branch, publishes a PushEvent and | |
| 122 | - | // schedules maintenance. Every step runs even when an earlier one fails; the | |
| 129 | + | // repairs HEAD when it points to a missing branch, tells the reader that the | |
| 130 | + | // repository changed, publishes a PushEvent and schedules maintenance. Every step runs even when an earlier one fails; the | |
| 123 | 131 | // failures are returned together and the push itself is not affected. | |
| 124 | 132 | func (u *Pushes) Record(ctx context.Context, p domain.Principal, name domain.RepoName, updates []domain.RefUpdate) error { | |
| 125 | 133 | log := zerowrap.FromCtx(ctx) | |
| @@ -133,6 +141,9 @@ func (u *Pushes) Record(ctx context.Context, p domain.Principal, name domain.Rep | |||
| 133 | 141 | if err := u.fixDefaultBranch(ctx, name, updates); err != nil { | |
| 134 | 142 | errs = append(errs, err) | |
| 135 | 143 | } | |
| 144 | + | if u.fresh != nil { | |
| 145 | + | u.fresh.Refresh(ctx, name) | |
| 146 | + | } | |
| 136 | 147 | u.bus.Publish(ctx, domain.PushEvent{ | |
| 137 | 148 | Repo: name, Updates: append([]domain.RefUpdate(nil), updates...), At: now, | |
| 138 | 149 | Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, | |
| @@ -38,6 +38,7 @@ type Pushes struct { | |||||
| 38 | clock ports.Clock | 38 | clock ports.Clock | ||
| 39 | maint MaintenanceScheduler | 39 | maint MaintenanceScheduler | ||
| 40 | auth Revalidator | 40 | auth Revalidator | ||
| 41 | + | fresh ports.RepoFreshener // nil: the reader needs no telling | |||
| 41 | } | 42 | } | ||
| 42 | 43 | ||||
| 43 | // NewPushes builds the push use cases. Every argument is required: auth | 44 | // NewPushes builds the push use cases. Every argument is required: auth | ||
| @@ -47,6 +48,13 @@ func NewPushes(meta ports.RepoMetaStore, repos *Repos, git ports.GitRepos, bus p | |||||
| 47 | return &Pushes{meta: meta, repos: repos, git: git, bus: bus, clock: clock, maint: maint, auth: auth} | 48 | return &Pushes{meta: meta, repos: repos, git: git, bus: bus, clock: clock, maint: maint, auth: auth} | ||
| 48 | } | 49 | } | ||
| 49 | 50 | ||||
| 51 | + | // WithFreshener makes Record tell the reader that a repository changed, so that | |||
| 52 | + | // it serves the push at once. Call it before the first push; it returns u. | |||
| 53 | + | func (u *Pushes) WithFreshener(f ports.RepoFreshener) *Pushes { | |||
| 54 | + | u.fresh = f | |||
| 55 | + | return u | |||
| 56 | + | } | |||
| 57 | + | ||||
| 50 | // Open decides whether the principal may push to the repository and, with | 58 | // Open decides whether the principal may push to the repository and, with | ||
| 51 | // create, creates it when it does not exist yet (private, published as | 59 | // create, creates it when it does not exist yet (private, published as | ||
| 52 | // RepoCreated by Repos.Create). create is for the first request of a push only. | 60 | // RepoCreated by Repos.Create). create is for the first request of a push only. | ||
| @@ -118,8 +126,8 @@ func (u *Pushes) Check(ctx context.Context, p domain.Principal, name domain.Repo | |||||
| 118 | } | 126 | } | ||
| 119 | 127 | ||||
| 120 | // Record is the post-receive step of an accepted push: it sets pushed_at, | 128 | // Record is the post-receive step of an accepted push: it sets pushed_at, | ||
| 121 | - | // repairs HEAD when it points to a missing branch, publishes a PushEvent and | 129 | + | // repairs HEAD when it points to a missing branch, tells the reader that the |
| 122 | - | // schedules maintenance. Every step runs even when an earlier one fails; the | 130 | + | // repository changed, publishes a PushEvent and schedules maintenance. Every step runs even when an earlier one fails; the |
| 123 | // failures are returned together and the push itself is not affected. | 131 | // failures are returned together and the push itself is not affected. | ||
| 124 | func (u *Pushes) Record(ctx context.Context, p domain.Principal, name domain.RepoName, updates []domain.RefUpdate) error { | 132 | func (u *Pushes) Record(ctx context.Context, p domain.Principal, name domain.RepoName, updates []domain.RefUpdate) error { | ||
| 125 | log := zerowrap.FromCtx(ctx) | 133 | log := zerowrap.FromCtx(ctx) | ||
| @@ -133,6 +141,9 @@ func (u *Pushes) Record(ctx context.Context, p domain.Principal, name domain.Rep | |||||
| 133 | if err := u.fixDefaultBranch(ctx, name, updates); err != nil { | 141 | if err := u.fixDefaultBranch(ctx, name, updates); err != nil { | ||
| 134 | errs = append(errs, err) | 142 | errs = append(errs, err) | ||
| 135 | } | 143 | } | ||
| 144 | + | if u.fresh != nil { | |||
| 145 | + | u.fresh.Refresh(ctx, name) | |||
| 146 | + | } | |||
| 136 | u.bus.Publish(ctx, domain.PushEvent{ | 147 | u.bus.Publish(ctx, domain.PushEvent{ | ||
| 137 | Repo: name, Updates: append([]domain.RefUpdate(nil), updates...), At: now, | 148 | Repo: name, Updates: append([]domain.RefUpdate(nil), updates...), At: now, | ||
| 138 | Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, | 149 | Visibility: repo.Visibility, CreatedAt: repo.CreatedAt, | ||
M internal/app/pushes_test.go +30 -0
| @@ -30,6 +30,24 @@ func (s *scheduler) Schedule(_ context.Context, n domain.RepoName) { | |||
| 30 | 30 | s.names = append(s.names, n) | |
| 31 | 31 | } | |
| 32 | 32 | ||
| 33 | + | // freshener records the repositories it was told changed. | |
| 34 | + | type freshener struct { | |
| 35 | + | mu sync.Mutex | |
| 36 | + | names []domain.RepoName | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | func (f *freshener) Refresh(_ context.Context, n domain.RepoName) { | |
| 40 | + | f.mu.Lock() | |
| 41 | + | defer f.mu.Unlock() | |
| 42 | + | f.names = append(f.names, n) | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func (f *freshener) seen() []domain.RepoName { | |
| 46 | + | f.mu.Lock() | |
| 47 | + | defer f.mu.Unlock() | |
| 48 | + | return append([]domain.RepoName(nil), f.names...) | |
| 49 | + | } | |
| 50 | + | ||
| 33 | 51 | type pushFixture struct { | |
| 34 | 52 | meta *mocks.RepoMetaStore | |
| 35 | 53 | git *mocks.GitRepos | |
| @@ -223,6 +241,18 @@ func TestPushesRecord(t *testing.T) { | |||
| 223 | 241 | assert.Equal(t, []domain.RepoName{n}, f.sched.names) | |
| 224 | 242 | }) | |
| 225 | 243 | ||
| 244 | + | t.Run("tells the reader that the repository changed", func(t *testing.T) { | |
| 245 | + | f := newPushFixture(t) | |
| 246 | + | fresh := &freshener{} | |
| 247 | + | f.pushes.WithFreshener(fresh) | |
| 248 | + | f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n}, nil) | |
| 249 | + | f.meta.EXPECT().Update(ctx, mock.Anything).Return(nil) | |
| 250 | + | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | |
| 251 | + | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | |
| 252 | + | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | |
| 253 | + | assert.Equal(t, []domain.RepoName{n}, fresh.seen()) | |
| 254 | + | }) | |
| 255 | + | ||
| 226 | 256 | t.Run("pushed_at retries after a conflict and never goes backwards", func(t *testing.T) { | |
| 227 | 257 | f := newPushFixture(t) | |
| 228 | 258 | repo := domain.Repo{Name: n} | |
| @@ -30,6 +30,24 @@ func (s *scheduler) Schedule(_ context.Context, n domain.RepoName) { | |||||
| 30 | s.names = append(s.names, n) | 30 | s.names = append(s.names, n) | ||
| 31 | } | 31 | } | ||
| 32 | 32 | ||||
| 33 | + | // freshener records the repositories it was told changed. | |||
| 34 | + | type freshener struct { | |||
| 35 | + | mu sync.Mutex | |||
| 36 | + | names []domain.RepoName | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | func (f *freshener) Refresh(_ context.Context, n domain.RepoName) { | |||
| 40 | + | f.mu.Lock() | |||
| 41 | + | defer f.mu.Unlock() | |||
| 42 | + | f.names = append(f.names, n) | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func (f *freshener) seen() []domain.RepoName { | |||
| 46 | + | f.mu.Lock() | |||
| 47 | + | defer f.mu.Unlock() | |||
| 48 | + | return append([]domain.RepoName(nil), f.names...) | |||
| 49 | + | } | |||
| 50 | + | ||||
| 33 | type pushFixture struct { | 51 | type pushFixture struct { | ||
| 34 | meta *mocks.RepoMetaStore | 52 | meta *mocks.RepoMetaStore | ||
| 35 | git *mocks.GitRepos | 53 | git *mocks.GitRepos | ||
| @@ -223,6 +241,18 @@ func TestPushesRecord(t *testing.T) { | |||||
| 223 | assert.Equal(t, []domain.RepoName{n}, f.sched.names) | 241 | assert.Equal(t, []domain.RepoName{n}, f.sched.names) | ||
| 224 | }) | 242 | }) | ||
| 225 | 243 | ||||
| 244 | + | t.Run("tells the reader that the repository changed", func(t *testing.T) { | |||
| 245 | + | f := newPushFixture(t) | |||
| 246 | + | fresh := &freshener{} | |||
| 247 | + | f.pushes.WithFreshener(fresh) | |||
| 248 | + | f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{Name: n}, nil) | |||
| 249 | + | f.meta.EXPECT().Update(ctx, mock.Anything).Return(nil) | |||
| 250 | + | f.git.EXPECT().DefaultBranch(ctx, n).Return("main", true, nil) | |||
| 251 | + | f.bus.EXPECT().Publish(ctx, mock.Anything).Return() | |||
| 252 | + | require.NoError(t, f.pushes.Record(ctx, p, n, ups)) | |||
| 253 | + | assert.Equal(t, []domain.RepoName{n}, fresh.seen()) | |||
| 254 | + | }) | |||
| 255 | + | ||||
| 226 | t.Run("pushed_at retries after a conflict and never goes backwards", func(t *testing.T) { | 256 | t.Run("pushed_at retries after a conflict and never goes backwards", func(t *testing.T) { | ||
| 227 | f := newPushFixture(t) | 257 | f := newPushFixture(t) | ||
| 228 | repo := domain.Repo{Name: n} | 258 | repo := domain.Repo{Name: n} | ||
A internal/ports/mocks/RepoFreshener.go +94 -0
| @@ -0,0 +1,94 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package mocks | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "context" | |
| 9 | + | ||
| 10 | + | "bnema.dev/ermite/internal/domain" | |
| 11 | + | mock "github.com/stretchr/testify/mock" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // NewRepoFreshener creates a new instance of RepoFreshener. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 15 | + | // The first argument is typically a *testing.T value. | |
| 16 | + | func NewRepoFreshener(t interface { | |
| 17 | + | mock.TestingT | |
| 18 | + | Cleanup(func()) | |
| 19 | + | }) *RepoFreshener { | |
| 20 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 21 | + | helper.Helper() | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | mock := &RepoFreshener{} | |
| 25 | + | mock.Mock.Test(t) | |
| 26 | + | ||
| 27 | + | t.Cleanup(func() { | |
| 28 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 29 | + | helper.Helper() | |
| 30 | + | } | |
| 31 | + | mock.AssertExpectations(t) | |
| 32 | + | }) | |
| 33 | + | ||
| 34 | + | return mock | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // RepoFreshener is an autogenerated mock type for the RepoFreshener type | |
| 38 | + | type RepoFreshener struct { | |
| 39 | + | mock.Mock | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | type RepoFreshener_Expecter struct { | |
| 43 | + | mock *mock.Mock | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func (_m *RepoFreshener) EXPECT() *RepoFreshener_Expecter { | |
| 47 | + | return &RepoFreshener_Expecter{mock: &_m.Mock} | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // Refresh provides a mock function for the type RepoFreshener | |
| 51 | + | func (_mock *RepoFreshener) Refresh(ctx context.Context, name domain.RepoName) { | |
| 52 | + | _mock.Called(ctx, name) | |
| 53 | + | return | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | // RepoFreshener_Refresh_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Refresh' | |
| 57 | + | type RepoFreshener_Refresh_Call struct { | |
| 58 | + | *mock.Call | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // Refresh is a helper method to define mock.On call | |
| 62 | + | // - ctx context.Context | |
| 63 | + | // - name domain.RepoName | |
| 64 | + | func (_e *RepoFreshener_Expecter) Refresh(ctx any, name any) *RepoFreshener_Refresh_Call { | |
| 65 | + | return &RepoFreshener_Refresh_Call{Call: _e.mock.On("Refresh", ctx, name)} | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func (_c *RepoFreshener_Refresh_Call) Run(run func(ctx context.Context, name domain.RepoName)) *RepoFreshener_Refresh_Call { | |
| 69 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 70 | + | var arg0 context.Context | |
| 71 | + | if args[0] != nil { | |
| 72 | + | arg0 = args[0].(context.Context) | |
| 73 | + | } | |
| 74 | + | var arg1 domain.RepoName | |
| 75 | + | if args[1] != nil { | |
| 76 | + | arg1 = args[1].(domain.RepoName) | |
| 77 | + | } | |
| 78 | + | run( | |
| 79 | + | arg0, | |
| 80 | + | arg1, | |
| 81 | + | ) | |
| 82 | + | }) | |
| 83 | + | return _c | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | func (_c *RepoFreshener_Refresh_Call) Return() *RepoFreshener_Refresh_Call { | |
| 87 | + | _c.Call.Return() | |
| 88 | + | return _c | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | func (_c *RepoFreshener_Refresh_Call) RunAndReturn(run func(ctx context.Context, name domain.RepoName)) *RepoFreshener_Refresh_Call { | |
| 92 | + | _c.Run(run) | |
| 93 | + | return _c | |
| 94 | + | } | |
| @@ -0,0 +1,94 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package mocks | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | "context" | |||
| 9 | + | ||||
| 10 | + | "bnema.dev/ermite/internal/domain" | |||
| 11 | + | mock "github.com/stretchr/testify/mock" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // NewRepoFreshener creates a new instance of RepoFreshener. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 15 | + | // The first argument is typically a *testing.T value. | |||
| 16 | + | func NewRepoFreshener(t interface { | |||
| 17 | + | mock.TestingT | |||
| 18 | + | Cleanup(func()) | |||
| 19 | + | }) *RepoFreshener { | |||
| 20 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 21 | + | helper.Helper() | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | mock := &RepoFreshener{} | |||
| 25 | + | mock.Mock.Test(t) | |||
| 26 | + | ||||
| 27 | + | t.Cleanup(func() { | |||
| 28 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 29 | + | helper.Helper() | |||
| 30 | + | } | |||
| 31 | + | mock.AssertExpectations(t) | |||
| 32 | + | }) | |||
| 33 | + | ||||
| 34 | + | return mock | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // RepoFreshener is an autogenerated mock type for the RepoFreshener type | |||
| 38 | + | type RepoFreshener struct { | |||
| 39 | + | mock.Mock | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | type RepoFreshener_Expecter struct { | |||
| 43 | + | mock *mock.Mock | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func (_m *RepoFreshener) EXPECT() *RepoFreshener_Expecter { | |||
| 47 | + | return &RepoFreshener_Expecter{mock: &_m.Mock} | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // Refresh provides a mock function for the type RepoFreshener | |||
| 51 | + | func (_mock *RepoFreshener) Refresh(ctx context.Context, name domain.RepoName) { | |||
| 52 | + | _mock.Called(ctx, name) | |||
| 53 | + | return | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | // RepoFreshener_Refresh_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Refresh' | |||
| 57 | + | type RepoFreshener_Refresh_Call struct { | |||
| 58 | + | *mock.Call | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // Refresh is a helper method to define mock.On call | |||
| 62 | + | // - ctx context.Context | |||
| 63 | + | // - name domain.RepoName | |||
| 64 | + | func (_e *RepoFreshener_Expecter) Refresh(ctx any, name any) *RepoFreshener_Refresh_Call { | |||
| 65 | + | return &RepoFreshener_Refresh_Call{Call: _e.mock.On("Refresh", ctx, name)} | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | func (_c *RepoFreshener_Refresh_Call) Run(run func(ctx context.Context, name domain.RepoName)) *RepoFreshener_Refresh_Call { | |||
| 69 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 70 | + | var arg0 context.Context | |||
| 71 | + | if args[0] != nil { | |||
| 72 | + | arg0 = args[0].(context.Context) | |||
| 73 | + | } | |||
| 74 | + | var arg1 domain.RepoName | |||
| 75 | + | if args[1] != nil { | |||
| 76 | + | arg1 = args[1].(domain.RepoName) | |||
| 77 | + | } | |||
| 78 | + | run( | |||
| 79 | + | arg0, | |||
| 80 | + | arg1, | |||
| 81 | + | ) | |||
| 82 | + | }) | |||
| 83 | + | return _c | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | func (_c *RepoFreshener_Refresh_Call) Return() *RepoFreshener_Refresh_Call { | |||
| 87 | + | _c.Call.Return() | |||
| 88 | + | return _c | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | func (_c *RepoFreshener_Refresh_Call) RunAndReturn(run func(ctx context.Context, name domain.RepoName)) *RepoFreshener_Refresh_Call { | |||
| 92 | + | _c.Run(run) | |||
| 93 | + | return _c | |||
| 94 | + | } | |||
M internal/ports/ports.go +11 -0
| @@ -84,6 +84,17 @@ type GitRepos interface { | |||
| 84 | 84 | Maintain(ctx context.Context, name domain.RepoName) error | |
| 85 | 85 | } | |
| 86 | 86 | ||
| 87 | + | // RepoFreshener is implemented by a RepoReader that keeps repositories open | |
| 88 | + | // between reads. The use cases that write to a repository (a push, the | |
| 89 | + | // maintenance that follows it) call Refresh so that the reader picks the change | |
| 90 | + | // up at once instead of when its own timer says so. It is optional: reads | |
| 91 | + | // stay correct without it. | |
| 92 | + | type RepoFreshener interface { | |
| 93 | + | // Refresh tells the reader that the repository changed on disk. It does | |
| 94 | + | // not fail: a reader that cannot refresh finds out on its next read. | |
| 95 | + | Refresh(ctx context.Context, name domain.RepoName) | |
| 96 | + | } | |
| 97 | + | ||
| 87 | 98 | // CommitQuery selects a page of history. | |
| 88 | 99 | type CommitQuery struct { | |
| 89 | 100 | // Revision is where the history starts; the zero value is HEAD. | |
| @@ -84,6 +84,17 @@ type GitRepos interface { | |||||
| 84 | Maintain(ctx context.Context, name domain.RepoName) error | 84 | Maintain(ctx context.Context, name domain.RepoName) error | ||
| 85 | } | 85 | } | ||
| 86 | 86 | ||||
| 87 | + | // RepoFreshener is implemented by a RepoReader that keeps repositories open | |||
| 88 | + | // between reads. The use cases that write to a repository (a push, the | |||
| 89 | + | // maintenance that follows it) call Refresh so that the reader picks the change | |||
| 90 | + | // up at once instead of when its own timer says so. It is optional: reads | |||
| 91 | + | // stay correct without it. | |||
| 92 | + | type RepoFreshener interface { | |||
| 93 | + | // Refresh tells the reader that the repository changed on disk. It does | |||
| 94 | + | // not fail: a reader that cannot refresh finds out on its next read. | |||
| 95 | + | Refresh(ctx context.Context, name domain.RepoName) | |||
| 96 | + | } | |||
| 97 | + | ||||
| 87 | // CommitQuery selects a page of history. | 98 | // CommitQuery selects a page of history. | ||
| 88 | type CommitQuery struct { | 99 | type CommitQuery struct { | ||
| 89 | // Revision is where the history starts; the zero value is HEAD. | 100 | // Revision is where the history starts; the zero value is HEAD. | ||