Skip to content

Commit 4d7da05

Browse files
Merge pull request #532 from jakubbortlik/feat/implement-mergeability-checks
feat: add mergeability checks to summary view
2 parents 3d2828a + 6ed0956 commit 4d7da05

12 files changed

Lines changed: 415 additions & 33 deletions

File tree

cmd/app/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Client struct {
3232
gitlab.UsersServiceInterface
3333
gitlab.DraftNotesServiceInterface
3434
gitlab.ProjectMarkdownUploadsServiceInterface
35+
gitlab.GraphQLInterface
3536
}
3637

3738
/* NewClient parses and validates the project settings and initializes the Gitlab client. */
@@ -100,6 +101,7 @@ func NewClient() (*Client, error) {
100101
client.Users,
101102
client.DraftNotes,
102103
client.ProjectMarkdownUploads,
104+
client.GraphQL,
103105
}, nil
104106
}
105107

cmd/app/mergeability_checks.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package app
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
8+
gitlab "gitlab.com/gitlab-org/api/client-go"
9+
)
10+
11+
type MergeabilityCheck struct {
12+
Identifier string `json:"identifier"`
13+
Status string `json:"status"`
14+
}
15+
16+
type MergeabilityChecksResponse struct {
17+
SuccessResponse
18+
MergeabilityChecks []*MergeabilityCheck `json:"mergeability_checks"`
19+
}
20+
21+
type mergeabilityChecksGraphQLResponse struct {
22+
Data struct {
23+
Project struct {
24+
MergeRequest struct {
25+
MergeabilityChecks []*MergeabilityCheck `json:"mergeabilityChecks"`
26+
} `json:"mergeRequest"`
27+
} `json:"project"`
28+
} `json:"data"`
29+
}
30+
31+
const mergeabilityChecksQuery = `
32+
query GetMergeabilityChecks($projectPath: ID!, $iid: String!) {
33+
project(fullPath: $projectPath) {
34+
mergeRequest(iid: $iid) {
35+
mergeabilityChecks {
36+
identifier
37+
status
38+
}
39+
}
40+
}
41+
}
42+
`
43+
44+
type mergeabilityChecksService struct {
45+
data
46+
client gitlab.GraphQLInterface
47+
}
48+
49+
func (a mergeabilityChecksService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
50+
checks, err := a.fetchMergeabilityChecks()
51+
if err != nil {
52+
handleError(w, err, "Could not get mergeability checks", http.StatusInternalServerError)
53+
return
54+
}
55+
56+
w.WriteHeader(http.StatusOK)
57+
response := MergeabilityChecksResponse{
58+
SuccessResponse: SuccessResponse{Message: "Mergeability checks retrieved"},
59+
MergeabilityChecks: checks,
60+
}
61+
62+
err = json.NewEncoder(w).Encode(response)
63+
if err != nil {
64+
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
65+
}
66+
}
67+
68+
func (a mergeabilityChecksService) fetchMergeabilityChecks() ([]*MergeabilityCheck, error) {
69+
var response mergeabilityChecksGraphQLResponse
70+
71+
_, err := a.client.Do(gitlab.GraphQLQuery{
72+
Query: mergeabilityChecksQuery,
73+
Variables: map[string]any{
74+
"projectPath": a.gitInfo.ProjectPath(),
75+
"iid": fmt.Sprintf("%d", a.projectInfo.MergeId),
76+
},
77+
}, &response)
78+
if err != nil {
79+
return nil, fmt.Errorf("failed to fetch mergeability checks: %w", err)
80+
}
81+
82+
return response.Data.Project.MergeRequest.MergeabilityChecks, nil
83+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package app
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/harrisoncramer/gitlab.nvim/cmd/app/git"
10+
gitlab "gitlab.com/gitlab-org/api/client-go"
11+
)
12+
13+
type fakeGraphQLClient struct {
14+
err error
15+
jsonData []byte
16+
}
17+
18+
func (f fakeGraphQLClient) Do(query gitlab.GraphQLQuery, response any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
19+
if f.err != nil {
20+
return nil, f.err
21+
}
22+
23+
// Actually unmarshal JSON into the response struct
24+
if err := json.Unmarshal(f.jsonData, response); err != nil {
25+
return nil, err
26+
}
27+
28+
// if resp, ok := response.(mergeabilityChecksGraphQLResponse); ok {
29+
// resp.Data.Project.MergeRequest.MergeabilityChecks = f.checks
30+
// }
31+
32+
return makeResponse(http.StatusOK), nil
33+
}
34+
35+
var testMergeabilityData = data{
36+
projectInfo: &ProjectInfo{MergeId: 123},
37+
gitInfo: &git.GitData{
38+
BranchName: "feature-branch",
39+
Namespace: "test-namespace",
40+
ProjectName: "test-project",
41+
},
42+
}
43+
44+
func TestMergeabilityChecksHandler(t *testing.T) {
45+
t.Run("Returns mergeability checks", func(t *testing.T) {
46+
request := makeRequest(t, http.MethodGet, "/mr/mergeability_checks", nil)
47+
client := fakeGraphQLClient{
48+
jsonData: []byte(`{
49+
"data": {
50+
"project": {
51+
"mergeRequest": {
52+
"mergeabilityChecks": [
53+
{"identifier": "CI_MUST_PASS", "status": "SUCCESS"},
54+
{"identifier": "CONFLICT", "status": "FAILED"}
55+
]
56+
}
57+
}
58+
}
59+
}`),
60+
}
61+
svc := middleware(
62+
mergeabilityChecksService{testMergeabilityData, client},
63+
withMethodCheck(http.MethodGet),
64+
)
65+
66+
res := httptest.NewRecorder()
67+
svc.ServeHTTP(res, request)
68+
69+
var data MergeabilityChecksResponse
70+
err := json.Unmarshal(res.Body.Bytes(), &data)
71+
assert(t, err, nil)
72+
73+
assert(t, data.Message, "Mergeability checks retrieved")
74+
assert(t, len(data.MergeabilityChecks), 2)
75+
assert(t, data.MergeabilityChecks[0].Identifier, "CI_MUST_PASS")
76+
assert(t, data.MergeabilityChecks[0].Status, "SUCCESS")
77+
assert(t, data.MergeabilityChecks[1].Identifier, "CONFLICT")
78+
assert(t, data.MergeabilityChecks[1].Status, "FAILED")
79+
})
80+
81+
t.Run("Returns empty list when there are no checks", func(t *testing.T) {
82+
request := makeRequest(t, http.MethodGet, "/mr/mergeability_checks", nil)
83+
client := fakeGraphQLClient{
84+
jsonData: []byte(`{
85+
"data": {
86+
"project": {
87+
"mergeRequest": {
88+
"mergeabilityChecks": []
89+
}
90+
}
91+
}
92+
}`),
93+
}
94+
svc := middleware(
95+
mergeabilityChecksService{testMergeabilityData, client},
96+
withMethodCheck(http.MethodGet),
97+
)
98+
99+
res := httptest.NewRecorder()
100+
svc.ServeHTTP(res, request)
101+
102+
var data MergeabilityChecksResponse
103+
err := json.Unmarshal(res.Body.Bytes(), &data)
104+
assert(t, err, nil)
105+
106+
assert(t, data.Message, "Mergeability checks retrieved")
107+
assert(t, len(data.MergeabilityChecks), 0)
108+
})
109+
110+
t.Run("Handles errors from Gitlab client", func(t *testing.T) {
111+
request := makeRequest(t, http.MethodGet, "/mr/mergeability_checks", nil)
112+
client := fakeGraphQLClient{err: errorFromGitlab}
113+
svc := middleware(
114+
mergeabilityChecksService{testMergeabilityData, client},
115+
withMethodCheck(http.MethodGet),
116+
)
117+
data, _ := getFailData(t, svc, request)
118+
assert(t, data.Message, "Could not get mergeability checks")
119+
assert(t, data.Details, "failed to fetch mergeability checks: "+errorFromGitlab.Error())
120+
})
121+
}

