detached 4d9eaac

#8 chore(hexcheck): bump to v0.4.0 and drop unused ignore directives

state
merged · 4d9eaac · fast-forward
branch
chore/hexcheck-v0.4.0 → main
opened
bnema ·
updated

Bump hexcheck to v0.4.0 and make hexcheck -fail-on warn pass. Comment-only changes in adapters.

  • Remove 89 ignore directives that v0.4.0 reports as unused.
  • Annotate the 16 new findings with accurate reasons.
  • Rewrite wrong reasons. 14 functions hold business rules marked 'pending move to domain/usecase' (revision resolution, commit budget, diff budget, workflow guard, commitsAhead, banner, mrPage, repoRecord, CreateRun, RotateOIDCKey, issuesOn, issueSubmittedPage); they move in follow-up MRs.

Ignore directives: 129 -> 57.

CommitSubjectAuthorDateSignatureStatus
4d9eaacfix(usecase): drop the unused repository read before the merge lockbriceGPG Verified
05fc636refactor: reuse the domain settings predicates and tighten the moved rules after reviewbriceGPG Verified
558568cfix(web,rpc): handle an API or web server built without merge requests or OIDC keysbriceGPG Verified
5344be5merge: hexmove/workflow into chore/hexcheck-v0.4.0briceGPG Verified
9f9eee5merge: hexmove/history into chore/hexcheck-v0.4.0briceGPG Verified
c902d2fmerge: hexmove/policies into chore/hexcheck-v0.4.0briceGPG Verified
6b4272dmerge: hexmove/budgets into chore/hexcheck-v0.4.0briceGPG Verified
fe7ab7dmerge: hexmove/revision into chore/hexcheck-v0.4.0briceGPG Verified
a1ec1afrefactor(domain): own the workflow-change rule for job tokens in WorkflowChangeOfbriceGPG Verified
6080cberefactor(usecase): own branch comparison, push summaries and unread commits in MergeRequestsbriceGPG Verified
c0f84a6refactor(usecase): own repo defaults, disabled workflows, OIDC rotation and issue visibility rulesbriceGPG Verified
0424a8arefactor(domain): own commit-object and diff read budgetsbriceGPG Verified
ac21956chore(hexcheck): bump to v0.4.0 and drop unused ignore directivesbriceGPG Verified
e188800refactor(domain): own the revision resolution order in Revision.CandidatesbriceGPG Verified

Checks 8

