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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 728 | 728 | if err != nil { | |
| 729 | 729 | 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 | 743 | return mustMarshal(&storagepb.MergeRequest{ | |
| 732 | 744 | Number: mr.Number, | |
| 733 | 745 | Title: mr.Title.String(), | |
| @@ -743,6 +755,8 @@ func encodeMergeRequest(mr domain.MergeRequest) ([]byte, error) { | |||
| 743 | 755 | MergedCommitId: mr.MergedCommitID.String(), | |
| 744 | 756 | MergeMode: mode, | |
| 745 | 757 | CommentSeq: mr.CommentSeq, | |
| 758 | + | AutoMerge: autoMerge, | |
| 759 | + | AutoMerged: mr.AutoMerged, | |
| 746 | 760 | }), nil | |
| 747 | 761 | } | |
| 748 | 762 | ||
| @@ -773,8 +787,24 @@ func decodeMergeRequest(repo domain.RepoName, item kv.Item) (domain.MergeRequest | |||
| 773 | 787 | CreatedAt: decodeTime(pb.GetCreatedAt()), UpdatedAt: decodeTime(pb.GetUpdatedAt()), | |
| 774 | 788 | MergeMode: decodeMode(pb.GetMergeMode()), | |
| 775 | 789 | CommentSeq: pb.GetCommentSeq(), | |
| 790 | + | AutoMerged: pb.GetAutoMerged(), | |
| 776 | 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 | 808 | for _, f := range []struct { | |
| 779 | 809 | dst *domain.ObjectID | |
| 780 | 810 | src string | |
| @@ -909,7 +939,7 @@ func decodeSide(s storagepb.DiffSide) domain.DiffSide { | |||
| 909 | 939 | } | |
| 910 | 940 | ||
| 911 | 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 | 943 | for _, m := range s.AllowedMergeModes { | |
| 914 | 944 | mode, err := encodeMode(m) | |
| 915 | 945 | if err != nil || mode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED { | |
| @@ -949,5 +979,5 @@ func decodeSettings(pb *storagepb.RepoSettings) domain.RepoSettings { | |||
| 949 | 979 | if !pb.GetMergeModesV2() && slices.Equal(normalized, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash}) { | |
| 950 | 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 | } | |
| @@ -728,6 +728,18 @@ func encodeMergeRequest(mr domain.MergeRequest) ([]byte, error) { | |||||
| 728 | if err != nil { | 728 | if err != nil { | ||
| 729 | return nil, err | 729 | 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 | }), nil | 760 | }), 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.ObjectID | 809 | dst *domain.ObjectID | ||
| 780 | src string | 810 | 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) { | |||
| 94 | 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 | 126 | func TestMergeRequestListIsNewestFirstAndPerRepository(t *testing.T) { | |
| 98 | 127 | ctx := context.Background() | |
| 99 | 128 | s, mrs, n := repoWithMRs(t) | |
| @@ -453,6 +482,24 @@ func TestRepoSettings(t *testing.T) { | |||
| 453 | 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 | 503 | // TestRepoWithoutStoredSettingsReadsTheDefaults: a repository stored before | |
| 457 | 504 | // merge requests existed has no settings field. | |
| 458 | 505 | 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 before | 503 | // 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 { | |||
| 131 | 131 | At time.Time | |
| 132 | 132 | Visibility Visibility | |
| 133 | 133 | CreatedAt time.Time | |
| 134 | + | // Auto says that auto-merge made the merge. | |
| 135 | + | Auto bool | |
| 134 | 136 | } | |
| 135 | 137 | ||
| 136 | 138 | // EventKind implements Event. | |
| 137 | 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 | 167 | // MergeRequestClosed is published when a merge request was closed without | |
| 140 | 168 | // merging. | |
| 141 | 169 | type MergeRequestClosed struct { | |
| @@ -131,11 +131,39 @@ type MergeRequestMerged struct { | |||||
| 131 | At time.Time | 131 | At time.Time | ||
| 132 | Visibility Visibility | 132 | Visibility Visibility | ||
| 133 | CreatedAt time.Time | 133 | 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 without | 167 | // 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) { | |||
| 52 | 52 | assert.Equal(t, "repo.pushed", domain.PushEvent{}.EventKind()) | |
| 53 | 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 | + | } | |
| @@ -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 { | |||
| 50 | 50 | FailingChecksBlock bool | |
| 51 | 51 | // Issues says who may write issues; the empty mode means IssuesOff. | |
| 52 | 52 | 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 | 58 | // DefaultRepoSettings are the settings of a new repository: every mode, and | |
| @@ -139,11 +142,29 @@ type MergeRequest struct { | |||
| 139 | 142 | MergeMode MergeMode | |
| 140 | 143 | // CommentSeq is the id of the last comment of the request. | |
| 141 | 144 | 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 | 149 | // Version is set by the store on read: Update fails with ErrConflict when | |
| 143 | 150 | // the stored request changed since. Opaque to everyone else. | |
| 144 | 151 | 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 | 168 | // draftPrefixes mark a draft at the start of a title, compared without regard | |
| 148 | 169 | // to case, as Forgejo and Gitea do. | |
| 149 | 170 | var draftPrefixes = []string{"wip:", "[wip]"} | |
| @@ -50,6 +50,9 @@ type RepoSettings struct { | |||||
| 50 | FailingChecksBlock bool | 50 | 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 IssuesMode | 52 | 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, and | 58 | // DefaultRepoSettings are the settings of a new repository: every mode, and | ||
| @@ -139,11 +142,29 @@ type MergeRequest struct { | |||||
| 139 | MergeMode MergeMode | 142 | 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 int64 | 144 | 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 when | 149 | // 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 uint64 | 151 | 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 regard | 168 | // 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) { | |||
| 330 | 330 | assert.False(t, m.IsFor(oid(t, 'a'), oid(t, 'c'))) | |
| 331 | 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 | + | } | |
| @@ -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 { | |||
| 30 | 30 | // {fast-forward, squash} was the old default and gains MERGE_MODE_MERGE on | |
| 31 | 31 | // read. | |
| 32 | 32 | bool merge_modes_v2 = 4; | |
| 33 | + | bool allow_auto_merge = 5; | |
| 33 | 34 | } | |
| 34 | 35 | ||
| 35 | 36 | // IssuesMode says who may write issues in a repository. | |
| @@ -78,6 +79,20 @@ message MergeRequest { | |||
| 78 | 79 | MergeMode merge_mode = 13; | |
| 79 | 80 | // Last comment id handed out for this request. | |
| 80 | 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 | 98 | // 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 on | 30 | // {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. | ||