cmd/app/server.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ func CreateRouter(gitlabClient *Client, projectInfo *ProjectInfo, s *shutdownSer
134134
withMr(d, gitlabClient),
135135
withMethodCheck(http.MethodGet),
136136
))
137+
m.HandleFunc("/mr/info/mergeability", middleware(
138+
mergeabilityChecksService{d, gitlabClient},
139+
withMr(d, gitlabClient),
140+
withMethodCheck(http.MethodGet),
141+
))
137142
m.HandleFunc("/mr/assignee", middleware(
138143
assigneesService{d, gitlabClient},
139144
withMr(d, gitlabClient),

doc/gitlab.nvim.txt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,39 @@ you call this function with no values the defaults will be used:
312312
"squash",
313313
"labels",
314314
"web_url",
315+
"mergeability_checks", -- See more detailed configuration below
316+
},
317+
-- Settings for the mergeability checks in the summary view
318+
-- https://docs.gitlab.com/api/graphql/reference/#mergeabilitycheckidentifier
319+
mergeability_checks = {
320+
-- Symbols for individual check statuses. Set values to `false` to hide checks with given status from summary
321+
statuses = {
322+
SUCCESS = "✅",
323+
CHECKING = "🔁",
324+
FAILED = "❌",
325+
WARNING = "⚠️",
326+
INACTIVE = "💤",
327+
},
328+
-- Descriptions for individual checks. Set values to `false` to hide given checks from summary
329+
checks = {
330+
CI_MUST_PASS = "Pipeline must succeed",
331+
COMMITS_STATUS = "Source branch exists and contains commits",
332+
CONFLICT = "Merge conflicts must be resolved",
333+
DISCUSSIONS_NOT_RESOLVED = "Open threads must be resolved",
334+
DRAFT_STATUS = "Merge request must not be draft",
335+
JIRA_ASSOCIATION_MISSING = "Title or description references a Jira issue",
336+
LOCKED_LFS_FILES = "All LFS files must be unlocked",
337+
LOCKED_PATHS = "All paths must be unlocked",
338+
MERGE_REQUEST_BLOCKED = "Merge request is not blocked",
339+
MERGE_TIME = "Merge is not blocked due to a scheduled merge time",
340+
NEED_REBASE = "Merge request must be rebased, fast-forward merge is not possible",
341+
NOT_APPROVED = "All required approvals must be given",
342+
NOT_OPEN = "Merge request must be open",
343+
REQUESTED_CHANGES = "Change requests must be approved by the requesting user",
344+
SECURITY_POLICY_VIOLATIONS = "Security policies are satisfied",
345+
STATUS_CHECKS_MUST_PASS = "External status checks pass",
346+
TITLE_REGEX = "Title matches the expected regex",
347+
},
315348
},
316349
},
317350
discussion_signs = {

lua/gitlab/actions/approvals.lua

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ local M = {}
66

77
local refresh_status_state = function(data)
88
u.notify(data.message, vim.log.levels.INFO)
9-
state.load_new_state("info", function()
10-
require("gitlab.actions.summary").update_summary_details()
9+
state.load_new_state("mergeability", function()
10+
state.load_new_state("info", function()
11+
require("gitlab.actions.summary").update_summary_details()
12+
end)
1113
end)
1214
end
1315

lua/gitlab/actions/data.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ local M = {}
66
local user = state.dependencies.user
77
local info = state.dependencies.info
88
local labels = state.dependencies.labels
9+
local mergeability = state.dependencies.mergeability
910
local project_members = state.dependencies.project_members
1011
local revisions = state.dependencies.revisions
1112
local latest_pipeline = state.dependencies.latest_pipeline
@@ -21,6 +22,7 @@ M.data = function(resources, cb)
2122
info = info,
2223
user = user,
2324
labels = labels,
25+
mergeability = mergeability,
2426
project_members = project_members,
2527
revisions = revisions,
2628
pipeline = latest_pipeline,

0 commit comments

Comments
 (0)