Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/app/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Client struct {
gitlab.UsersServiceInterface
gitlab.DraftNotesServiceInterface
gitlab.ProjectMarkdownUploadsServiceInterface
gitlab.GraphQLInterface
}

/* NewClient parses and validates the project settings and initializes the Gitlab client. */
Expand Down Expand Up @@ -100,6 +101,7 @@ func NewClient() (*Client, error) {
client.Users,
client.DraftNotes,
client.ProjectMarkdownUploads,
client.GraphQL,
}, nil
}

Expand Down
83 changes: 83 additions & 0 deletions cmd/app/mergeability_checks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package app

import (
"encoding/json"
"fmt"
"net/http"

gitlab "gitlab.com/gitlab-org/api/client-go"
)

type MergeabilityCheck struct {
Identifier string `json:"identifier"`
Status string `json:"status"`
}

type MergeabilityChecksResponse struct {
SuccessResponse
MergeabilityChecks []*MergeabilityCheck `json:"mergeability_checks"`
}

type mergeabilityChecksGraphQLResponse struct {
Data struct {
Project struct {
MergeRequest struct {
MergeabilityChecks []*MergeabilityCheck `json:"mergeabilityChecks"`
} `json:"mergeRequest"`
} `json:"project"`
} `json:"data"`
}

const mergeabilityChecksQuery = `
query GetMergeabilityChecks($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
mergeabilityChecks {
identifier
status
}
}
}
}
`

type mergeabilityChecksService struct {
data
client gitlab.GraphQLInterface
}

func (a mergeabilityChecksService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
checks, err := a.fetchMergeabilityChecks()
if err != nil {
handleError(w, err, "Could not get mergeability checks", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusOK)
response := MergeabilityChecksResponse{
SuccessResponse: SuccessResponse{Message: "Mergeability checks retrieved"},
MergeabilityChecks: checks,
}

err = json.NewEncoder(w).Encode(response)
if err != nil {
handleError(w, err, "Could not encode response", http.StatusInternalServerError)
}
}

func (a mergeabilityChecksService) fetchMergeabilityChecks() ([]*MergeabilityCheck, error) {
var response mergeabilityChecksGraphQLResponse

_, err := a.client.Do(gitlab.GraphQLQuery{
Query: mergeabilityChecksQuery,
Variables: map[string]any{
"projectPath": a.gitInfo.ProjectPath(),
"iid": fmt.Sprintf("%d", a.projectInfo.MergeId),
},
}, &response)
if err != nil {
return nil, fmt.Errorf("failed to fetch mergeability checks: %w", err)
}

return response.Data.Project.MergeRequest.MergeabilityChecks, nil
}
121 changes: 121 additions & 0 deletions cmd/app/mergeability_checks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package app

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/harrisoncramer/gitlab.nvim/cmd/app/git"
gitlab "gitlab.com/gitlab-org/api/client-go"
)

type fakeGraphQLClient struct {
err error
jsonData []byte
}

func (f fakeGraphQLClient) Do(query gitlab.GraphQLQuery, response any, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) {
if f.err != nil {
return nil, f.err
}

// Actually unmarshal JSON into the response struct
if err := json.Unmarshal(f.jsonData, response); err != nil {
return nil, err
}

// if resp, ok := response.(mergeabilityChecksGraphQLResponse); ok {
// resp.Data.Project.MergeRequest.MergeabilityChecks = f.checks
// }

return makeResponse(http.StatusOK), nil
}

var testMergeabilityData = data{
projectInfo: &ProjectInfo{MergeId: 123},
gitInfo: &git.GitData{
BranchName: "feature-branch",
Namespace: "test-namespace",
ProjectName: "test-project",
},
}

func TestMergeabilityChecksHandler(t *testing.T) {
t.Run("Returns mergeability checks", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/mr/mergeability_checks", nil)
client := fakeGraphQLClient{
jsonData: []byte(`{
"data": {
"project": {
"mergeRequest": {
"mergeabilityChecks": [
{"identifier": "CI_MUST_PASS", "status": "SUCCESS"},
{"identifier": "CONFLICT", "status": "FAILED"}
]
}
}
}
}`),
}
svc := middleware(
mergeabilityChecksService{testMergeabilityData, client},
withMethodCheck(http.MethodGet),
)

res := httptest.NewRecorder()
svc.ServeHTTP(res, request)

var data MergeabilityChecksResponse
err := json.Unmarshal(res.Body.Bytes(), &data)
assert(t, err, nil)

assert(t, data.Message, "Mergeability checks retrieved")
assert(t, len(data.MergeabilityChecks), 2)
assert(t, data.MergeabilityChecks[0].Identifier, "CI_MUST_PASS")
assert(t, data.MergeabilityChecks[0].Status, "SUCCESS")
assert(t, data.MergeabilityChecks[1].Identifier, "CONFLICT")
assert(t, data.MergeabilityChecks[1].Status, "FAILED")
})

