detached d1f49e5

feat(rpc): serve merge request auto-merge and its events

commit
d1f49e581aa34a06632406e669c56bc9dd3f03fe
author
brice · · 2026-10-20 16:17 UTC
parent
ba47d48
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +160 -19
M internal/adapters/client/client_test.go +3 -3
@@ -139,16 +139,16 @@ func TestDescribeBoundsLongMessages(t *testing.T) {
139139 func TestMissingFeatures(t *testing.T) {
140140 assert.Empty(t, client.MissingFeatures(client.UsedFeatures()))
141141 assert.Equal(t,
142- []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures},
142+ []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures, client.FeatureAutoMerge},
143143 client.MissingFeatures([]string{client.FeatureRepoUpdate, client.FeatureSession, client.FeaturePasskeys, "other"}))
144144 assert.Equal(t, []string{client.FeatureSigningKey}, client.MissingFeatures([]string{
145145 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSession,
146146 client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows,
147- client.FeatureCIArtifacts, client.FeaturePasskeys, client.FeatureCommitSignatures,
147+ client.FeatureCIArtifacts, client.FeaturePasskeys, client.FeatureCommitSignatures, client.FeatureAutoMerge,
148148 }))
149149 assert.Equal(t, []string{client.FeaturePasskeys}, client.MissingFeatures([]string{
150150 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureSession,
151- client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures,
151+ client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures, client.FeatureAutoMerge,
152152 }))
153153 assert.Equal(t, client.UsedFeatures(), client.MissingFeatures(nil))
154154 }
@@ -139,16 +139,16 @@ func TestDescribeBoundsLongMessages(t *testing.T) {
139 func TestMissingFeatures(t *testing.T) {139 func TestMissingFeatures(t *testing.T) {
140 assert.Empty(t, client.MissingFeatures(client.UsedFeatures()))140 assert.Empty(t, client.MissingFeatures(client.UsedFeatures()))
141 assert.Equal(t,141 assert.Equal(t,
142- []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures},142+ []string{client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures, client.FeatureAutoMerge},
143 client.MissingFeatures([]string{client.FeatureRepoUpdate, client.FeatureSession, client.FeaturePasskeys, "other"}))143 client.MissingFeatures([]string{client.FeatureRepoUpdate, client.FeatureSession, client.FeaturePasskeys, "other"}))
144 assert.Equal(t, []string{client.FeatureSigningKey}, client.MissingFeatures([]string{144 assert.Equal(t, []string{client.FeatureSigningKey}, client.MissingFeatures([]string{
145 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSession,145 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSession,
146 client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows,146 client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows,
147- client.FeatureCIArtifacts, client.FeaturePasskeys, client.FeatureCommitSignatures,147+ client.FeatureCIArtifacts, client.FeaturePasskeys, client.FeatureCommitSignatures, client.FeatureAutoMerge,
148 }))148 }))
149 assert.Equal(t, []string{client.FeaturePasskeys}, client.MissingFeatures([]string{149 assert.Equal(t, []string{client.FeaturePasskeys}, client.MissingFeatures([]string{
150 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureSession,150 client.FeatureRepoUpdate, client.FeatureEventsResume, client.FeatureMergeRequests, client.FeatureMergeDeleteBranch, client.FeatureIssues, client.FeatureMergeability, client.FeatureSigningKey, client.FeatureSession,
151- client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures,151+ client.FeatureTokenBranchPrefixes, client.FeatureCIRuns, client.FeatureCIRunners, client.FeatureCISecrets, client.FeatureCIDispatch, client.FeatureCIWorkflows, client.FeatureCIArtifacts, client.FeatureCommitSignatures, client.FeatureAutoMerge,
152 }))152 }))
153 assert.Equal(t, client.UsedFeatures(), client.MissingFeatures(nil))153 assert.Equal(t, client.UsedFeatures(), client.MissingFeatures(nil))
154 }154 }
M internal/adapters/client/features.go +5 -1
@@ -57,11 +57,15 @@ const (
5757 // FeatureMergeability is MergeRequest.mergeability and the
5858 // merge_request_mergeability event.
5959 FeatureMergeability = "merge_requests.mergeability"
60+ // FeatureAutoMerge is MergeRequestService.EnableAutoMerge and
61+ // CancelAutoMerge, RepoSettings.allow_auto_merge and the auto-merge events
62+ // (`ermite mr merge --auto`).
63+ FeatureAutoMerge = "merge_requests.auto_merge"
6064 )
6165
6266 // UsedFeatures lists the features the CLI uses, in the order they are reported.
6367 func UsedFeatures() []string {
64- return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureMergeDeleteBranch, FeatureIssues, FeatureMergeability, FeatureSigningKey, FeatureSession, FeatureTokenBranchPrefixes, FeatureCIRuns, FeatureCIRunners, FeatureCISecrets, FeatureCIDispatch, FeatureCIWorkflows, FeatureCIArtifacts, FeaturePasskeys, FeatureCommitSignatures}
68+ return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureMergeDeleteBranch, FeatureIssues, FeatureMergeability, FeatureSigningKey, FeatureSession, FeatureTokenBranchPrefixes, FeatureCIRuns, FeatureCIRunners, FeatureCISecrets, FeatureCIDispatch, FeatureCIWorkflows, FeatureCIArtifacts, FeaturePasskeys, FeatureCommitSignatures, FeatureAutoMerge}
6569 }
6670
6771 // MissingFeatures returns the features the CLI uses that have is lacking.
@@ -57,11 +57,15 @@ const (
57 // FeatureMergeability is MergeRequest.mergeability and the57 // FeatureMergeability is MergeRequest.mergeability and the
58 // merge_request_mergeability event.58 // merge_request_mergeability event.
59 FeatureMergeability = "merge_requests.mergeability"59 FeatureMergeability = "merge_requests.mergeability"
60+ // FeatureAutoMerge is MergeRequestService.EnableAutoMerge and
61+ // CancelAutoMerge, RepoSettings.allow_auto_merge and the auto-merge events
62+ // (`ermite mr merge --auto`).
63+ FeatureAutoMerge = "merge_requests.auto_merge"
60 )64 )
61 65
62 // UsedFeatures lists the features the CLI uses, in the order they are reported.66 // UsedFeatures lists the features the CLI uses, in the order they are reported.
63 func UsedFeatures() []string {67 func UsedFeatures() []string {
64- return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureMergeDeleteBranch, FeatureIssues, FeatureMergeability, FeatureSigningKey, FeatureSession, FeatureTokenBranchPrefixes, FeatureCIRuns, FeatureCIRunners, FeatureCISecrets, FeatureCIDispatch, FeatureCIWorkflows, FeatureCIArtifacts, FeaturePasskeys, FeatureCommitSignatures}68+ return []string{FeatureRepoUpdate, FeatureEventsResume, FeatureMergeRequests, FeatureMergeDeleteBranch, FeatureIssues, FeatureMergeability, FeatureSigningKey, FeatureSession, FeatureTokenBranchPrefixes, FeatureCIRuns, FeatureCIRunners, FeatureCISecrets, FeatureCIDispatch, FeatureCIWorkflows, FeatureCIArtifacts, FeaturePasskeys, FeatureCommitSignatures, FeatureAutoMerge}
65 }69 }
66 70
67 // MissingFeatures returns the features the CLI uses that have is lacking.71 // MissingFeatures returns the features the CLI uses that have is lacking.
M internal/adapters/rpc/api.go +1 -1
@@ -35,7 +35,7 @@ const RunnerMaxRequestBytes = MaxRequestBytes + 64<<10
3535 // always implements, as GetVersion lists them. FeaturePasskeys comes on top
3636 // when Config.Passkeys is set, and FeatureCISecrets when the settings services
3737 // are, and FeatureCommitSignatures when Config.TrustedKeys is set.
38-var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows"}
38+var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows", "merge_requests.auto_merge"}
3939
4040 // FeaturePasskeys is listed by GetVersion when passkeys are available.
4141 // FeatureCIArtifacts (artifact.go) when the artifact services are served.
@@ -35,7 +35,7 @@ const RunnerMaxRequestBytes = MaxRequestBytes + 64<<10
35 // always implements, as GetVersion lists them. FeaturePasskeys comes on top35 // always implements, as GetVersion lists them. FeaturePasskeys comes on top
36 // when Config.Passkeys is set, and FeatureCISecrets when the settings services36 // when Config.Passkeys is set, and FeatureCISecrets when the settings services
37 // are, and FeatureCommitSignatures when Config.TrustedKeys is set.37 // are, and FeatureCommitSignatures when Config.TrustedKeys is set.
38-var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows"}38+var Features = []string{"repo.update", "events.resume", "merge_requests", "merge_requests.delete_branch", "issues", "merge_requests.mergeability", "signing_key", "session", "git.list_paths", "token.branch_prefixes", "ci.runs", "ci.runs.filters", "ci.runners", "ci.actions", "ci.annotations", "ci.summaries", "ci.concurrency", "ci.dispatch", "ci.cache", "ci.workflows", "merge_requests.auto_merge"}
39 39
40 // FeaturePasskeys is listed by GetVersion when passkeys are available.40 // FeaturePasskeys is listed by GetVersion when passkeys are available.
41 // FeatureCIArtifacts (artifact.go) when the artifact services are served.41 // FeatureCIArtifacts (artifact.go) when the artifact services are served.
M internal/adapters/rpc/convert.go +8 -2
@@ -72,7 +72,7 @@ func repoToPB(v ports.RepoView) *pb.Repo {
7272 }
7373
7474 func settingsToPB(s domain.RepoSettings) *pb.RepoSettings {
75- out := &pb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, Issues: issuesModeToPB(s.Issues)}
75+ out := &pb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, AllowAutoMerge: s.AllowAutoMerge, Issues: issuesModeToPB(s.Issues)}
7676 for _, m := range s.AllowedMergeModes {
7777 out.AllowedMergeModes = append(out.AllowedMergeModes, mergeModeToPB(m))
7878 }
@@ -483,7 +483,13 @@ func watchEventToPB(we ports.WatchEvent) *pb.Event {
483483 out.Kind = &pb.Event_MergeRequestOpened{MergeRequestOpened: &pb.MergeRequestOpened{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
484484 case domain.MergeRequestMerged:
485485 out.Time = timestamp(ev.At)
486- out.Kind = &pb.Event_MergeRequestMerged{MergeRequestMerged: &pb.MergeRequestMerged{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
486+ out.Kind = &pb.Event_MergeRequestMerged{MergeRequestMerged: &pb.MergeRequestMerged{MergeRequest: mergeRequestToPB(ev.MergeRequest), Auto: ev.Auto}}
487+ case domain.MergeRequestAutoMergeEnabled:
488+ out.Time = timestamp(ev.At)
489+ out.Kind = &pb.Event_MergeRequestAutoMergeEnabled{MergeRequestAutoMergeEnabled: &pb.MergeRequestAutoMergeEnabled{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
490+ case domain.MergeRequestAutoMergeCancelled:
491+ out.Time = timestamp(ev.At)
492+ out.Kind = &pb.Event_MergeRequestAutoMergeCancelled{MergeRequestAutoMergeCancelled: &pb.MergeRequestAutoMergeCancelled{MergeRequest: mergeRequestToPB(ev.MergeRequest), Reason: ev.Reason}}
487493 case domain.MergeRequestClosed:
488494 out.Time = timestamp(ev.At)
489495 out.Kind = &pb.Event_MergeRequestClosed{MergeRequestClosed: &pb.MergeRequestClosed{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
@@ -72,7 +72,7 @@ func repoToPB(v ports.RepoView) *pb.Repo {
72 }72 }
73 73
74 func settingsToPB(s domain.RepoSettings) *pb.RepoSettings {74 func settingsToPB(s domain.RepoSettings) *pb.RepoSettings {
75- out := &pb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, Issues: issuesModeToPB(s.Issues)}75+ out := &pb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, AllowAutoMerge: s.AllowAutoMerge, Issues: issuesModeToPB(s.Issues)}
76 for _, m := range s.AllowedMergeModes {76 for _, m := range s.AllowedMergeModes {
77 out.AllowedMergeModes = append(out.AllowedMergeModes, mergeModeToPB(m))77 out.AllowedMergeModes = append(out.AllowedMergeModes, mergeModeToPB(m))
78 }78 }
@@ -483,7 +483,13 @@ func watchEventToPB(we ports.WatchEvent) *pb.Event {
483 out.Kind = &pb.Event_MergeRequestOpened{MergeRequestOpened: &pb.MergeRequestOpened{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}483 out.Kind = &pb.Event_MergeRequestOpened{MergeRequestOpened: &pb.MergeRequestOpened{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
484 case domain.MergeRequestMerged:484 case domain.MergeRequestMerged:
485 out.Time = timestamp(ev.At)485 out.Time = timestamp(ev.At)
486- out.Kind = &pb.Event_MergeRequestMerged{MergeRequestMerged: &pb.MergeRequestMerged{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}486+ out.Kind = &pb.Event_MergeRequestMerged{MergeRequestMerged: &pb.MergeRequestMerged{MergeRequest: mergeRequestToPB(ev.MergeRequest), Auto: ev.Auto}}
487+ case domain.MergeRequestAutoMergeEnabled:
488+ out.Time = timestamp(ev.At)
489+ out.Kind = &pb.Event_MergeRequestAutoMergeEnabled{MergeRequestAutoMergeEnabled: &pb.MergeRequestAutoMergeEnabled{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
490+ case domain.MergeRequestAutoMergeCancelled:
491+ out.Time = timestamp(ev.At)
492+ out.Kind = &pb.Event_MergeRequestAutoMergeCancelled{MergeRequestAutoMergeCancelled: &pb.MergeRequestAutoMergeCancelled{MergeRequest: mergeRequestToPB(ev.MergeRequest), Reason: ev.Reason}}
487 case domain.MergeRequestClosed:493 case domain.MergeRequestClosed:
488 out.Time = timestamp(ev.At)494 out.Time = timestamp(ev.At)
489 out.Kind = &pb.Event_MergeRequestClosed{MergeRequestClosed: &pb.MergeRequestClosed{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}495 out.Kind = &pb.Event_MergeRequestClosed{MergeRequestClosed: &pb.MergeRequestClosed{MergeRequest: mergeRequestToPB(ev.MergeRequest)}}
M internal/adapters/rpc/mergerequest.go +31 -7
@@ -4,7 +4,6 @@ package rpc
44
55 import (
66 "context"
7- "errors"
87 "fmt"
98 "strconv"
109
@@ -20,14 +19,32 @@ type mergeRequestService struct {
2019 mrs ports.MergeRequestService
2120 }
2221
23-// EnableAutoMerge is not wired yet.
24-func (s *mergeRequestService) EnableAutoMerge(context.Context, *connect.Request[pb.EnableAutoMergeRequest]) (*connect.Response[pb.EnableAutoMergeResponse], error) {
25- return nil, connect.NewError(connect.CodeUnimplemented, errors.New("auto-merge is not implemented yet"))
22+func (s *mergeRequestService) EnableAutoMerge(ctx context.Context, req *connect.Request[pb.EnableAutoMergeRequest]) (*connect.Response[pb.EnableAutoMergeResponse], error) {
23+ name, err := domain.ParseRepoName(req.Msg.GetRepo())
24+ if err != nil {
25+ return nil, connectError(ctx, err)
26+ }
27+ mode, err := mergeModeFromPB(req.Msg.GetMode())
28+ if err != nil {
29+ return nil, connectError(ctx, err)
30+ }
31+ mr, err := s.mrs.EnableAutoMerge(ctx, caller(ctx), name, req.Msg.GetNumber(), ports.EnableAutoMergeParams{Mode: mode})
32+ if err != nil {
33+ return nil, connectError(ctx, err)
34+ }
35+ return connect.NewResponse(&pb.EnableAutoMergeResponse{MergeRequest: mergeRequestToPB(mr)}), nil
2636 }
2737
28-// CancelAutoMerge is not wired yet.
29-func (s *mergeRequestService) CancelAutoMerge(context.Context, *connect.Request[pb.CancelAutoMergeRequest]) (*connect.Response[pb.CancelAutoMergeResponse], error) {
30- return nil, connect.NewError(connect.CodeUnimplemented, errors.New("auto-merge is not implemented yet"))
38+func (s *mergeRequestService) CancelAutoMerge(ctx context.Context, req *connect.Request[pb.CancelAutoMergeRequest]) (*connect.Response[pb.CancelAutoMergeResponse], error) {
39+ name, err := domain.ParseRepoName(req.Msg.GetRepo())
40+ if err != nil {
41+ return nil, connectError(ctx, err)
42+ }
43+ mr, err := s.mrs.CancelAutoMerge(ctx, caller(ctx), name, req.Msg.GetNumber())
44+ if err != nil {
45+ return nil, connectError(ctx, err)
46+ }
47+ return connect.NewResponse(&pb.CancelAutoMergeResponse{MergeRequest: mergeRequestToPB(mr)}), nil
3148 }
3249
3350 func (s *mergeRequestService) EditComment(ctx context.Context, req *connect.Request[pb.EditCommentRequest]) (*connect.Response[pb.EditCommentResponse], error) {
@@ -144,6 +161,13 @@ func mergeRequestToPB(mr domain.MergeRequest) *pb.MergeRequest {
144161 HeadId: mr.HeadID.String(),
145162 MergedCommitId: mr.MergedCommitID.String(),
146163 MergeMode: mergeModeToPB(mr.MergeMode),
164+ AutoMerged: mr.AutoMerged,
165+ }
166+ if am := mr.AutoMerge; am != nil {
167+ out.AutoMerge = &pb.AutoMerge{
168+ Mode: mergeModeToPB(am.Mode), HeadId: am.HeadID.String(),
169+ EnabledBy: am.EnablerLabel, EnabledAt: timestamp(am.EnabledAt),
170+ }
147171 }
148172 if mr.IsOpen() {
149173 out.Mergeability = &pb.Mergeability{State: pb.MergeabilityState_MERGEABILITY_STATE_CHECKING}
@@ -4,7 +4,6 @@ package rpc
4 4
5 import (5 import (
6 "context"6 "context"
7- "errors"
8 "fmt"7 "fmt"
9 "strconv"8 "strconv"
10 9
@@ -20,14 +19,32 @@ type mergeRequestService struct {
20 mrs ports.MergeRequestService19 mrs ports.MergeRequestService
21 }20 }
22 21
23-// EnableAutoMerge is not wired yet.22+func (s *mergeRequestService) EnableAutoMerge(ctx context.Context, req *connect.Request[pb.EnableAutoMergeRequest]) (*connect.Response[pb.EnableAutoMergeResponse], error) {
24-func (s *mergeRequestService) EnableAutoMerge(context.Context, *connect.Request[pb.EnableAutoMergeRequest]) (*connect.Response[pb.EnableAutoMergeResponse], error) {23+ name, err := domain.ParseRepoName(req.Msg.GetRepo())
25- return nil, connect.NewError(connect.CodeUnimplemented, errors.New("auto-merge is not implemented yet"))24+ if err != nil {
25+ return nil, connectError(ctx, err)
26+ }
27+ mode, err := mergeModeFromPB(req.Msg.GetMode())
28+ if err != nil {
29+ return nil, connectError(ctx, err)
30+ }
31+ mr, err := s.mrs.EnableAutoMerge(ctx, caller(ctx), name, req.Msg.GetNumber(), ports.EnableAutoMergeParams{Mode: mode})
32+ if err != nil {
33+ return nil, connectError(ctx, err)
34+ }
35+ return connect.NewResponse(&pb.EnableAutoMergeResponse{MergeRequest: mergeRequestToPB(mr)}), nil
26 }36 }
27 37
28-// CancelAutoMerge is not wired yet.38+func (s *mergeRequestService) CancelAutoMerge(ctx context.Context, req *connect.Request[pb.CancelAutoMergeRequest]) (*connect.Response[pb.CancelAutoMergeResponse], error) {
29-func (s *mergeRequestService) CancelAutoMerge(context.Context, *connect.Request[pb.CancelAutoMergeRequest]) (*connect.Response[pb.CancelAutoMergeResponse], error) {39+ name, err := domain.ParseRepoName(req.Msg.GetRepo())
30- return nil, connect.NewError(connect.CodeUnimplemented, errors.New("auto-merge is not implemented yet"))40+ if err != nil {
41+ return nil, connectError(ctx, err)
42+ }
43+ mr, err := s.mrs.CancelAutoMerge(ctx, caller(ctx), name, req.Msg.GetNumber())
44+ if err != nil {
45+ return nil, connectError(ctx, err)
46+ }
47+ return connect.NewResponse(&pb.CancelAutoMergeResponse{MergeRequest: mergeRequestToPB(mr)}), nil
31 }48 }
32 49
33 func (s *mergeRequestService) EditComment(ctx context.Context, req *connect.Request[pb.EditCommentRequest]) (*connect.Response[pb.EditCommentResponse], error) {50 func (s *mergeRequestService) EditComment(ctx context.Context, req *connect.Request[pb.EditCommentRequest]) (*connect.Response[pb.EditCommentResponse], error) {
@@ -144,6 +161,13 @@ func mergeRequestToPB(mr domain.MergeRequest) *pb.MergeRequest {
144 HeadId: mr.HeadID.String(),161 HeadId: mr.HeadID.String(),
145 MergedCommitId: mr.MergedCommitID.String(),162 MergedCommitId: mr.MergedCommitID.String(),
146 MergeMode: mergeModeToPB(mr.MergeMode),163 MergeMode: mergeModeToPB(mr.MergeMode),
164+ AutoMerged: mr.AutoMerged,
165+ }
166+ if am := mr.AutoMerge; am != nil {
167+ out.AutoMerge = &pb.AutoMerge{
168+ Mode: mergeModeToPB(am.Mode), HeadId: am.HeadID.String(),
169+ EnabledBy: am.EnablerLabel, EnabledAt: timestamp(am.EnabledAt),
170+ }
147 }171 }
148 if mr.IsOpen() {172 if mr.IsOpen() {
149 out.Mergeability = &pb.Mergeability{State: pb.MergeabilityState_MERGEABILITY_STATE_CHECKING}173 out.Mergeability = &pb.Mergeability{State: pb.MergeabilityState_MERGEABILITY_STATE_CHECKING}
M internal/adapters/rpc/mr_api_test.go +78 -0
@@ -3,6 +3,7 @@
33 package rpc
44
55 import (
6+ "strings"
67 "testing"
78 "time"
89
@@ -122,3 +123,80 @@ func TestNoteVerdictsDetailAndScope(t *testing.T) {
122123 _, err = (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, Verdict: pb.Verdict_VERDICT_APPROVE}))
123124 requireCode(t, connect.CodePermissionDenied, err)
124125 }
126+
127+func TestAutoMergeRPCCodesAndFields(t *testing.T) {
128+ now := time.Date(2026, 10, 20, 9, 0, 0, 0, time.UTC)
129+ head, err := domain.ParseObjectID(strings.Repeat("a", 40))
130+ require.NoError(t, err)
131+ armed := domain.MergeRequest{
132+ Repo: mustRepoName(t, "demo"), Number: 1, State: domain.MRStateOpen, Source: "feat", Target: "main", HeadID: head,
133+ AutoMerge: &domain.AutoMerge{Mode: domain.MergeModeSquash, HeadID: head, EnablerLabel: "owner", EnabledAt: now},
134+ }
135+ idle := armed
136+ idle.AutoMerge = nil
137+
138+ pbArmed := mergeRequestToPB(armed)
139+ require.NotNil(t, pbArmed.GetAutoMerge())
140+ assert.Equal(t, pb.MergeMode_MERGE_MODE_SQUASH, pbArmed.GetAutoMerge().GetMode())
141+ assert.Equal(t, head.String(), pbArmed.GetAutoMerge().GetHeadId())
142+ assert.Equal(t, "owner", pbArmed.GetAutoMerge().GetEnabledBy())
143+ assert.Equal(t, now, pbArmed.GetAutoMerge().GetEnabledAt().AsTime())
144+ assert.False(t, pbArmed.GetAutoMerged())
145+ assert.Nil(t, mergeRequestToPB(idle).GetAutoMerge())
146+ done := idle
147+ done.State, done.AutoMerged = domain.MRStateMerged, true
148+ assert.True(t, mergeRequestToPB(done).GetAutoMerged())
149+
150+ // Success: the mode and the request reach the service, the fields come back.
151+ m := mocks.NewMergeRequestService(t)
152+ m.EXPECT().EnableAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.EnableAutoMergeParams{Mode: domain.MergeModeSquash}).Return(armed, nil).Once()
153+ res, err := (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1, Mode: pb.MergeMode_MERGE_MODE_SQUASH}))
154+ require.NoError(t, err)
155+ assert.Equal(t, pbArmed, res.Msg.GetMergeRequest())
156+ m.EXPECT().CancelAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1)).Return(idle, nil).Once()
157+ cres, err := (&mergeRequestService{m}).CancelAutoMerge(ownerCtx(), connect.NewRequest(&pb.CancelAutoMergeRequest{Repo: "demo", Number: 1}))
158+ require.NoError(t, err)
159+ assert.Nil(t, cres.Msg.GetMergeRequest().GetAutoMerge())
160+
161+ // Bad input never reaches the service.
162+ _, err = (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1, Mode: pb.MergeMode(99)}))
163+ requireCode(t, connect.CodeInvalidArgument, err)
164+ _, err = (&mergeRequestService{m}).CancelAutoMerge(ownerCtx(), connect.NewRequest(&pb.CancelAutoMergeRequest{Repo: "", Number: 1}))
165+ requireCode(t, connect.CodeInvalidArgument, err)
166+
167+ // Domain errors map like the other merge request calls.
168+ for _, tc := range []struct {
169+ err error
170+ code connect.Code
171+ }{
172+ {domain.ErrPreconditionFailed, connect.CodeFailedPrecondition},
173+ {domain.ErrForbidden, connect.CodePermissionDenied},
174+ {domain.ErrNotFound, connect.CodeNotFound},
175+ } {
176+ m := mocks.NewMergeRequestService(t)
177+ m.EXPECT().EnableAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), mock.Anything).Return(domain.MergeRequest{}, tc.err).Once()
178+ _, err := (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1}))
179+ requireCode(t, tc.code, err)
180+ m.EXPECT().CancelAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1)).Return(domain.MergeRequest{}, tc.err).Once()
181+ _, err = (&mergeRequestService{m}).CancelAutoMerge(ownerCtx(), connect.NewRequest(&pb.CancelAutoMergeRequest{Repo: "demo", Number: 1}))
182+ requireCode(t, tc.code, err)
183+ }
184+}
185+
186+func TestAutoMergeWatchEventsAndSettingsToPB(t *testing.T) {
187+ armed := domain.MergeRequest{Repo: mustRepoName(t, "demo"), Number: 1, State: domain.MRStateOpen,
188+ AutoMerge: &domain.AutoMerge{Mode: domain.MergeModeMerge, EnablerLabel: "owner"}}
189+ got := watchEventToPB(ports.WatchEvent{ID: "1", Event: domain.MergeRequestAutoMergeEnabled{MergeRequest: armed}})
190+ require.NotNil(t, got.GetMergeRequestAutoMergeEnabled())
191+ assert.Equal(t, pb.MergeMode_MERGE_MODE_MERGE_COMMIT, got.GetMergeRequestAutoMergeEnabled().GetMergeRequest().GetAutoMerge().GetMode())
192+ got = watchEventToPB(ports.WatchEvent{ID: "2", Event: domain.MergeRequestAutoMergeCancelled{MergeRequest: armed, Reason: "the branch moved"}})
193+ require.NotNil(t, got.GetMergeRequestAutoMergeCancelled())
194+ assert.Equal(t, "the branch moved", got.GetMergeRequestAutoMergeCancelled().GetReason())
195+ got = watchEventToPB(ports.WatchEvent{ID: "3", Event: domain.MergeRequestMerged{MergeRequest: armed, Auto: true}})
196+ assert.True(t, got.GetMergeRequestMerged().GetAuto())
197+ got = watchEventToPB(ports.WatchEvent{ID: "4", Event: domain.MergeRequestMerged{MergeRequest: armed}})
198+ assert.False(t, got.GetMergeRequestMerged().GetAuto())
199+
200+ assert.True(t, settingsToPB(domain.RepoSettings{AllowAutoMerge: true}).GetAllowAutoMerge())
201+ assert.False(t, settingsToPB(domain.RepoSettings{}).GetAllowAutoMerge())
202+}
@@ -3,6 +3,7 @@
3 package rpc3 package rpc
4 4
5 import (5 import (
6+ "strings"
6 "testing"7 "testing"
7 "time"8 "time"
8 9
@@ -122,3 +123,80 @@ func TestNoteVerdictsDetailAndScope(t *testing.T) {
122 _, err = (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, Verdict: pb.Verdict_VERDICT_APPROVE}))123 _, err = (&mergeRequestService{m}).AddComment(ownerCtx(), connect.NewRequest(&pb.AddCommentRequest{Repo: "demo", Number: 1, Verdict: pb.Verdict_VERDICT_APPROVE}))
123 requireCode(t, connect.CodePermissionDenied, err)124 requireCode(t, connect.CodePermissionDenied, err)
124 }125 }
126+
127+func TestAutoMergeRPCCodesAndFields(t *testing.T) {
128+ now := time.Date(2026, 10, 20, 9, 0, 0, 0, time.UTC)
129+ head, err := domain.ParseObjectID(strings.Repeat("a", 40))
130+ require.NoError(t, err)
131+ armed := domain.MergeRequest{
132+ Repo: mustRepoName(t, "demo"), Number: 1, State: domain.MRStateOpen, Source: "feat", Target: "main", HeadID: head,
133+ AutoMerge: &domain.AutoMerge{Mode: domain.MergeModeSquash, HeadID: head, EnablerLabel: "owner", EnabledAt: now},
134+ }
135+ idle := armed
136+ idle.AutoMerge = nil
137+
138+ pbArmed := mergeRequestToPB(armed)
139+ require.NotNil(t, pbArmed.GetAutoMerge())
140+ assert.Equal(t, pb.MergeMode_MERGE_MODE_SQUASH, pbArmed.GetAutoMerge().GetMode())
141+ assert.Equal(t, head.String(), pbArmed.GetAutoMerge().GetHeadId())
142+ assert.Equal(t, "owner", pbArmed.GetAutoMerge().GetEnabledBy())
143+ assert.Equal(t, now, pbArmed.GetAutoMerge().GetEnabledAt().AsTime())
144+ assert.False(t, pbArmed.GetAutoMerged())
145+ assert.Nil(t, mergeRequestToPB(idle).GetAutoMerge())
146+ done := idle
147+ done.State, done.AutoMerged = domain.MRStateMerged, true
148+ assert.True(t, mergeRequestToPB(done).GetAutoMerged())
149+
150+ // Success: the mode and the request reach the service, the fields come back.
151+ m := mocks.NewMergeRequestService(t)
152+ m.EXPECT().EnableAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), ports.EnableAutoMergeParams{Mode: domain.MergeModeSquash}).Return(armed, nil).Once()
153+ res, err := (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1, Mode: pb.MergeMode_MERGE_MODE_SQUASH}))
154+ require.NoError(t, err)
155+ assert.Equal(t, pbArmed, res.Msg.GetMergeRequest())
156+ m.EXPECT().CancelAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1)).Return(idle, nil).Once()
157+ cres, err := (&mergeRequestService{m}).CancelAutoMerge(ownerCtx(), connect.NewRequest(&pb.CancelAutoMergeRequest{Repo: "demo", Number: 1}))
158+ require.NoError(t, err)
159+ assert.Nil(t, cres.Msg.GetMergeRequest().GetAutoMerge())
160+
161+ // Bad input never reaches the service.
162+ _, err = (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1, Mode: pb.MergeMode(99)}))
163+ requireCode(t, connect.CodeInvalidArgument, err)
164+ _, err = (&mergeRequestService{m}).CancelAutoMerge(ownerCtx(), connect.NewRequest(&pb.CancelAutoMergeRequest{Repo: "", Number: 1}))
165+ requireCode(t, connect.CodeInvalidArgument, err)
166+
167+ // Domain errors map like the other merge request calls.
168+ for _, tc := range []struct {
169+ err error
170+ code connect.Code
171+ }{
172+ {domain.ErrPreconditionFailed, connect.CodeFailedPrecondition},
173+ {domain.ErrForbidden, connect.CodePermissionDenied},
174+ {domain.ErrNotFound, connect.CodeNotFound},
175+ } {
176+ m := mocks.NewMergeRequestService(t)
177+ m.EXPECT().EnableAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1), mock.Anything).Return(domain.MergeRequest{}, tc.err).Once()
178+ _, err := (&mergeRequestService{m}).EnableAutoMerge(ownerCtx(), connect.NewRequest(&pb.EnableAutoMergeRequest{Repo: "demo", Number: 1}))
179+ requireCode(t, tc.code, err)
180+ m.EXPECT().CancelAutoMerge(mock.Anything, domain.Owner(), mustRepoName(t, "demo"), int64(1)).Return(domain.MergeRequest{}, tc.err).Once()
181+ _, err = (&mergeRequestService{m}).CancelAutoMerge(ownerCtx(), connect.NewRequest(&pb.CancelAutoMergeRequest{Repo: "demo", Number: 1}))
182+ requireCode(t, tc.code, err)
183+ }
184+}
185+
186+func TestAutoMergeWatchEventsAndSettingsToPB(t *testing.T) {
187+ armed := domain.MergeRequest{Repo: mustRepoName(t, "demo"), Number: 1, State: domain.MRStateOpen,
188+ AutoMerge: &domain.AutoMerge{Mode: domain.MergeModeMerge, EnablerLabel: "owner"}}
189+ got := watchEventToPB(ports.WatchEvent{ID: "1", Event: domain.MergeRequestAutoMergeEnabled{MergeRequest: armed}})
190+ require.NotNil(t, got.GetMergeRequestAutoMergeEnabled())
191+ assert.Equal(t, pb.MergeMode_MERGE_MODE_MERGE_COMMIT, got.GetMergeRequestAutoMergeEnabled().GetMergeRequest().GetAutoMerge().GetMode())
192+ got = watchEventToPB(ports.WatchEvent{ID: "2", Event: domain.MergeRequestAutoMergeCancelled{MergeRequest: armed, Reason: "the branch moved"}})
193+ require.NotNil(t, got.GetMergeRequestAutoMergeCancelled())
194+ assert.Equal(t, "the branch moved", got.GetMergeRequestAutoMergeCancelled().GetReason())
195+ got = watchEventToPB(ports.WatchEvent{ID: "3", Event: domain.MergeRequestMerged{MergeRequest: armed, Auto: true}})
196+ assert.True(t, got.GetMergeRequestMerged().GetAuto())
197+ got = watchEventToPB(ports.WatchEvent{ID: "4", Event: domain.MergeRequestMerged{MergeRequest: armed}})
198+ assert.False(t, got.GetMergeRequestMerged().GetAuto())
199+
200+ assert.True(t, settingsToPB(domain.RepoSettings{AllowAutoMerge: true}).GetAllowAutoMerge())
201+ assert.False(t, settingsToPB(domain.RepoSettings{}).GetAllowAutoMerge())
202+}
M internal/adapters/rpc/repo.go +9 -5
@@ -80,8 +80,8 @@ func (s *repoService) UpdateRepo(ctx context.Context, req *connect.Request[pb.Up
8080
8181 // updateParams maps the field mask of an update. The mask must name at least
8282 // one of "description", "topics", "visibility", "default_branch", "settings",
83-// "settings.allowed_merge_modes" and "settings.failing_checks_block", and
84-// nothing else; the optional issues field stands outside the mask, and alone
83+// "settings.allowed_merge_modes", "settings.failing_checks_block" and
84+// "settings.allow_auto_merge", and nothing else; the optional issues field stands outside the mask, and alone
8585 // it is a valid update.
8686 func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) {
8787 var params ports.UpdateRepoParams
@@ -113,17 +113,21 @@ func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) {
113113 params.Visibility = &vis
114114 case "default_branch":
115115 params.DefaultBranch = new(msg.GetDefaultBranch())
116- case "settings", "settings.allowed_merge_modes", "settings.failing_checks_block":
117- if path != "settings.failing_checks_block" {
116+ case "settings", "settings.allowed_merge_modes", "settings.failing_checks_block", "settings.allow_auto_merge":
117+ all := path == "settings"
118+ if all || path == "settings.allowed_merge_modes" {
118119 modes, err := mergeModesFromPB(msg.GetSettings().GetAllowedMergeModes())
119120 if err != nil {
120121 return params, err
121122 }
122123 params.AllowedMergeModes = &modes
123124 }
124- if path != "settings.allowed_merge_modes" {
125+ if all || path == "settings.failing_checks_block" {
125126 params.FailingChecksBlock = new(msg.GetSettings().GetFailingChecksBlock())
126127 }
128+ if all || path == "settings.allow_auto_merge" {
129+ params.AllowAutoMerge = new(msg.GetSettings().GetAllowAutoMerge())
130+ }
127131 default:
128132 return params, fmt.Errorf("%w: unknown path %q in update_mask", domain.ErrInvalid, path)
129133 }
@@ -80,8 +80,8 @@ func (s *repoService) UpdateRepo(ctx context.Context, req *connect.Request[pb.Up
80 80
81 // updateParams maps the field mask of an update. The mask must name at least81 // updateParams maps the field mask of an update. The mask must name at least
82 // one of "description", "topics", "visibility", "default_branch", "settings",82 // one of "description", "topics", "visibility", "default_branch", "settings",
83-// "settings.allowed_merge_modes" and "settings.failing_checks_block", and83+// "settings.allowed_merge_modes", "settings.failing_checks_block" and
84-// nothing else; the optional issues field stands outside the mask, and alone84+// "settings.allow_auto_merge", and nothing else; the optional issues field stands outside the mask, and alone
85 // it is a valid update.85 // it is a valid update.
86 func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) {86 func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) {
87 var params ports.UpdateRepoParams87 var params ports.UpdateRepoParams
@@ -113,17 +113,21 @@ func updateParams(msg *pb.UpdateRepoRequest) (ports.UpdateRepoParams, error) {
113 params.Visibility = &vis113 params.Visibility = &vis
114 case "default_branch":114 case "default_branch":
115 params.DefaultBranch = new(msg.GetDefaultBranch())115 params.DefaultBranch = new(msg.GetDefaultBranch())
116- case "settings", "settings.allowed_merge_modes", "settings.failing_checks_block":116+ case "settings", "settings.allowed_merge_modes", "settings.failing_checks_block", "settings.allow_auto_merge":
117- if path != "settings.failing_checks_block" {117+ all := path == "settings"
118+ if all || path == "settings.allowed_merge_modes" {
118 modes, err := mergeModesFromPB(msg.GetSettings().GetAllowedMergeModes())119 modes, err := mergeModesFromPB(msg.GetSettings().GetAllowedMergeModes())
119 if err != nil {120 if err != nil {
120 return params, err121 return params, err
121 }122 }
122 params.AllowedMergeModes = &modes123 params.AllowedMergeModes = &modes
123 }124 }
124- if path != "settings.allowed_merge_modes" {125+ if all || path == "settings.failing_checks_block" {
125 params.FailingChecksBlock = new(msg.GetSettings().GetFailingChecksBlock())126 params.FailingChecksBlock = new(msg.GetSettings().GetFailingChecksBlock())
126 }127 }
128+ if all || path == "settings.allow_auto_merge" {
129+ params.AllowAutoMerge = new(msg.GetSettings().GetAllowAutoMerge())
130+ }
127 default:131 default:
128 return params, fmt.Errorf("%w: unknown path %q in update_mask", domain.ErrInvalid, path)132 return params, fmt.Errorf("%w: unknown path %q in update_mask", domain.ErrInvalid, path)
129 }133 }
M internal/adapters/rpc/repo_test.go +25 -0
@@ -280,3 +280,28 @@ func TestRepoToPBTopics(t *testing.T) {
280280 assert.Equal(t, []string{"cli", "go"}, got.GetTopics())
281281 assert.Empty(t, repoToPB(ports.RepoView{Repo: domain.Repo{Name: mustRepoName(t, "demo")}}).GetTopics())
282282 }
283+
284+func TestUpdateParamsAllowAutoMerge(t *testing.T) {
285+ t.Parallel()
286+ mask := func(paths ...string) *fieldmaskpb.FieldMask { return &fieldmaskpb.FieldMask{Paths: paths} }
287+ settings := &pb.RepoSettings{AllowAutoMerge: true, FailingChecksBlock: true, AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}}
288+
289+ params, err := updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.allow_auto_merge"), Settings: settings})
290+ require.NoError(t, err)
291+ require.NotNil(t, params.AllowAutoMerge)
292+ assert.True(t, *params.AllowAutoMerge)
293+ assert.Nil(t, params.FailingChecksBlock, "only the masked setting is set")
294+ assert.Nil(t, params.AllowedMergeModes)
295+
296+ params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings"), Settings: settings})
297+ require.NoError(t, err)
298+ require.NotNil(t, params.AllowAutoMerge, "settings covers allow_auto_merge")
299+ assert.True(t, *params.AllowAutoMerge)
300+ require.NotNil(t, params.FailingChecksBlock)
301+ require.NotNil(t, params.AllowedMergeModes)
302+
303+ params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.failing_checks_block"), Settings: settings})
304+ require.NoError(t, err)
305+ assert.Nil(t, params.AllowAutoMerge)
306+ require.NotNil(t, params.FailingChecksBlock)
307+}
@@ -280,3 +280,28 @@ func TestRepoToPBTopics(t *testing.T) {
280 assert.Equal(t, []string{"cli", "go"}, got.GetTopics())280 assert.Equal(t, []string{"cli", "go"}, got.GetTopics())
281 assert.Empty(t, repoToPB(ports.RepoView{Repo: domain.Repo{Name: mustRepoName(t, "demo")}}).GetTopics())281 assert.Empty(t, repoToPB(ports.RepoView{Repo: domain.Repo{Name: mustRepoName(t, "demo")}}).GetTopics())
282 }282 }
283+
284+func TestUpdateParamsAllowAutoMerge(t *testing.T) {
285+ t.Parallel()
286+ mask := func(paths ...string) *fieldmaskpb.FieldMask { return &fieldmaskpb.FieldMask{Paths: paths} }
287+ settings := &pb.RepoSettings{AllowAutoMerge: true, FailingChecksBlock: true, AllowedMergeModes: []pb.MergeMode{pb.MergeMode_MERGE_MODE_SQUASH}}
288+
289+ params, err := updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.allow_auto_merge"), Settings: settings})
290+ require.NoError(t, err)
291+ require.NotNil(t, params.AllowAutoMerge)
292+ assert.True(t, *params.AllowAutoMerge)
293+ assert.Nil(t, params.FailingChecksBlock, "only the masked setting is set")
294+ assert.Nil(t, params.AllowedMergeModes)
295+
296+ params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings"), Settings: settings})
297+ require.NoError(t, err)
298+ require.NotNil(t, params.AllowAutoMerge, "settings covers allow_auto_merge")
299+ assert.True(t, *params.AllowAutoMerge)
300+ require.NotNil(t, params.FailingChecksBlock)
301+ require.NotNil(t, params.AllowedMergeModes)
302+
303+ params, err = updateParams(&pb.UpdateRepoRequest{UpdateMask: mask("settings.failing_checks_block"), Settings: settings})
304+ require.NoError(t, err)
305+ assert.Nil(t, params.AllowAutoMerge)
306+ require.NotNil(t, params.FailingChecksBlock)
307+}

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