detached 4bb6db7

feat(mr): store auto-merge state and allow_auto_merge setting

commit
4bb6db7919afdadd47a988c5ec165937b4bf960c
author
brice · · 2026-10-20 15:44 UTC
parent
3ed7469
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +319 -37
M gen/ermite/storage/merge_request.pb.go +163 -35 generated file: collapsed
M internal/adapters/kvstore/mergerequests.go +32 -2
@@ -728,6 +728,18 @@ func encodeMergeRequest(mr domain.MergeRequest) ([]byte, error) {
728728 if err != nil {
729729 return nil, err
730730 }
731+ var autoMerge *storagepb.AutoMerge
732+ if am := mr.AutoMerge; am != nil {
733+ amMode, err := encodeMode(am.Mode)
734+ if err != nil || amMode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED {
735+ return nil, fmt.Errorf("%w: merge mode %q in the auto-merge", domain.ErrInvalid, am.Mode)
736+ }
737+ autoMerge = &storagepb.AutoMerge{
738+ Mode: amMode, HeadId: am.HeadID.String(),
739+ EnablerKind: string(am.EnabledBy.Kind), EnablerTokenId: am.EnabledBy.TokenID,
740+ EnablerLabel: am.EnablerLabel, EnabledAt: encodeTime(am.EnabledAt),
741+ }
742+ }
731743 return mustMarshal(&storagepb.MergeRequest{
732744 Number: mr.Number,
733745 Title: mr.Title.String(),
@@ -743,6 +755,8 @@ func encodeMergeRequest(mr domain.MergeRequest) ([]byte, error) {
743755 MergedCommitId: mr.MergedCommitID.String(),
744756 MergeMode: mode,
745757 CommentSeq: mr.CommentSeq,
758+ AutoMerge: autoMerge,
759+ AutoMerged: mr.AutoMerged,
746760 }), nil
747761 }
748762
@@ -773,8 +787,24 @@ func decodeMergeRequest(repo domain.RepoName, item kv.Item) (domain.MergeRequest
773787 CreatedAt: decodeTime(pb.GetCreatedAt()), UpdatedAt: decodeTime(pb.GetUpdatedAt()),
774788 MergeMode: decodeMode(pb.GetMergeMode()),
775789 CommentSeq: pb.GetCommentSeq(),
790+ AutoMerged: pb.GetAutoMerged(),
776791 Version: item.Version,
777792 }
793+ if am := pb.GetAutoMerge(); am != nil {
794+ head, err := domain.ParseObjectID(am.GetHeadId())
795+ if err != nil {
796+ return fail(err)
797+ }
798+ mode := decodeMode(am.GetMode())
799+ if mode == "" {
800+ return fail(fmt.Errorf("unknown auto-merge mode %d", am.GetMode()))
801+ }
802+ mr.AutoMerge = &domain.AutoMerge{
803+ Mode: mode, HeadID: head,
804+ EnabledBy: domain.CIPusher{Kind: domain.CIPusherKind(am.GetEnablerKind()), TokenID: am.GetEnablerTokenId()},
805+ EnablerLabel: am.GetEnablerLabel(), EnabledAt: decodeTime(am.GetEnabledAt()),
806+ }
807+ }
778808 for _, f := range []struct {
779809 dst *domain.ObjectID
780810 src string
@@ -909,7 +939,7 @@ func decodeSide(s storagepb.DiffSide) domain.DiffSide {
909939 }
910940
911941 func encodeSettings(s domain.RepoSettings) (*storagepb.RepoSettings, error) {
912- pb := &storagepb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, MergeModesV2: true}
942+ pb := &storagepb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, MergeModesV2: true, AllowAutoMerge: s.AllowAutoMerge}
913943 for _, m := range s.AllowedMergeModes {
914944 mode, err := encodeMode(m)
915945 if err != nil || mode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED {
@@ -949,5 +979,5 @@ func decodeSettings(pb *storagepb.RepoSettings) domain.RepoSettings {
949979 if !pb.GetMergeModesV2() && slices.Equal(normalized, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash}) {
950980 normalized = domain.MergeModes()
951981 }
952- return domain.RepoSettings{AllowedMergeModes: normalized, FailingChecksBlock: pb.GetFailingChecksBlock(), Issues: decodeIssuesMode(pb.GetIssues())}
982+ return domain.RepoSettings{AllowedMergeModes: normalized, FailingChecksBlock: pb.GetFailingChecksBlock(), Issues: decodeIssuesMode(pb.GetIssues()), AllowAutoMerge: pb.GetAllowAutoMerge()}
953983 }
@@ -728,6 +728,18 @@ func encodeMergeRequest(mr domain.MergeRequest) ([]byte, error) {
728 if err != nil {728 if err != nil {
729 return nil, err729 return nil, err
730 }730 }
731+ var autoMerge *storagepb.AutoMerge
732+ if am := mr.AutoMerge; am != nil {
733+ amMode, err := encodeMode(am.Mode)
734+ if err != nil || amMode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED {
735+ return nil, fmt.Errorf("%w: merge mode %q in the auto-merge", domain.ErrInvalid, am.Mode)
736+ }
737+ autoMerge = &storagepb.AutoMerge{
738+ Mode: amMode, HeadId: am.HeadID.String(),
739+ EnablerKind: string(am.EnabledBy.Kind), EnablerTokenId: am.EnabledBy.TokenID,
740+ EnablerLabel: am.EnablerLabel, EnabledAt: encodeTime(am.EnabledAt),
741+ }
742+ }
731 return mustMarshal(&storagepb.MergeRequest{743 return mustMarshal(&storagepb.MergeRequest{
732 Number: mr.Number,744 Number: mr.Number,
733 Title: mr.Title.String(),745 Title: mr.Title.String(),
@@ -743,6 +755,8 @@ func encodeMergeRequest(mr domain.MergeRequest) ([]byte, error) {
743 MergedCommitId: mr.MergedCommitID.String(),755 MergedCommitId: mr.MergedCommitID.String(),
744 MergeMode: mode,756 MergeMode: mode,
745 CommentSeq: mr.CommentSeq,757 CommentSeq: mr.CommentSeq,
758+ AutoMerge: autoMerge,
759+ AutoMerged: mr.AutoMerged,
746 }), nil760 }), nil
747 }761 }
748 762
@@ -773,8 +787,24 @@ func decodeMergeRequest(repo domain.RepoName, item kv.Item) (domain.MergeRequest
773 CreatedAt: decodeTime(pb.GetCreatedAt()), UpdatedAt: decodeTime(pb.GetUpdatedAt()),787 CreatedAt: decodeTime(pb.GetCreatedAt()), UpdatedAt: decodeTime(pb.GetUpdatedAt()),
774 MergeMode: decodeMode(pb.GetMergeMode()),788 MergeMode: decodeMode(pb.GetMergeMode()),
775 CommentSeq: pb.GetCommentSeq(),789 CommentSeq: pb.GetCommentSeq(),
790+ AutoMerged: pb.GetAutoMerged(),
776 Version: item.Version,791 Version: item.Version,
777 }792 }
793+ if am := pb.GetAutoMerge(); am != nil {
794+ head, err := domain.ParseObjectID(am.GetHeadId())
795+ if err != nil {
796+ return fail(err)
797+ }
798+ mode := decodeMode(am.GetMode())
799+ if mode == "" {
800+ return fail(fmt.Errorf("unknown auto-merge mode %d", am.GetMode()))
801+ }
802+ mr.AutoMerge = &domain.AutoMerge{
803+ Mode: mode, HeadID: head,
804+ EnabledBy: domain.CIPusher{Kind: domain.CIPusherKind(am.GetEnablerKind()), TokenID: am.GetEnablerTokenId()},
805+ EnablerLabel: am.GetEnablerLabel(), EnabledAt: decodeTime(am.GetEnabledAt()),
806+ }
807+ }
778 for _, f := range []struct {808 for _, f := range []struct {
779 dst *domain.ObjectID809 dst *domain.ObjectID
780 src string810 src string
@@ -909,7 +939,7 @@ func decodeSide(s storagepb.DiffSide) domain.DiffSide {
909 }939 }
910 940
911 func encodeSettings(s domain.RepoSettings) (*storagepb.RepoSettings, error) {941 func encodeSettings(s domain.RepoSettings) (*storagepb.RepoSettings, error) {
912- pb := &storagepb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, MergeModesV2: true}942+ pb := &storagepb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock, MergeModesV2: true, AllowAutoMerge: s.AllowAutoMerge}
913 for _, m := range s.AllowedMergeModes {943 for _, m := range s.AllowedMergeModes {
914 mode, err := encodeMode(m)944 mode, err := encodeMode(m)
915 if err != nil || mode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED {945 if err != nil || mode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED {
@@ -949,5 +979,5 @@ func decodeSettings(pb *storagepb.RepoSettings) domain.RepoSettings {
949 if !pb.GetMergeModesV2() && slices.Equal(normalized, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash}) {979 if !pb.GetMergeModesV2() && slices.Equal(normalized, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash}) {
950 normalized = domain.MergeModes()980 normalized = domain.MergeModes()
951 }981 }
952- return domain.RepoSettings{AllowedMergeModes: normalized, FailingChecksBlock: pb.GetFailingChecksBlock(), Issues: decodeIssuesMode(pb.GetIssues())}982+ return domain.RepoSettings{AllowedMergeModes: normalized, FailingChecksBlock: pb.GetFailingChecksBlock(), Issues: decodeIssuesMode(pb.GetIssues()), AllowAutoMerge: pb.GetAllowAutoMerge()}
953 }983 }
M internal/adapters/kvstore/mergerequests_test.go +47 -0
@@ -94,6 +94,35 @@ func TestMergeRequestRoundTrip(t *testing.T) {
9494 assert.ErrorIs(t, err, domain.ErrNotFound)
9595 }
9696
97+func TestMergeRequestAutoMergeRoundTrip(t *testing.T) {
98+ ctx := context.Background()
99+ _, mrs, n := repoWithMRs(t)
100+
101+ set := newMR(t, n, "Auto")
102+ set.AutoMerge = &domain.AutoMerge{
103+ Mode: domain.MergeModeSquash, HeadID: oid('d'),
104+ EnabledBy: domain.CIPusher{Kind: domain.CIPusherToken, TokenID: "tok1"},
105+ EnablerLabel: "ci-bot", EnabledAt: created,
106+ }
107+ created1, err := mrs.Create(ctx, set)
108+ require.NoError(t, err)
109+ got, err := mrs.Get(ctx, n, created1.Number)
110+ require.NoError(t, err)
111+ require.NotNil(t, got.AutoMerge)
112+ assert.Equal(t, *set.AutoMerge, *got.AutoMerge)
113+ assert.False(t, got.AutoMerged)
114+
115+ unset := newMR(t, n, "Merged by auto")
116+ unset.State = domain.MRStateMerged
117+ unset.AutoMerged = true
118+ created2, err := mrs.Create(ctx, unset)
119+ require.NoError(t, err)
120+ got, err = mrs.Get(ctx, n, created2.Number)
121+ require.NoError(t, err)
122+ assert.Nil(t, got.AutoMerge)
123+ assert.True(t, got.AutoMerged)
124+}
125+
97126 func TestMergeRequestListIsNewestFirstAndPerRepository(t *testing.T) {
98127 ctx := context.Background()
99128 s, mrs, n := repoWithMRs(t)
@@ -453,6 +482,24 @@ func TestRepoSettings(t *testing.T) {
453482 assert.ErrorIs(t, err, domain.ErrInvalid)
454483 }
455484
485+func TestRepoSettingsKeepAllowAutoMerge(t *testing.T) {
486+ ctx := context.Background()
487+ s := openStore(t, t.TempDir())
488+ n := name(t, "proj")
489+ require.NoError(t, s.Create(ctx, domain.Repo{Name: n, CreatedAt: created, Visibility: domain.VisibilityPrivate}))
490+ got, err := s.Get(ctx, n)
491+ require.NoError(t, err)
492+ assert.False(t, got.Settings.AllowAutoMerge)
493+
494+ settings := domain.DefaultRepoSettings()
495+ settings.AllowAutoMerge = true
496+ _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = settings; return true, nil })
497+ require.NoError(t, err)
498+ got, err = s.Get(ctx, n)
499+ require.NoError(t, err)
500+ assert.Equal(t, settings, got.Settings)
501+}
502+
456503 // TestRepoWithoutStoredSettingsReadsTheDefaults: a repository stored before
457504 // merge requests existed has no settings field.
458505 func TestRepoWithoutStoredSettingsReadsTheDefaults(t *testing.T) {
@@ -94,6 +94,35 @@ func TestMergeRequestRoundTrip(t *testing.T) {
94 assert.ErrorIs(t, err, domain.ErrNotFound)94 assert.ErrorIs(t, err, domain.ErrNotFound)
95 }95 }
96 96
97+func TestMergeRequestAutoMergeRoundTrip(t *testing.T) {
98+ ctx := context.Background()
99+ _, mrs, n := repoWithMRs(t)
100+
101+ set := newMR(t, n, "Auto")
102+ set.AutoMerge = &domain.AutoMerge{
103+ Mode: domain.MergeModeSquash, HeadID: oid('d'),
104+ EnabledBy: domain.CIPusher{Kind: domain.CIPusherToken, TokenID: "tok1"},
105+ EnablerLabel: "ci-bot", EnabledAt: created,
106+ }
107+ created1, err := mrs.Create(ctx, set)
108+ require.NoError(t, err)
109+ got, err := mrs.Get(ctx, n, created1.Number)
110+ require.NoError(t, err)
111+ require.NotNil(t, got.AutoMerge)
112+ assert.Equal(t, *set.AutoMerge, *got.AutoMerge)
113+ assert.False(t, got.AutoMerged)
114+
115+ unset := newMR(t, n, "Merged by auto")
116+ unset.State = domain.MRStateMerged
117+ unset.AutoMerged = true
118+ created2, err := mrs.Create(ctx, unset)
119+ require.NoError(t, err)
120+ got, err = mrs.Get(ctx, n, created2.Number)
121+ require.NoError(t, err)
122+ assert.Nil(t, got.AutoMerge)
123+ assert.True(t, got.AutoMerged)
124+}
125+
97 func TestMergeRequestListIsNewestFirstAndPerRepository(t *testing.T) {126 func TestMergeRequestListIsNewestFirstAndPerRepository(t *testing.T) {
98 ctx := context.Background()127 ctx := context.Background()
99 s, mrs, n := repoWithMRs(t)128 s, mrs, n := repoWithMRs(t)
@@ -453,6 +482,24 @@ func TestRepoSettings(t *testing.T) {
453 assert.ErrorIs(t, err, domain.ErrInvalid)482 assert.ErrorIs(t, err, domain.ErrInvalid)
454 }483 }
455 484
485+func TestRepoSettingsKeepAllowAutoMerge(t *testing.T) {
486+ ctx := context.Background()
487+ s := openStore(t, t.TempDir())
488+ n := name(t, "proj")
489+ require.NoError(t, s.Create(ctx, domain.Repo{Name: n, CreatedAt: created, Visibility: domain.VisibilityPrivate}))
490+ got, err := s.Get(ctx, n)
491+ require.NoError(t, err)
492+ assert.False(t, got.Settings.AllowAutoMerge)
493+
494+ settings := domain.DefaultRepoSettings()
495+ settings.AllowAutoMerge = true
496+ _, err = s.Mutate(ctx, n, func(r *domain.Repo) (bool, error) { r.Settings = settings; return true, nil })
497+ require.NoError(t, err)
498+ got, err = s.Get(ctx, n)
499+ require.NoError(t, err)
500+ assert.Equal(t, settings, got.Settings)
501+}
502+
456 // TestRepoWithoutStoredSettingsReadsTheDefaults: a repository stored before503 // TestRepoWithoutStoredSettingsReadsTheDefaults: a repository stored before
457 // merge requests existed has no settings field.504 // merge requests existed has no settings field.
458 func TestRepoWithoutStoredSettingsReadsTheDefaults(t *testing.T) {505 func TestRepoWithoutStoredSettingsReadsTheDefaults(t *testing.T) {
M internal/domain/event.go +28 -0
@@ -131,11 +131,39 @@ type MergeRequestMerged struct {
131131 At time.Time
132132 Visibility Visibility
133133 CreatedAt time.Time
134+ // Auto says that auto-merge made the merge.
135+ Auto bool
134136 }
135137
136138 // EventKind implements Event.
137139 func (MergeRequestMerged) EventKind() string { return "mr.merged" }
138140
141+// MergeRequestAutoMergeEnabled is published when auto-merge was enabled on a
142+// merge request.
143+type MergeRequestAutoMergeEnabled struct {
144+ MergeRequest MergeRequest
145+ At time.Time
146+ Visibility Visibility
147+ CreatedAt time.Time
148+}
149+
150+// EventKind implements Event.
151+func (MergeRequestAutoMergeEnabled) EventKind() string { return "mr.auto_merge_enabled" }
152+
153+// MergeRequestAutoMergeCancelled is published when auto-merge was cancelled
154+// on a merge request, by hand or by the server.
155+type MergeRequestAutoMergeCancelled struct {
156+ MergeRequest MergeRequest
157+ // Reason says why, in words.
158+ Reason string
159+ At time.Time
160+ Visibility Visibility
161+ CreatedAt time.Time
162+}
163+
164+// EventKind implements Event.
165+func (MergeRequestAutoMergeCancelled) EventKind() string { return "mr.auto_merge_cancelled" }
166+
139167 // MergeRequestClosed is published when a merge request was closed without
140168 // merging.
141169 type MergeRequestClosed struct {
@@ -131,11 +131,39 @@ type MergeRequestMerged struct {
131 At time.Time131 At time.Time
132 Visibility Visibility132 Visibility Visibility
133 CreatedAt time.Time133 CreatedAt time.Time
134+ // Auto says that auto-merge made the merge.
135+ Auto bool
134 }136 }
135 137
136 // EventKind implements Event.138 // EventKind implements Event.
137 func (MergeRequestMerged) EventKind() string { return "mr.merged" }139 func (MergeRequestMerged) EventKind() string { return "mr.merged" }
138 140
141+// MergeRequestAutoMergeEnabled is published when auto-merge was enabled on a
142+// merge request.
143+type MergeRequestAutoMergeEnabled struct {
144+ MergeRequest MergeRequest
145+ At time.Time
146+ Visibility Visibility
147+ CreatedAt time.Time
148+}
149+
150+// EventKind implements Event.
151+func (MergeRequestAutoMergeEnabled) EventKind() string { return "mr.auto_merge_enabled" }
152+
153+// MergeRequestAutoMergeCancelled is published when auto-merge was cancelled
154+// on a merge request, by hand or by the server.
155+type MergeRequestAutoMergeCancelled struct {
156+ MergeRequest MergeRequest
157+ // Reason says why, in words.
158+ Reason string
159+ At time.Time
160+ Visibility Visibility
161+ CreatedAt time.Time
162+}
163+
164+// EventKind implements Event.
165+func (MergeRequestAutoMergeCancelled) EventKind() string { return "mr.auto_merge_cancelled" }
166+
139 // MergeRequestClosed is published when a merge request was closed without167 // MergeRequestClosed is published when a merge request was closed without
140 // merging.168 // merging.
141 type MergeRequestClosed struct {169 type MergeRequestClosed struct {
M internal/domain/event_test.go +7 -0
@@ -52,3 +52,10 @@ func TestEventKinds(t *testing.T) {
5252 assert.Equal(t, "repo.pushed", domain.PushEvent{}.EventKind())
5353 assert.Equal(t, "gap", domain.EventGap{}.EventKind())
5454 }
55+
56+func TestAutoMergeEventKinds(t *testing.T) {
57+ t.Parallel()
58+ assert.Equal(t, "mr.auto_merge_enabled", domain.MergeRequestAutoMergeEnabled{}.EventKind())
59+ assert.Equal(t, "mr.auto_merge_cancelled", domain.MergeRequestAutoMergeCancelled{}.EventKind())
60+ assert.Equal(t, "mr.merged", domain.MergeRequestMerged{Auto: true}.EventKind())
61+}
@@ -52,3 +52,10 @@ func TestEventKinds(t *testing.T) {
52 assert.Equal(t, "repo.pushed", domain.PushEvent{}.EventKind())52 assert.Equal(t, "repo.pushed", domain.PushEvent{}.EventKind())
53 assert.Equal(t, "gap", domain.EventGap{}.EventKind())53 assert.Equal(t, "gap", domain.EventGap{}.EventKind())
54 }54 }
55+
56+func TestAutoMergeEventKinds(t *testing.T) {
57+ t.Parallel()
58+ assert.Equal(t, "mr.auto_merge_enabled", domain.MergeRequestAutoMergeEnabled{}.EventKind())
59+ assert.Equal(t, "mr.auto_merge_cancelled", domain.MergeRequestAutoMergeCancelled{}.EventKind())
60+ assert.Equal(t, "mr.merged", domain.MergeRequestMerged{Auto: true}.EventKind())
61+}
M internal/domain/mergerequest.go +21 -0
@@ -50,6 +50,9 @@ type RepoSettings struct {
5050 FailingChecksBlock bool
5151 // Issues says who may write issues; the empty mode means IssuesOff.
5252 Issues IssuesMode
53+ // AllowAutoMerge says that merge requests may be set to merge themselves
54+ // once their checks pass. Off by default.
55+ AllowAutoMerge bool
5356 }
5457
5558 // DefaultRepoSettings are the settings of a new repository: every mode, and
@@ -139,11 +142,29 @@ type MergeRequest struct {
139142 MergeMode MergeMode
140143 // CommentSeq is the id of the last comment of the request.
141144 CommentSeq int64
145+ // AutoMerge is the pending auto-merge; nil when off.
146+ AutoMerge *AutoMerge
147+ // AutoMerged says that the request was merged by auto-merge.
148+ AutoMerged bool
142149 // Version is set by the store on read: Update fails with ErrConflict when
143150 // the stored request changed since. Opaque to everyone else.
144151 Version uint64
145152 }
146153
154+// AutoMerge is the pending auto-merge of an open merge request.
155+type AutoMerge struct {
156+ // Mode is the merge mode chosen when auto-merge was enabled.
157+ Mode MergeMode
158+ // HeadID is the head it was enabled for; a new head cancels it.
159+ HeadID ObjectID
160+ // EnabledBy is who enabled it; the merge is made on their behalf.
161+ EnabledBy CIPusher
162+ // EnablerLabel is the display name of the enabler.
163+ EnablerLabel string
164+ // EnabledAt is when auto-merge was enabled.
165+ EnabledAt time.Time
166+}
167+
147168 // draftPrefixes mark a draft at the start of a title, compared without regard
148169 // to case, as Forgejo and Gitea do.
149170 var draftPrefixes = []string{"wip:", "[wip]"}
@@ -50,6 +50,9 @@ type RepoSettings struct {
50 FailingChecksBlock bool50 FailingChecksBlock bool
51 // Issues says who may write issues; the empty mode means IssuesOff.51 // Issues says who may write issues; the empty mode means IssuesOff.
52 Issues IssuesMode52 Issues IssuesMode
53+ // AllowAutoMerge says that merge requests may be set to merge themselves
54+ // once their checks pass. Off by default.
55+ AllowAutoMerge bool
53 }56 }
54 57
55 // DefaultRepoSettings are the settings of a new repository: every mode, and58 // DefaultRepoSettings are the settings of a new repository: every mode, and
@@ -139,11 +142,29 @@ type MergeRequest struct {
139 MergeMode MergeMode142 MergeMode MergeMode
140 // CommentSeq is the id of the last comment of the request.143 // CommentSeq is the id of the last comment of the request.
141 CommentSeq int64144 CommentSeq int64
145+ // AutoMerge is the pending auto-merge; nil when off.
146+ AutoMerge *AutoMerge
147+ // AutoMerged says that the request was merged by auto-merge.
148+ AutoMerged bool
142 // Version is set by the store on read: Update fails with ErrConflict when149 // Version is set by the store on read: Update fails with ErrConflict when
143 // the stored request changed since. Opaque to everyone else.150 // the stored request changed since. Opaque to everyone else.
144 Version uint64151 Version uint64
145 }152 }
146 153
154+// AutoMerge is the pending auto-merge of an open merge request.
155+type AutoMerge struct {
156+ // Mode is the merge mode chosen when auto-merge was enabled.
157+ Mode MergeMode
158+ // HeadID is the head it was enabled for; a new head cancels it.
159+ HeadID ObjectID
160+ // EnabledBy is who enabled it; the merge is made on their behalf.
161+ EnabledBy CIPusher
162+ // EnablerLabel is the display name of the enabler.
163+ EnablerLabel string
164+ // EnabledAt is when auto-merge was enabled.
165+ EnabledAt time.Time
166+}
167+
147 // draftPrefixes mark a draft at the start of a title, compared without regard168 // draftPrefixes mark a draft at the start of a title, compared without regard
148 // to case, as Forgejo and Gitea do.169 // to case, as Forgejo and Gitea do.
149 var draftPrefixes = []string{"wip:", "[wip]"}170 var draftPrefixes = []string{"wip:", "[wip]"}
M internal/domain/mergerequest_test.go +6 -0
@@ -330,3 +330,9 @@ func TestMergeabilityAllowsAndIsFor(t *testing.T) {
330330 assert.False(t, m.IsFor(oid(t, 'a'), oid(t, 'c')))
331331 assert.False(t, domain.Mergeability{}.IsFor(domain.ObjectID{}, domain.ObjectID{}))
332332 }
333+
334+func TestRepoSettingsAllowAutoMergeDefaultOff(t *testing.T) {
335+ t.Parallel()
336+ assert.False(t, domain.DefaultRepoSettings().AllowAutoMerge)
337+ assert.False(t, domain.RepoSettings{}.AllowAutoMerge)
338+}
@@ -330,3 +330,9 @@ func TestMergeabilityAllowsAndIsFor(t *testing.T) {
330 assert.False(t, m.IsFor(oid(t, 'a'), oid(t, 'c')))330 assert.False(t, m.IsFor(oid(t, 'a'), oid(t, 'c')))
331 assert.False(t, domain.Mergeability{}.IsFor(domain.ObjectID{}, domain.ObjectID{}))331 assert.False(t, domain.Mergeability{}.IsFor(domain.ObjectID{}, domain.ObjectID{}))
332 }332 }
333+
334+func TestRepoSettingsAllowAutoMergeDefaultOff(t *testing.T) {
335+ t.Parallel()
336+ assert.False(t, domain.DefaultRepoSettings().AllowAutoMerge)
337+ assert.False(t, domain.RepoSettings{}.AllowAutoMerge)
338+}
M proto/ermite/storage/merge_request.proto +15 -0
@@ -30,6 +30,7 @@ message RepoSettings {
3030 // {fast-forward, squash} was the old default and gains MERGE_MODE_MERGE on
3131 // read.
3232 bool merge_modes_v2 = 4;
33+ bool allow_auto_merge = 5;
3334 }
3435
3536 // IssuesMode says who may write issues in a repository.
@@ -78,6 +79,20 @@ message MergeRequest {
7879 MergeMode merge_mode = 13;
7980 // Last comment id handed out for this request.
8081 int64 comment_seq = 14;
82+ // Pending auto-merge; unset when off.
83+ AutoMerge auto_merge = 15;
84+ bool auto_merged = 16;
85+}
86+
87+// AutoMerge is the pending auto-merge of an open merge request.
88+message AutoMerge {
89+ MergeMode mode = 1;
90+ string head_id = 2;
91+ // Kind and token id of the CIPusher that enabled it.
92+ string enabler_kind = 3;
93+ string enabler_token_id = 4;
94+ string enabler_label = 5;
95+ google.protobuf.Timestamp enabled_at = 6;
8196 }
8297
8398 // Verdict is the immutable review decision of a note.
@@ -30,6 +30,7 @@ message RepoSettings {
30 // {fast-forward, squash} was the old default and gains MERGE_MODE_MERGE on30 // {fast-forward, squash} was the old default and gains MERGE_MODE_MERGE on
31 // read.31 // read.
32 bool merge_modes_v2 = 4;32 bool merge_modes_v2 = 4;
33+ bool allow_auto_merge = 5;
33 }34 }
34 35
35 // IssuesMode says who may write issues in a repository.36 // IssuesMode says who may write issues in a repository.
@@ -78,6 +79,20 @@ message MergeRequest {
78 MergeMode merge_mode = 13;79 MergeMode merge_mode = 13;
79 // Last comment id handed out for this request.80 // Last comment id handed out for this request.
80 int64 comment_seq = 14;81 int64 comment_seq = 14;
82+ // Pending auto-merge; unset when off.
83+ AutoMerge auto_merge = 15;
84+ bool auto_merged = 16;
85+}
86+
87+// AutoMerge is the pending auto-merge of an open merge request.
88+message AutoMerge {
89+ MergeMode mode = 1;
90+ string head_id = 2;
91+ // Kind and token id of the CIPusher that enabled it.
92+ string enabler_kind = 3;
93+ string enabler_token_id = 4;
94+ string enabler_label = 5;
95+ google.protobuf.Timestamp enabled_at = 6;
81 }96 }
82 97
83 // Verdict is the immutable review decision of a note.98 // Verdict is the immutable review decision of a note.

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 43

↑↓ moveenter openesc close