detached 435618a

feat(domain): add update/delete events, request-scoped principal and gitcli branch helpers

RepoUpdated/RepoDeleted events, ErrInvalid/ErrPreconditionFailed, the
principal carried in ctx, GitRepos.HasBranch/HeadBranch, and Trash drops
the cat-file process of the moved directory.
commit
435618a674c26b75ab493ff007cc18c6de988969
author
brice · · 2026-10-10 09:41 UTC
parent
6e542e5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +258 -0
M internal/adapters/gitcli/catfile.go +9 -0
@@ -273,6 +273,15 @@ func (p *catPool) startLocked(ctx context.Context, dir string) (*catProc, error)
273273 return cp, nil
274274 }
275275
276+// forget kills the process of the repository in dir, if any.
277+func (p *catPool) forget(dir string) {
278+ p.mu.Lock()
279+ defer p.mu.Unlock()
280+ if cp := p.procs[dir]; cp != nil {
281+ p.dropLocked(cp)
282+ }
283+}
284+
276285 // close kills every process and waits, until ctx ends, for them to be
277286 // collected. Later requests fail with ErrClosed.
278287 func (p *catPool) close(ctx context.Context) error {
@@ -273,6 +273,15 @@ func (p *catPool) startLocked(ctx context.Context, dir string) (*catProc, error)
273 return cp, nil273 return cp, nil
274 }274 }
275 275
276+// forget kills the process of the repository in dir, if any.
277+func (p *catPool) forget(dir string) {
278+ p.mu.Lock()
279+ defer p.mu.Unlock()
280+ if cp := p.procs[dir]; cp != nil {
281+ p.dropLocked(cp)
282+ }
283+}
284+
276 // close kills every process and waits, until ctx ends, for them to be285 // close kills every process and waits, until ctx ends, for them to be
277 // collected. Later requests fail with ErrClosed.286 // collected. Later requests fail with ErrClosed.
278 func (p *catPool) close(ctx context.Context) error {287 func (p *catPool) close(ctx context.Context) error {
M internal/adapters/gitcli/repos.go +52 -0
@@ -10,6 +10,7 @@ import (
1010 "fmt"
1111 "io/fs"
1212 "os"
13+ "os/exec"
1314 "path/filepath"
1415 "strings"
1516 "time"
@@ -164,6 +165,9 @@ func (g *Git) Trash(ctx context.Context, name domain.RepoName) error {
164165 if err != nil {
165166 return fmt.Errorf("trash repository %q: %w", name, err)
166167 }
168+ // A cat-file process of the old directory must not serve a repository
169+ // created later under the same name.
170+ g.cat.forget(src)
167171 if err := syncDir(g.reposDir()); err != nil {
168172 return fmt.Errorf("trash repository %q: %w", name, err)
169173 }
@@ -253,6 +257,54 @@ func (g *Git) DefaultBranch(ctx context.Context, name domain.RepoName) (string,
253257 return branch, strings.TrimSpace(out) == ref, nil
254258 }
255259
260+// HasBranch checks refs/heads/<branch> with `git for-each-ref` after
261+// `git check-ref-format`, which rejects the glob characters for-each-ref
262+// would otherwise interpret.
263+//
264+//hexcheck:ignore suspicious-business-logic-in-adapter port method name only
265+func (g *Git) HasBranch(ctx context.Context, name domain.RepoName, branch string) (bool, error) {
266+ dir, err := g.repoDir(name)
267+ if err != nil {
268+ return false, err
269+ }
270+ ref := headsPrefix + branch
271+ if branch == "" || strings.HasPrefix(branch, "-") {
272+ return false, nil
273+ }
274+ if _, err := g.run(ctx, "check-ref-format", ref); err != nil {
275+ var exit *exec.ExitError
276+ if errors.As(err, &exit) {
277+ return false, nil
278+ }
279+ return false, err
280+ }
281+ out, err := g.run(ctx, "-C", dir, "for-each-ref", "--format=%(refname)", ref)
282+ if err != nil {
283+ return false, fmt.Errorf("look up %s in %q: %w", ref, name, err)
284+ }
285+ return strings.TrimSpace(out) == ref, nil
286+}
287+
288+// HeadBranch reads the HEAD file of the bare repository (files ref backend).
289+func (g *Git) HeadBranch(_ context.Context, name domain.RepoName) (string, error) {
290+ dir, err := g.repoDir(name)
291+ if err != nil {
292+ return "", err
293+ }
294+ raw, err := os.ReadFile(filepath.Join(dir, "HEAD"))
295+ if err != nil {
296+ if errors.Is(err, fs.ErrNotExist) {
297+ return "", domain.ErrNotFound
298+ }
299+ return "", fmt.Errorf("read HEAD of %q: %w", name, err)
300+ }
301+ ref, ok := strings.CutPrefix(strings.TrimSpace(string(raw)), "ref: ")
302+ if !ok {
303+ return "", nil // detached
304+ }
305+ return strings.TrimPrefix(ref, headsPrefix), nil
306+}
307+
256308 // SetDefaultBranch runs `git symbolic-ref HEAD refs/heads/<branch>`.
257309 func (g *Git) SetDefaultBranch(ctx context.Context, name domain.RepoName, branch string) error {
258310 dir, err := g.repoDir(name)
@@ -10,6 +10,7 @@ import (
10 "fmt"10 "fmt"
11 "io/fs"11 "io/fs"
12 "os"12 "os"
13+ "os/exec"
13 "path/filepath"14 "path/filepath"
14 "strings"15 "strings"
15 "time"16 "time"
@@ -164,6 +165,9 @@ func (g *Git) Trash(ctx context.Context, name domain.RepoName) error {
164 if err != nil {165 if err != nil {
165 return fmt.Errorf("trash repository %q: %w", name, err)166 return fmt.Errorf("trash repository %q: %w", name, err)
166 }167 }
168+ // A cat-file process of the old directory must not serve a repository
169+ // created later under the same name.
170+ g.cat.forget(src)
167 if err := syncDir(g.reposDir()); err != nil {171 if err := syncDir(g.reposDir()); err != nil {
168 return fmt.Errorf("trash repository %q: %w", name, err)172 return fmt.Errorf("trash repository %q: %w", name, err)
169 }173 }
@@ -253,6 +257,54 @@ func (g *Git) DefaultBranch(ctx context.Context, name domain.RepoName) (string,
253 return branch, strings.TrimSpace(out) == ref, nil257 return branch, strings.TrimSpace(out) == ref, nil
254 }258 }
255 259
260+// HasBranch checks refs/heads/<branch> with `git for-each-ref` after
261+// `git check-ref-format`, which rejects the glob characters for-each-ref
262+// would otherwise interpret.
263+//
264+//hexcheck:ignore suspicious-business-logic-in-adapter port method name only
265+func (g *Git) HasBranch(ctx context.Context, name domain.RepoName, branch string) (bool, error) {
266+ dir, err := g.repoDir(name)
267+ if err != nil {
268+ return false, err
269+ }
270+ ref := headsPrefix + branch
271+ if branch == "" || strings.HasPrefix(branch, "-") {
272+ return false, nil
273+ }
274+ if _, err := g.run(ctx, "check-ref-format", ref); err != nil {
275+ var exit *exec.ExitError
276+ if errors.As(err, &exit) {
277+ return false, nil
278+ }
279+ return false, err
280+ }
281+ out, err := g.run(ctx, "-C", dir, "for-each-ref", "--format=%(refname)", ref)
282+ if err != nil {
283+ return false, fmt.Errorf("look up %s in %q: %w", ref, name, err)
284+ }
285+ return strings.TrimSpace(out) == ref, nil
286+}
287+
288+// HeadBranch reads the HEAD file of the bare repository (files ref backend).
289+func (g *Git) HeadBranch(_ context.Context, name domain.RepoName) (string, error) {
290+ dir, err := g.repoDir(name)
291+ if err != nil {
292+ return "", err
293+ }
294+ raw, err := os.ReadFile(filepath.Join(dir, "HEAD"))
295+ if err != nil {
296+ if errors.Is(err, fs.ErrNotExist) {
297+ return "", domain.ErrNotFound
298+ }
299+ return "", fmt.Errorf("read HEAD of %q: %w", name, err)
300+ }
301+ ref, ok := strings.CutPrefix(strings.TrimSpace(string(raw)), "ref: ")
302+ if !ok {
303+ return "", nil // detached
304+ }
305+ return strings.TrimPrefix(ref, headsPrefix), nil
306+}
307+
256 // SetDefaultBranch runs `git symbolic-ref HEAD refs/heads/<branch>`.308 // SetDefaultBranch runs `git symbolic-ref HEAD refs/heads/<branch>`.
257 func (g *Git) SetDefaultBranch(ctx context.Context, name domain.RepoName, branch string) error {309 func (g *Git) SetDefaultBranch(ctx context.Context, name domain.RepoName, branch string) error {
258 dir, err := g.repoDir(name)310 dir, err := g.repoDir(name)
M internal/domain/errors.go +7 -0
@@ -26,4 +26,11 @@ var (
2626 ErrRejected = errors.New("rejected")
2727 // ErrConflict means a concurrent change prevented the operation.
2828 ErrConflict = errors.New("conflict")
29+ // ErrInvalid means an argument of a use case is not acceptable and no
30+ // more specific error says why: an empty update, an unknown topic, a
31+ // description that breaks the rules.
32+ ErrInvalid = errors.New("invalid argument")
33+ // ErrPreconditionFailed means the state of the forge does not allow the
34+ // operation, for example a default branch that does not exist.
35+ ErrPreconditionFailed = errors.New("failed precondition")
2936 )
@@ -26,4 +26,11 @@ var (
26 ErrRejected = errors.New("rejected")26 ErrRejected = errors.New("rejected")
27 // ErrConflict means a concurrent change prevented the operation.27 // ErrConflict means a concurrent change prevented the operation.
28 ErrConflict = errors.New("conflict")28 ErrConflict = errors.New("conflict")
29+ // ErrInvalid means an argument of a use case is not acceptable and no
30+ // more specific error says why: an empty update, an unknown topic, a
31+ // description that breaks the rules.
32+ ErrInvalid = errors.New("invalid argument")
33+ // ErrPreconditionFailed means the state of the forge does not allow the
34+ // operation, for example a default branch that does not exist.
35+ ErrPreconditionFailed = errors.New("failed precondition")
29 )36 )
M internal/domain/event.go +21 -0
@@ -60,6 +60,27 @@ type RepoCreated struct {
6060 // EventKind implements Event.
6161 func (RepoCreated) EventKind() string { return "repo.created" }
6262
63+// RepoUpdated is published when the metadata of a repository, or its default
64+// branch, changed. The new state is read from the repository.
65+type RepoUpdated struct {
66+ Repo RepoName
67+ At time.Time
68+}
69+
70+// EventKind implements Event.
71+func (RepoUpdated) EventKind() string { return "repo.updated" }
72+
73+// RepoDeleted is published after a repository was deleted. It records the
74+// visibility the repository had, which decides who may be told.
75+type RepoDeleted struct {
76+ Repo RepoName
77+ Visibility Visibility
78+ At time.Time
79+}
80+
81+// EventKind implements Event.
82+func (RepoDeleted) EventKind() string { return "repo.deleted" }
83+
6384 // PushEvent is published after a push has been accepted.
6485 type PushEvent struct {
6586 Repo RepoName
@@ -60,6 +60,27 @@ type RepoCreated struct {
60 // EventKind implements Event.60 // EventKind implements Event.
61 func (RepoCreated) EventKind() string { return "repo.created" }61 func (RepoCreated) EventKind() string { return "repo.created" }
62 62
63+// RepoUpdated is published when the metadata of a repository, or its default
64+// branch, changed. The new state is read from the repository.
65+type RepoUpdated struct {
66+ Repo RepoName
67+ At time.Time
68+}
69+
70+// EventKind implements Event.
71+func (RepoUpdated) EventKind() string { return "repo.updated" }
72+
73+// RepoDeleted is published after a repository was deleted. It records the
74+// visibility the repository had, which decides who may be told.
75+type RepoDeleted struct {
76+ Repo RepoName
77+ Visibility Visibility
78+ At time.Time
79+}
80+
81+// EventKind implements Event.
82+func (RepoDeleted) EventKind() string { return "repo.deleted" }
83+
63 // PushEvent is published after a push has been accepted.84 // PushEvent is published after a push has been accepted.
64 type PushEvent struct {85 type PushEvent struct {
65 Repo RepoName86 Repo RepoName
A internal/domain/principalctx.go +22 -0
@@ -0,0 +1,22 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+import "context"
6+
7+type principalKey struct{}
8+
9+// ContextWithPrincipal returns a context that carries p as the identity of the
10+// caller. The API adapter sets it after authenticating a request; the web
11+// sets it for the calls it makes in process (anonymous, or the owner of a
12+// session). Authorization stays in the use cases.
13+func ContextWithPrincipal(ctx context.Context, p Principal) context.Context {
14+ return context.WithValue(ctx, principalKey{}, p)
15+}
16+
17+// PrincipalFromContext returns the principal ContextWithPrincipal stored.
18+// ok is false when the context carries none.
19+func PrincipalFromContext(ctx context.Context) (p Principal, ok bool) {
20+ p, ok = ctx.Value(principalKey{}).(Principal)
21+ return p, ok
22+}
@@ -0,0 +1,22 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+import "context"
6+
7+type principalKey struct{}
8+
9+// ContextWithPrincipal returns a context that carries p as the identity of the
10+// caller. The API adapter sets it after authenticating a request; the web
11+// sets it for the calls it makes in process (anonymous, or the owner of a
12+// session). Authorization stays in the use cases.
13+func ContextWithPrincipal(ctx context.Context, p Principal) context.Context {
14+ return context.WithValue(ctx, principalKey{}, p)
15+}
16+
17+// PrincipalFromContext returns the principal ContextWithPrincipal stored.
18+// ok is false when the context carries none.
19+func PrincipalFromContext(ctx context.Context) (p Principal, ok bool) {
20+ p, ok = ctx.Value(principalKey{}).(Principal)
21+ return p, ok
22+}
M internal/ports/mocks/GitRepos.go +138 -0
@@ -177,6 +177,144 @@ func (_c *GitRepos_Discard_Call) RunAndReturn(run func(ctx context.Context, id p
177177 return _c
178178 }
179179
180+// HasBranch provides a mock function for the type GitRepos
181+func (_mock *GitRepos) HasBranch(ctx context.Context, name domain.RepoName, branch string) (bool, error) {
182+ ret := _mock.Called(ctx, name, branch)
183+
184+ if len(ret) == 0 {
185+ panic("no return value specified for HasBranch")
186+ }
187+
188+ var r0 bool
189+ var r1 error
190+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) (bool, error)); ok {
191+ return returnFunc(ctx, name, branch)
192+ }
193+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) bool); ok {
194+ r0 = returnFunc(ctx, name, branch)
195+ } else {
196+ r0 = ret.Get(0).(bool)
197+ }
198+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, string) error); ok {
199+ r1 = returnFunc(ctx, name, branch)
200+ } else {
201+ r1 = ret.Error(1)
202+ }
203+ return r0, r1
204+}
205+
206+// GitRepos_HasBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasBranch'
207+type GitRepos_HasBranch_Call struct {
208+ *mock.Call
209+}
210+
211+// HasBranch is a helper method to define mock.On call
212+// - ctx context.Context
213+// - name domain.RepoName
214+// - branch string
215+func (_e *GitRepos_Expecter) HasBranch(ctx any, name any, branch any) *GitRepos_HasBranch_Call {
216+ return &GitRepos_HasBranch_Call{Call: _e.mock.On("HasBranch", ctx, name, branch)}
217+}
218+
219+func (_c *GitRepos_HasBranch_Call) Run(run func(ctx context.Context, name domain.RepoName, branch string)) *GitRepos_HasBranch_Call {
220+ _c.Call.Run(func(args mock.Arguments) {
221+ var arg0 context.Context
222+ if args[0] != nil {
223+ arg0 = args[0].(context.Context)
224+ }
225+ var arg1 domain.RepoName
226+ if args[1] != nil {
227+ arg1 = args[1].(domain.RepoName)
228+ }
229+ var arg2 string
230+ if args[2] != nil {
231+ arg2 = args[2].(string)
232+ }
233+ run(
234+ arg0,
235+ arg1,
236+ arg2,
237+ )
238+ })
239+ return _c
240+}
241+
242+func (_c *GitRepos_HasBranch_Call) Return(b bool, err error) *GitRepos_HasBranch_Call {
243+ _c.Call.Return(b, err)
244+ return _c
245+}
246+
247+func (_c *GitRepos_HasBranch_Call) RunAndReturn(run func(ctx context.Context, name domain.RepoName, branch string) (bool, error)) *GitRepos_HasBranch_Call {
248+ _c.Call.Return(run)
249+ return _c
250+}
251+
252+// HeadBranch provides a mock function for the type GitRepos
253+func (_mock *GitRepos) HeadBranch(ctx context.Context, name domain.RepoName) (string, error) {
254+ ret := _mock.Called(ctx, name)
255+
256+ if len(ret) == 0 {
257+ panic("no return value specified for HeadBranch")
258+ }
259+
260+ var r0 string
261+ var r1 error
262+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) (string, error)); ok {
263+ return returnFunc(ctx, name)
264+ }
265+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) string); ok {
266+ r0 = returnFunc(ctx, name)
267+ } else {
268+ r0 = ret.Get(0).(string)
269+ }
270+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
271+ r1 = returnFunc(ctx, name)
272+ } else {
273+ r1 = ret.Error(1)
274+ }
275+ return r0, r1
276+}
277+
278+// GitRepos_HeadBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HeadBranch'
279+type GitRepos_HeadBranch_Call struct {
280+ *mock.Call
281+}
282+
283+// HeadBranch is a helper method to define mock.On call
284+// - ctx context.Context
285+// - name domain.RepoName
286+func (_e *GitRepos_Expecter) HeadBranch(ctx any, name any) *GitRepos_HeadBranch_Call {
287+ return &GitRepos_HeadBranch_Call{Call: _e.mock.On("HeadBranch", ctx, name)}
288+}
289+
290+func (_c *GitRepos_HeadBranch_Call) Run(run func(ctx context.Context, name domain.RepoName)) *GitRepos_HeadBranch_Call {
291+ _c.Call.Run(func(args mock.Arguments) {
292+ var arg0 context.Context
293+ if args[0] != nil {
294+ arg0 = args[0].(context.Context)
295+ }
296+ var arg1 domain.RepoName
297+ if args[1] != nil {
298+ arg1 = args[1].(domain.RepoName)
299+ }
300+ run(
301+ arg0,
302+ arg1,
303+ )
304+ })
305+ return _c
306+}
307+
308+func (_c *GitRepos_HeadBranch_Call) Return(s string, err error) *GitRepos_HeadBranch_Call {
309+ _c.Call.Return(s, err)
310+ return _c
311+}
312+
313+func (_c *GitRepos_HeadBranch_Call) RunAndReturn(run func(ctx context.Context, name domain.RepoName) (string, error)) *GitRepos_HeadBranch_Call {
314+ _c.Call.Return(run)
315+ return _c
316+}
317+
180318 // List provides a mock function for the type GitRepos
181319 func (_mock *GitRepos) List(ctx context.Context) (ports.RepoDirs, error) {
182320 ret := _mock.Called(ctx)
@@ -177,6 +177,144 @@ func (_c *GitRepos_Discard_Call) RunAndReturn(run func(ctx context.Context, id p
177 return _c177 return _c
178 }178 }
179 179
180+// HasBranch provides a mock function for the type GitRepos
181+func (_mock *GitRepos) HasBranch(ctx context.Context, name domain.RepoName, branch string) (bool, error) {
182+ ret := _mock.Called(ctx, name, branch)
183+
184+ if len(ret) == 0 {
185+ panic("no return value specified for HasBranch")
186+ }
187+
188+ var r0 bool
189+ var r1 error
190+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) (bool, error)); ok {
191+ return returnFunc(ctx, name, branch)
192+ }
193+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) bool); ok {
194+ r0 = returnFunc(ctx, name, branch)
195+ } else {
196+ r0 = ret.Get(0).(bool)
197+ }
198+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, string) error); ok {
199+ r1 = returnFunc(ctx, name, branch)
200+ } else {
201+ r1 = ret.Error(1)
202+ }
203+ return r0, r1
204+}
205+
206+// GitRepos_HasBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasBranch'
207+type GitRepos_HasBranch_Call struct {
208+ *mock.Call
209+}
210+
211+// HasBranch is a helper method to define mock.On call
212+// - ctx context.Context
213+// - name domain.RepoName
214+// - branch string
215+func (_e *GitRepos_Expecter) HasBranch(ctx any, name any, branch any) *GitRepos_HasBranch_Call {
216+ return &GitRepos_HasBranch_Call{Call: _e.mock.On("HasBranch", ctx, name, branch)}
217+}
218+
219+func (_c *GitRepos_HasBranch_Call) Run(run func(ctx context.Context, name domain.RepoName, branch string)) *GitRepos_HasBranch_Call {
220+ _c.Call.Run(func(args mock.Arguments) {
221+ var arg0 context.Context
222+ if args[0] != nil {
223+ arg0 = args[0].(context.Context)
224+ }
225+ var arg1 domain.RepoName
226+ if args[1] != nil {
227+ arg1 = args[1].(domain.RepoName)
228+ }
229+ var arg2 string
230+ if args[2] != nil {
231+ arg2 = args[2].(string)
232+ }
233+ run(
234+ arg0,
235+ arg1,
236+ arg2,
237+ )
238+ })
239+ return _c
240+}
241+
242+func (_c *GitRepos_HasBranch_Call) Return(b bool, err error) *GitRepos_HasBranch_Call {
243+ _c.Call.Return(b, err)
244+ return _c
245+}
246+
247+func (_c *GitRepos_HasBranch_Call) RunAndReturn(run func(ctx context.Context, name domain.RepoName, branch string) (bool, error)) *GitRepos_HasBranch_Call {
248+ _c.Call.Return(run)
249+ return _c
250+}
251+
252+// HeadBranch provides a mock function for the type GitRepos
253+func (_mock *GitRepos) HeadBranch(ctx context.Context, name domain.RepoName) (string, error) {
254+ ret := _mock.Called(ctx, name)
255+
256+ if len(ret) == 0 {
257+ panic("no return value specified for HeadBranch")
258+ }
259+
260+ var r0 string
261+ var r1 error
262+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) (string, error)); ok {
263+ return returnFunc(ctx, name)
264+ }
265+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) string); ok {
266+ r0 = returnFunc(ctx, name)
267+ } else {
268+ r0 = ret.Get(0).(string)
269+ }
270+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
271+ r1 = returnFunc(ctx, name)
272+ } else {
273+ r1 = ret.Error(1)
274+ }
275+ return r0, r1
276+}
277+
278+// GitRepos_HeadBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HeadBranch'
279+type GitRepos_HeadBranch_Call struct {
280+ *mock.Call
281+}
282+
283+// HeadBranch is a helper method to define mock.On call
284+// - ctx context.Context
285+// - name domain.RepoName
286+func (_e *GitRepos_Expecter) HeadBranch(ctx any, name any) *GitRepos_HeadBranch_Call {
287+ return &GitRepos_HeadBranch_Call{Call: _e.mock.On("HeadBranch", ctx, name)}
288+}
289+
290+func (_c *GitRepos_HeadBranch_Call) Run(run func(ctx context.Context, name domain.RepoName)) *GitRepos_HeadBranch_Call {
291+ _c.Call.Run(func(args mock.Arguments) {
292+ var arg0 context.Context
293+ if args[0] != nil {
294+ arg0 = args[0].(context.Context)
295+ }
296+ var arg1 domain.RepoName
297+ if args[1] != nil {
298+ arg1 = args[1].(domain.RepoName)
299+ }
300+ run(
301+ arg0,
302+ arg1,
303+ )
304+ })
305+ return _c
306+}
307+
308+func (_c *GitRepos_HeadBranch_Call) Return(s string, err error) *GitRepos_HeadBranch_Call {
309+ _c.Call.Return(s, err)
310+ return _c
311+}
312+
313+func (_c *GitRepos_HeadBranch_Call) RunAndReturn(run func(ctx context.Context, name domain.RepoName) (string, error)) *GitRepos_HeadBranch_Call {
314+ _c.Call.Return(run)
315+ return _c
316+}
317+
180 // List provides a mock function for the type GitRepos318 // List provides a mock function for the type GitRepos
181 func (_mock *GitRepos) List(ctx context.Context) (ports.RepoDirs, error) {319 func (_mock *GitRepos) List(ctx context.Context) (ports.RepoDirs, error) {
182 ret := _mock.Called(ctx)320 ret := _mock.Called(ctx)
M internal/ports/ports.go +9 -0
@@ -65,6 +65,15 @@ type GitRepos interface {
6565 // and whether that branch exists. It returns domain.ErrNotFound if there
6666 // is no such repository.
6767 DefaultBranch(ctx context.Context, name domain.RepoName) (branch string, exists bool, err error)
68+ // HasBranch reports whether refs/heads/<branch> exists. A name that
69+ // cannot be a branch does not exist. It returns domain.ErrNotFound if
70+ // there is no such repository.
71+ HasBranch(ctx context.Context, name domain.RepoName, branch string) (bool, error)
72+ // HeadBranch returns the short name of the branch HEAD points to, whether
73+ // or not that branch exists, and "" when HEAD is detached. It reads no
74+ // refs, so it is cheap enough for a listing. It returns
75+ // domain.ErrNotFound if there is no such repository.
76+ HeadBranch(ctx context.Context, name domain.RepoName) (string, error)
6877 // SetDefaultBranch points HEAD to the branch, which need not exist yet.
6978 SetDefaultBranch(ctx context.Context, name domain.RepoName, branch string) error
7079 // Maintain refreshes the commit-graph of the repository.
@@ -65,6 +65,15 @@ type GitRepos interface {
65 // and whether that branch exists. It returns domain.ErrNotFound if there65 // and whether that branch exists. It returns domain.ErrNotFound if there
66 // is no such repository.66 // is no such repository.
67 DefaultBranch(ctx context.Context, name domain.RepoName) (branch string, exists bool, err error)67 DefaultBranch(ctx context.Context, name domain.RepoName) (branch string, exists bool, err error)
68+ // HasBranch reports whether refs/heads/<branch> exists. A name that
69+ // cannot be a branch does not exist. It returns domain.ErrNotFound if
70+ // there is no such repository.
71+ HasBranch(ctx context.Context, name domain.RepoName, branch string) (bool, error)
72+ // HeadBranch returns the short name of the branch HEAD points to, whether
73+ // or not that branch exists, and "" when HEAD is detached. It reads no
74+ // refs, so it is cheap enough for a listing. It returns
75+ // domain.ErrNotFound if there is no such repository.
76+ HeadBranch(ctx context.Context, name domain.RepoName) (string, error)
68 // SetDefaultBranch points HEAD to the branch, which need not exist yet.77 // SetDefaultBranch points HEAD to the branch, which need not exist yet.
69 SetDefaultBranch(ctx context.Context, name domain.RepoName, branch string) error78 SetDefaultBranch(ctx context.Context, name domain.RepoName, branch string) error
70 // Maintain refreshes the commit-graph of the repository.79 // Maintain refreshes the commit-graph of the repository.

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 45

↑↓ moveenter openesc close