102 files · +2491 -1408
7/8
M docs/merge-requests.md +13 -0
M go.mod +1 -1
M go.sum +2 -2
M internal/adapters/cilogs/cilogs.go +2 -0
M internal/adapters/cisummaries/cisummaries.go +2 -0
M internal/adapters/ermitegit/commitobjects.go +12 -11
M internal/adapters/ermitegit/convert.go +25 -36
M internal/adapters/ermitegit/ermitegit.go +1 -1
M internal/adapters/gitcli/archive.go +0 -2
M internal/adapters/gitcli/changed.go +0 -4
M internal/adapters/gitcli/commitobjects.go +11 -15
M internal/adapters/gitcli/diff.go +15 -47
M internal/adapters/gitcli/export_test.go +0 -6
M internal/adapters/gitcli/merge.go +0 -16
M internal/adapters/gitcli/pull.go +0 -2
A internal/adapters/gitcli/push_trees.go +400 -0
R internal/adapters/gitcli/workflow_guard_test.go → internal/adapters/gitcli/push_trees_test.go +158 -217 renamed, 53% similar
M internal/adapters/gitcli/read.go +31 -60
M internal/adapters/gitcli/repos.go +1 -3
M internal/adapters/gitcli/stats.go +0 -6
D internal/adapters/gitcli/workflow_guard.go +0 -438
M internal/adapters/githttp/handler.go +4 -4
M internal/adapters/githttp/hooks.go +0 -2
M internal/adapters/kvstore/ci.go +11 -18
M internal/adapters/kvstore/ci_artifact.go +3 -5
M internal/adapters/kvstore/ci_concurrency.go +0 -4
M internal/adapters/kvstore/ci_settings.go +6 -4
M internal/adapters/kvstore/issues.go +0 -8
M internal/adapters/kvstore/issues_pending.go +0 -5
M internal/adapters/kvstore/mergerequests.go +3 -9
M internal/adapters/kvstore/passkeys.go +0 -2
M internal/adapters/kvstore/repos.go +2 -10
M internal/adapters/kvstore/store.go +0 -2
M internal/adapters/kvstore/tokens.go +0 -6
M internal/adapters/rpc/api.go +2 -4
M internal/adapters/rpc/convert.go +1 -4
M internal/adapters/rpc/git.go +0 -1
M internal/adapters/rpc/mergerequest.go +0 -2
M internal/adapters/rpc/repo.go +0 -2
M internal/adapters/rpc/server.go +5 -8
M internal/adapters/rpc/server_test.go +11 -0
M internal/adapters/sigverify/gpg.go +0 -2
M internal/adapters/sigverify/ssh.go +0 -2
M internal/adapters/sigverify/verifier.go +0 -2
M internal/adapters/web/actions_artifacts.go +0 -4
M internal/adapters/web/browse.go +0 -2
M internal/adapters/web/checks.go +2 -0
M internal/adapters/web/commit.go +0 -2
M internal/adapters/web/compare.go +3 -55
M internal/adapters/web/context.go +1 -1
M internal/adapters/web/diff_highlight.go +0 -2
M internal/adapters/web/events.go +3 -1
M internal/adapters/web/events_test.go +13 -0
M internal/adapters/web/fragments.go +43 -32
M internal/adapters/web/issues.go +3 -5
M internal/adapters/web/issues_form.go +3 -1
M internal/adapters/web/issues_post.go +0 -1
M internal/adapters/web/mr.go +96 -88
M internal/adapters/web/mr_files.go +0 -2
M internal/adapters/web/mr_post.go +0 -2
M internal/adapters/web/mr_test.go +4 -4
M internal/adapters/web/paths.go +0 -2
M internal/adapters/web/raw.go +0 -2
M internal/adapters/web/refs.go +0 -4
M internal/adapters/web/repo.go +3 -7
M internal/adapters/web/tree.go +0 -2
M internal/adapters/webauthn/webauthn.go +0 -2
M internal/app/forge.go +3 -8
A internal/domain/budget.go +85 -0
A internal/domain/budget_test.go +141 -0
A internal/domain/ci_workflow_guard.go +123 -0
A internal/domain/ci_workflow_guard_test.go +120 -0
M internal/domain/mergerequest.go +9 -0
M internal/domain/repo.go +8 -0
M internal/domain/repo_test.go +34 -0
A internal/domain/revision.go +54 -0
A internal/domain/revision_test.go +58 -0
M internal/ports/ci.go +41 -15
M internal/ports/dto.go +28 -0
M internal/ports/mocks/MergeRequestService.go +168 -0
M internal/ports/mocks/OIDCKeyService.go +44 -0
A internal/ports/mocks/PushTreeReader.go +223 -0
D internal/ports/mocks/WorkflowGuard.go +0 -127
M internal/ports/oidc.go +2 -0
M internal/ports/services.go +4 -0
M internal/usecase/ci.go +1 -1
A internal/usecase/ci_creation_policy_test.go +48 -0
M internal/usecase/ci_oidc.go +3 -9
M internal/usecase/ci_pull.go +9 -1
M internal/usecase/ci_runner.go +1 -1
M internal/usecase/events.go +2 -2
M internal/usecase/inbox.go +1 -1
M internal/usecase/issue_moderation.go +1 -1
M internal/usecase/issue_submissions.go +1 -1
M internal/usecase/issues.go +1 -1
M internal/usecase/mergerequests.go +57 -8
M internal/usecase/mergerequests_test.go +144 -0
M internal/usecase/oidc.go +27 -3
M internal/usecase/oidc_test.go +44 -1
M internal/usecase/pushes.go +30 -14
M internal/usecase/pushes_test.go +79 -17
@@ -6,6 +6,7 @@ import (
66 "context"
77 "errors"
88 "fmt"
9+ "strings"
910 "testing"
1011 "time"
1112
@@ -440,16 +441,41 @@ func (f *pushFixture) check(ctx context.Context, p domain.Principal, n domain.Re
440441 return f.pushes.Check(ctx, p, n, ups, ports.PushObjects{})
441442 }
442443
443-type fakeGuard struct {
444- path string
445- err error
446- got []domain.RefUpdate
447- objs ports.PushObjects
444+type fakePushTrees struct {
445+ path string
446+ err error
447+ diffErr error
448+ got []domain.RefUpdate
449+ objs ports.PushObjects
450+ bounds domain.PushWalkBounds
451+ subtreePath string
452+ calls int
453+ old, new domain.ObjectID
454+ data *domain.PushSubtrees
455+ change bool
448456 }
449457
450-func (g *fakeGuard) ChangedWorkflow(_ context.Context, _ domain.RepoName, ups []domain.RefUpdate, o ports.PushObjects) (string, error) {
451- g.got, g.objs = ups, o
452- return g.path, g.err
458+func (g *fakePushTrees) PushSubtrees(_ context.Context, _ domain.RepoName, ups []domain.RefUpdate, o ports.PushObjects, path string, bounds domain.PushWalkBounds) (domain.PushSubtrees, error) {
459+ g.got, g.objs, g.subtreePath, g.bounds = ups, o, path, bounds
460+ if g.data != nil {
461+ return *g.data, g.err
462+ }
463+ t := domain.PushSubtrees{Ends: map[string]domain.PushObjectType{}, Subtrees: map[string]domain.Subtree{}}
464+ for _, up := range ups {
465+ t.Ends[up.Old] = domain.PushObjectCommit
466+ t.Ends[up.New] = domain.PushObjectCommit
467+ if g.path != "" || g.change {
468+ id, _ := domain.ParseObjectID(up.New)
469+ t.Subtrees[up.New] = domain.Subtree{Kind: domain.EntryDirectory, ID: id}
470+ }
471+ }
472+ return t, g.err
473+}
474+
475+func (g *fakePushTrees) FirstDifferentEntry(_ context.Context, _ domain.RepoName, _ ports.PushObjects, old, new domain.ObjectID) (string, error) {
476+ g.calls++
477+ g.old, g.new = old, new
478+ return strings.TrimPrefix(g.path, domain.WorkflowDir+"/"), g.diffErr
453479 }
454480
455481 // A push by the token of a job is judged on the workflows it changes, and by it
@@ -464,10 +490,10 @@ func TestPushesCheckRefusesWorkflowChangesOfJobTokens(t *testing.T) {
464490 job := domain.RepoTokenPrincipal(domain.CIRepoToken{ID: "j1", Repo: n, ExpiresAt: exp, Permissions: map[string]string{"contents": "write"}})
465491 readOnlyJob := domain.RepoTokenPrincipal(domain.CIRepoToken{ID: "j2", Repo: n, ExpiresAt: exp})
466492
467- build := func(t *testing.T, g *fakeGuard) *pushFixture {
493+ build := func(t *testing.T, g *fakePushTrees) *pushFixture {
468494 f := newPushFixture(t)
469495 if g != nil {
470- f.pushes.WithWorkflowGuard(g)
496+ f.pushes.WithPushTreeReader(g)
471497 }
472498 f.cirepo.EXPECT().GetRepoToken(ctx, mock.Anything).Return(domain.CIRepoToken{ID: "j1", ExpiresAt: exp}, nil).Maybe()
473499 return f
@@ -475,29 +501,65 @@ func TestPushesCheckRefusesWorkflowChangesOfJobTokens(t *testing.T) {
475501
476502 t.Run("no workflow touched", func(t *testing.T) {
477503 t.Parallel()
478- g := &fakeGuard{}
504+ g := &fakePushTrees{}
479505 f := build(t, g)
480506 require.NoError(t, f.pushes.Check(ctx, job, n, ups, objs))
481507 assert.Equal(t, ups, g.got, "every ref is judged")
482508 assert.Equal(t, objs, g.objs, "from the objects of the push in quarantine")
509+ assert.Equal(t, domain.WorkflowDir, g.subtreePath)
510+ assert.Equal(t, domain.DefaultPushWalkBounds(), g.bounds)
511+ assert.Zero(t, g.calls)
483512 })
484513 t.Run("a workflow touched refuses the whole push with GitHub's text", func(t *testing.T) {
485514 t.Parallel()
486- f := build(t, &fakeGuard{path: ".github/workflows/ci.yml"})
515+ f := build(t, &fakePushTrees{path: ".github/workflows/ci.yml"})
487516 err := f.pushes.Check(ctx, job, n, ups, objs)
488517 require.ErrorIs(t, err, domain.ErrRejected)
489518 assert.Contains(t, err.Error(), "refusing to allow a GitHub App to create or update workflow `.github/workflows/ci.yml` without `workflows` permission")
490519 })
520+ t.Run("empty differing entry reports the directory and uses the domain pair once", func(t *testing.T) {
521+ g := &fakePushTrees{change: true}
522+ f := build(t, g)
523+ err := f.pushes.Check(ctx, job, n, ups, objs)
524+ require.ErrorIs(t, err, domain.ErrRejected)
525+ assert.Contains(t, err.Error(), "workflow `.github/workflows`")
526+ assert.Equal(t, 1, g.calls)
527+ assert.True(t, g.old.IsZero())
528+ assert.Equal(t, oidB, g.new.String())
529+ })
530+ t.Run("diff invalid is rejected with reason", func(t *testing.T) {
531+ g := &fakePushTrees{change: true, diffErr: fmt.Errorf("%w: path too long", domain.ErrInvalid)}
532+ f := build(t, g)
533+ err := f.pushes.Check(ctx, job, n, ups, objs)
534+ require.ErrorIs(t, err, domain.ErrRejected)
535+ assert.Contains(t, err.Error(), "path too long")
536+ assert.Equal(t, 1, g.calls)
537+ })
538+ t.Run("diff I/O failure retains its class", func(t *testing.T) {
539+ boom := errors.New("diff broke")
540+ g := &fakePushTrees{change: true, diffErr: boom}
541+ f := build(t, g)
542+ err := f.pushes.Check(ctx, job, n, ups, objs)
543+ require.ErrorIs(t, err, boom)
544+ assert.NotErrorIs(t, err, domain.ErrRejected)
545+ })
546+ t.Run("missing endpoint is rejected before diff", func(t *testing.T) {
547+ g := &fakePushTrees{data: &domain.PushSubtrees{}}
548+ f := build(t, g)
549+ err := f.pushes.Check(ctx, job, n, ups, objs)
550+ require.ErrorIs(t, err, domain.ErrRejected)
551+ assert.Zero(t, g.calls)
552+ })
491553 t.Run("a failure to tell refuses it, not as a policy", func(t *testing.T) {
492554 t.Parallel()
493- f := build(t, &fakeGuard{err: errors.New("git broke")})
555+ f := build(t, &fakePushTrees{err: errors.New("git broke")})
494556 err := f.pushes.Check(ctx, job, n, ups, objs)
495557 require.Error(t, err)
496558 assert.NotErrorIs(t, err, domain.ErrRejected)
497559 })
498560 t.Run("a push beyond the guard's bounds is refused as a policy, with the reason", func(t *testing.T) {
499561 t.Parallel()
500- f := build(t, &fakeGuard{err: fmt.Errorf("%w: the push brings more than 10000 commits", domain.ErrInvalid)})
562+ f := build(t, &fakePushTrees{err: fmt.Errorf("%w: the push brings more than 10000 commits", domain.ErrInvalid)})
501563 err := f.pushes.Check(ctx, job, n, ups, objs)
502564 require.ErrorIs(t, err, domain.ErrRejected)
503565 assert.Contains(t, err.Error(), "more than 10000 commits")
@@ -509,19 +571,19 @@ func TestPushesCheckRefusesWorkflowChangesOfJobTokens(t *testing.T) {
509571 })
510572 t.Run("a job token without contents: write cannot push at all", func(t *testing.T) {
511573 t.Parallel()
512- f := build(t, &fakeGuard{})
574+ f := build(t, &fakePushTrees{})
513575 assert.ErrorIs(t, f.pushes.Check(ctx, readOnlyJob, n, ups, objs), domain.ErrRejected)
514576 })
515577 t.Run("the owner is not asked", func(t *testing.T) {
516578 t.Parallel()
517- g := &fakeGuard{path: ".github/workflows/ci.yml"}
579+ g := &fakePushTrees{path: ".github/workflows/ci.yml"}
518580 f := build(t, g)
519581 require.NoError(t, f.pushes.Check(ctx, domain.Root(), n, ups, objs))
520582 assert.Nil(t, g.got)
521583 })
522584 t.Run("a job token never creates a repository on push", func(t *testing.T) {
523585 t.Parallel()
524- f := build(t, &fakeGuard{})
586+ f := build(t, &fakePushTrees{})
525587 f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{}, domain.ErrNotFound)
526588 assert.ErrorIs(t, f.pushes.Open(ctx, job, n, true), domain.ErrNotFound)
527589 })
@@ -6,6 +6,7 @@ import (
6 "context"6 "context"
7 "errors"7 "errors"
8 "fmt"8 "fmt"
9+ "strings"
9 "testing"10 "testing"
10 "time"11 "time"
11 12
@@ -440,16 +441,41 @@ func (f *pushFixture) check(ctx context.Context, p domain.Principal, n domain.Re
440 return f.pushes.Check(ctx, p, n, ups, ports.PushObjects{})441 return f.pushes.Check(ctx, p, n, ups, ports.PushObjects{})
441 }442 }
442 443
443-type fakeGuard struct {444+type fakePushTrees struct {
444- path string445+ path string
445- err error446+ err error
446- got []domain.RefUpdate447+ diffErr error
447- objs ports.PushObjects448+ got []domain.RefUpdate
449+ objs ports.PushObjects
450+ bounds domain.PushWalkBounds
451+ subtreePath string
452+ calls int
453+ old, new domain.ObjectID
454+ data *domain.PushSubtrees
455+ change bool
448 }456 }
449 457
450-func (g *fakeGuard) ChangedWorkflow(_ context.Context, _ domain.RepoName, ups []domain.RefUpdate, o ports.PushObjects) (string, error) {458+func (g *fakePushTrees) PushSubtrees(_ context.Context, _ domain.RepoName, ups []domain.RefUpdate, o ports.PushObjects, path string, bounds domain.PushWalkBounds) (domain.PushSubtrees, error) {
451- g.got, g.objs = ups, o459+ g.got, g.objs, g.subtreePath, g.bounds = ups, o, path, bounds
452- return g.path, g.err460+ if g.data != nil {
461+ return *g.data, g.err
462+ }
463+ t := domain.PushSubtrees{Ends: map[string]domain.PushObjectType{}, Subtrees: map[string]domain.Subtree{}}
464+ for _, up := range ups {
465+ t.Ends[up.Old] = domain.PushObjectCommit
466+ t.Ends[up.New] = domain.PushObjectCommit
467+ if g.path != "" || g.change {
468+ id, _ := domain.ParseObjectID(up.New)
469+ t.Subtrees[up.New] = domain.Subtree{Kind: domain.EntryDirectory, ID: id}
470+ }
471+ }
472+ return t, g.err
473+}
474+
475+func (g *fakePushTrees) FirstDifferentEntry(_ context.Context, _ domain.RepoName, _ ports.PushObjects, old, new domain.ObjectID) (string, error) {
476+ g.calls++
477+ g.old, g.new = old, new
478+ return strings.TrimPrefix(g.path, domain.WorkflowDir+"/"), g.diffErr
453 }479 }
454 480
455 // A push by the token of a job is judged on the workflows it changes, and by it481 // A push by the token of a job is judged on the workflows it changes, and by it
@@ -464,10 +490,10 @@ func TestPushesCheckRefusesWorkflowChangesOfJobTokens(t *testing.T) {
464 job := domain.RepoTokenPrincipal(domain.CIRepoToken{ID: "j1", Repo: n, ExpiresAt: exp, Permissions: map[string]string{"contents": "write"}})490 job := domain.RepoTokenPrincipal(domain.CIRepoToken{ID: "j1", Repo: n, ExpiresAt: exp, Permissions: map[string]string{"contents": "write"}})
465 readOnlyJob := domain.RepoTokenPrincipal(domain.CIRepoToken{ID: "j2", Repo: n, ExpiresAt: exp})491 readOnlyJob := domain.RepoTokenPrincipal(domain.CIRepoToken{ID: "j2", Repo: n, ExpiresAt: exp})
466 492
467- build := func(t *testing.T, g *fakeGuard) *pushFixture {493+ build := func(t *testing.T, g *fakePushTrees) *pushFixture {
468 f := newPushFixture(t)494 f := newPushFixture(t)
469 if g != nil {495 if g != nil {
470- f.pushes.WithWorkflowGuard(g)496+ f.pushes.WithPushTreeReader(g)
471 }497 }
472 f.cirepo.EXPECT().GetRepoToken(ctx, mock.Anything).Return(domain.CIRepoToken{ID: "j1", ExpiresAt: exp}, nil).Maybe()498 f.cirepo.EXPECT().GetRepoToken(ctx, mock.Anything).Return(domain.CIRepoToken{ID: "j1", ExpiresAt: exp}, nil).Maybe()
473 return f499 return f
@@ -475,29 +501,65 @@ func TestPushesCheckRefusesWorkflowChangesOfJobTokens(t *testing.T) {
475 501
476 t.Run("no workflow touched", func(t *testing.T) {502 t.Run("no workflow touched", func(t *testing.T) {
477 t.Parallel()503 t.Parallel()
478- g := &fakeGuard{}504+ g := &fakePushTrees{}
479 f := build(t, g)505 f := build(t, g)
480 require.NoError(t, f.pushes.Check(ctx, job, n, ups, objs))506 require.NoError(t, f.pushes.Check(ctx, job, n, ups, objs))
481 assert.Equal(t, ups, g.got, "every ref is judged")507 assert.Equal(t, ups, g.got, "every ref is judged")
482 assert.Equal(t, objs, g.objs, "from the objects of the push in quarantine")508 assert.Equal(t, objs, g.objs, "from the objects of the push in quarantine")
509+ assert.Equal(t, domain.WorkflowDir, g.subtreePath)
510+ assert.Equal(t, domain.DefaultPushWalkBounds(), g.bounds)
511+ assert.Zero(t, g.calls)
483 })512 })
484 t.Run("a workflow touched refuses the whole push with GitHub's text", func(t *testing.T) {513 t.Run("a workflow touched refuses the whole push with GitHub's text", func(t *testing.T) {
485 t.Parallel()514 t.Parallel()
486- f := build(t, &fakeGuard{path: ".github/workflows/ci.yml"})515+ f := build(t, &fakePushTrees{path: ".github/workflows/ci.yml"})
487 err := f.pushes.Check(ctx, job, n, ups, objs)516 err := f.pushes.Check(ctx, job, n, ups, objs)
488 require.ErrorIs(t, err, domain.ErrRejected)517 require.ErrorIs(t, err, domain.ErrRejected)
489 assert.Contains(t, err.Error(), "refusing to allow a GitHub App to create or update workflow `.github/workflows/ci.yml` without `workflows` permission")518 assert.Contains(t, err.Error(), "refusing to allow a GitHub App to create or update workflow `.github/workflows/ci.yml` without `workflows` permission")
490 })519 })
520+ t.Run("empty differing entry reports the directory and uses the domain pair once", func(t *testing.T) {
521+ g := &fakePushTrees{change: true}
522+ f := build(t, g)
523+ err := f.pushes.Check(ctx, job, n, ups, objs)
524+ require.ErrorIs(t, err, domain.ErrRejected)
525+ assert.Contains(t, err.Error(), "workflow `.github/workflows`")
526+ assert.Equal(t, 1, g.calls)
527+ assert.True(t, g.old.IsZero())
528+ assert.Equal(t, oidB, g.new.String())
529+ })
530+ t.Run("diff invalid is rejected with reason", func(t *testing.T) {
531+ g := &fakePushTrees{change: true, diffErr: fmt.Errorf("%w: path too long", domain.ErrInvalid)}
532+ f := build(t, g)
533+ err := f.pushes.Check(ctx, job, n, ups, objs)
534+ require.ErrorIs(t, err, domain.ErrRejected)
535+ assert.Contains(t, err.Error(), "path too long")
536+ assert.Equal(t, 1, g.calls)
537+ })
538+ t.Run("diff I/O failure retains its class", func(t *testing.T) {
539+ boom := errors.New("diff broke")
540+ g := &fakePushTrees{change: true, diffErr: boom}
541+ f := build(t, g)
542+ err := f.pushes.Check(ctx, job, n, ups, objs)
543+ require.ErrorIs(t, err, boom)
544+ assert.NotErrorIs(t, err, domain.ErrRejected)
545+ })
546+ t.Run("missing endpoint is rejected before diff", func(t *testing.T) {
547+ g := &fakePushTrees{data: &domain.PushSubtrees{}}
548+ f := build(t, g)
549+ err := f.pushes.Check(ctx, job, n, ups, objs)
550+ require.ErrorIs(t, err, domain.ErrRejected)
551+ assert.Zero(t, g.calls)
552+ })
491 t.Run("a failure to tell refuses it, not as a policy", func(t *testing.T) {553 t.Run("a failure to tell refuses it, not as a policy", func(t *testing.T) {
492 t.Parallel()554 t.Parallel()
493- f := build(t, &fakeGuard{err: errors.New("git broke")})555+ f := build(t, &fakePushTrees{err: errors.New("git broke")})
494 err := f.pushes.Check(ctx, job, n, ups, objs)556 err := f.pushes.Check(ctx, job, n, ups, objs)
495 require.Error(t, err)557 require.Error(t, err)
496 assert.NotErrorIs(t, err, domain.ErrRejected)558 assert.NotErrorIs(t, err, domain.ErrRejected)
497 })559 })
498 t.Run("a push beyond the guard's bounds is refused as a policy, with the reason", func(t *testing.T) {560 t.Run("a push beyond the guard's bounds is refused as a policy, with the reason", func(t *testing.T) {
499 t.Parallel()561 t.Parallel()
500- f := build(t, &fakeGuard{err: fmt.Errorf("%w: the push brings more than 10000 commits", domain.ErrInvalid)})562+ f := build(t, &fakePushTrees{err: fmt.Errorf("%w: the push brings more than 10000 commits", domain.ErrInvalid)})
501 err := f.pushes.Check(ctx, job, n, ups, objs)563 err := f.pushes.Check(ctx, job, n, ups, objs)
502 require.ErrorIs(t, err, domain.ErrRejected)564 require.ErrorIs(t, err, domain.ErrRejected)
503 assert.Contains(t, err.Error(), "more than 10000 commits")565 assert.Contains(t, err.Error(), "more than 10000 commits")
@@ -509,19 +571,19 @@ func TestPushesCheckRefusesWorkflowChangesOfJobTokens(t *testing.T) {
509 })571 })
510 t.Run("a job token without contents: write cannot push at all", func(t *testing.T) {572 t.Run("a job token without contents: write cannot push at all", func(t *testing.T) {
511 t.Parallel()573 t.Parallel()
512- f := build(t, &fakeGuard{})574+ f := build(t, &fakePushTrees{})
513 assert.ErrorIs(t, f.pushes.Check(ctx, readOnlyJob, n, ups, objs), domain.ErrRejected)575 assert.ErrorIs(t, f.pushes.Check(ctx, readOnlyJob, n, ups, objs), domain.ErrRejected)
514 })576 })
515 t.Run("the owner is not asked", func(t *testing.T) {577 t.Run("the owner is not asked", func(t *testing.T) {
516 t.Parallel()578 t.Parallel()
517- g := &fakeGuard{path: ".github/workflows/ci.yml"}579+ g := &fakePushTrees{path: ".github/workflows/ci.yml"}
518 f := build(t, g)580 f := build(t, g)
519 require.NoError(t, f.pushes.Check(ctx, domain.Root(), n, ups, objs))581 require.NoError(t, f.pushes.Check(ctx, domain.Root(), n, ups, objs))
520 assert.Nil(t, g.got)582 assert.Nil(t, g.got)
521 })583 })
522 t.Run("a job token never creates a repository on push", func(t *testing.T) {584 t.Run("a job token never creates a repository on push", func(t *testing.T) {
523 t.Parallel()585 t.Parallel()
524- f := build(t, &fakeGuard{})586+ f := build(t, &fakePushTrees{})
525 f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{}, domain.ErrNotFound)587 f.meta.EXPECT().Get(ctx, n).Return(domain.Repo{}, domain.ErrNotFound)
526 assert.ErrorIs(t, f.pushes.Open(ctx, job, n, true), domain.ErrNotFound)588 assert.ErrorIs(t, f.pushes.Open(ctx, job, n, true), domain.ErrNotFound)
527 })589 })
M internal/usecase/repos_write.go +1 -3
@@ -165,9 +165,7 @@ func (r *Repos) updateMeta(ctx context.Context, name domain.RepoName, params por
165165 if params.Visibility != nil {
166166 repo.Visibility = *params.Visibility
167167 }
168- if repo.Settings.IsZero() {
169- repo.Settings = domain.DefaultRepoSettings()
170- }
168+ repo.Settings = repo.EffectiveSettings()
171169 if params.AllowedMergeModes != nil {
172170 repo.Settings.AllowedMergeModes = *params.AllowedMergeModes
173171 }
@@ -165,9 +165,7 @@ func (r *Repos) updateMeta(ctx context.Context, name domain.RepoName, params por
165 if params.Visibility != nil {165 if params.Visibility != nil {
166 repo.Visibility = *params.Visibility166 repo.Visibility = *params.Visibility
167 }167 }
168- if repo.Settings.IsZero() {168+ repo.Settings = repo.EffectiveSettings()
169- repo.Settings = domain.DefaultRepoSettings()
170- }
171 if params.AllowedMergeModes != nil {169 if params.AllowedMergeModes != nil {
172 repo.Settings.AllowedMergeModes = *params.AllowedMergeModes170 repo.Settings.AllowedMergeModes = *params.AllowedMergeModes
173 }171 }

Comments1 open / 1

bnema outdated

Scope extended: the business rules flagged in adapters now live in domain/usecase.

  • domain: Revision.Candidates, CommitObjectBudget/ValidateCommitObjects/DiffBudget, WorkflowChangeOf (port WorkflowGuard -> PushTreeReader), Repo.EffectiveSettings, IssuesEnabled/ShowsIssueSubmissionReceipt/AcceptsAnonymousIssues.
  • usecase: CI.createRun refuses disabled workflows (kvstore keeps the atomic precondition); OIDC service always built, decides disabled-mode errors; MergeRequests.Compare/PushSummary, Get returns NewCommits/HeadUnread.
  • Deliberate changes: the commits banner detects a rewrite by ancestry; the compare page uses the real target..source range.
  • Ignore directives: 129 -> 49, none marked 'pending move'.

Reviewed (correctness/security + architecture/maintainability/tests), findings fixed or deferred (GO-005 wiring, GO-011/012 budget loop factoring). Rebased on main 4534992. Build, vet, gofmt, hexcheck -fail-on warn, go test ./internal/... and -race on touched packages pass.

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
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 3

Tags 44

↑↓ moveenter openesc close