t.Run("Returns empty list when there are no checks", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/mr/mergeability_checks", nil)
client := fakeGraphQLClient{
jsonData: []byte(`{
"data": {
"project": {
"mergeRequest": {
"mergeabilityChecks": []
}
}
}
}`),
}
svc := middleware(
mergeabilityChecksService{testMergeabilityData, client},
withMethodCheck(http.MethodGet),
)

res := httptest.NewRecorder()
svc.ServeHTTP(res, request)

var data MergeabilityChecksResponse
err := json.Unmarshal(res.Body.Bytes(), &data)
assert(t, err, nil)

assert(t, data.Message, "Mergeability checks retrieved")
assert(t, len(data.MergeabilityChecks), 0)
})

t.Run("Handles errors from Gitlab client", func(t *testing.T) {
request := makeRequest(t, http.MethodGet, "/mr/mergeability_checks", nil)
client := fakeGraphQLClient{err: errorFromGitlab}
svc := middleware(
mergeabilityChecksService{testMergeabilityData, client},
withMethodCheck(http.MethodGet),
)
data, _ := getFailData(t, svc, request)
assert(t, data.Message, "Could not get mergeability checks")
assert(t, data.Details, "failed to fetch mergeability checks: "+errorFromGitlab.Error())
})
}
5 changes: 5 additions & 0 deletions cmd/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ func CreateRouter(gitlabClient *Client, projectInfo *ProjectInfo, s *shutdownSer
withMr(d, gitlabClient),
withMethodCheck(http.MethodGet),
))
m.HandleFunc("/mr/info/mergeability", middleware(
mergeabilityChecksService{d, gitlabClient},
withMr(d, gitlabClient),
withMethodCheck(http.MethodGet),
))
m.HandleFunc("/mr/assignee", middleware(
assigneesService{d, gitlabClient},
withMr(d, gitlabClient),
Expand Down
33 changes: 33 additions & 0 deletions doc/gitlab.nvim.txt
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,39 @@ you call this function with no values the defaults will be used:
"squash",
"labels",
"web_url",
"mergeability_checks", -- See more detailed configuration below
},
-- Settings for the mergeability checks in the summary view
-- https://docs.gitlab.com/api/graphql/reference/#mergeabilitycheckidentifier
mergeability_checks = {
-- Symbols for individual check statuses. Set values to `false` to hide checks with given status from summary
statuses = {
SUCCESS = "✅",
CHECKING = "🔁",
FAILED = "❌",
WARNING = "⚠️",
INACTIVE = "💤",
},
-- Descriptions for individual checks. Set values to `false` to hide given checks from summary
checks = {
CI_MUST_PASS = "Pipeline must succeed",
COMMITS_STATUS = "Source branch exists and contains commits",
CONFLICT = "Merge conflicts must be resolved",
DISCUSSIONS_NOT_RESOLVED = "Open threads must be resolved",
DRAFT_STATUS = "Merge request must not be draft",
JIRA_ASSOCIATION_MISSING = "Title or description references a Jira issue",
LOCKED_LFS_FILES = "All LFS files must be unlocked",
LOCKED_PATHS = "All paths must be unlocked",
MERGE_REQUEST_BLOCKED = "Merge request is not blocked",
MERGE_TIME = "Merge is not blocked due to a scheduled merge time",
NEED_REBASE = "Merge request must be rebased, fast-forward merge is not possible",
NOT_APPROVED = "All required approvals must be given",
NOT_OPEN = "Merge request must be open",
REQUESTED_CHANGES = "Change requests must be approved by the requesting user",
SECURITY_POLICY_VIOLATIONS = "Security policies are satisfied",
STATUS_CHECKS_MUST_PASS = "External status checks pass",
TITLE_REGEX = "Title matches the expected regex",
},
},
},
discussion_signs = {
Expand Down
6 changes: 4 additions & 2 deletions lua/gitlab/actions/approvals.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ local M = {}

local refresh_status_state = function(data)
u.notify(data.message, vim.log.levels.INFO)
state.load_new_state("info", function()
require("gitlab.actions.summary").update_summary_details()
state.load_new_state("mergeability", function()
state.load_new_state("info", function()
require("gitlab.actions.summary").update_summary_details()
end)
end)
end

Expand Down
2 changes: 2 additions & 0 deletions lua/gitlab/actions/data.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ local M = {}
local user = state.dependencies.user
local info = state.dependencies.info
local labels = state.dependencies.labels
local mergeability = state.dependencies.mergeability
local project_members = state.dependencies.project_members
local revisions = state.dependencies.revisions
local latest_pipeline = state.dependencies.latest_pipeline
Expand All @@ -21,6 +22,7 @@ M.data = function(resources, cb)
info = info,
user = user,
labels = labels,
mergeability = mergeability,
project_members = project_members,
revisions = revisions,
pipeline = latest_pipeline,
Expand Down
Loading
Loading