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
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
46 changes: 36 additions & 10 deletions lua/gitlab/actions/summary.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ local job = require("gitlab.job")
local common = require("gitlab.actions.common")
local u = require("gitlab.utils")
local popup = require("gitlab.popup")
local List = require("gitlab.utils.list")
local state = require("gitlab.state")
local miscellaneous = require("gitlab.actions.miscellaneous")

Expand Down Expand Up @@ -108,6 +107,28 @@ M.update_details_popup = function(bufnr, info_lines)
M.color_details(bufnr) -- Color values in details popup
end

---Return the mergeability checks statuses and descriptions
---@return string[]
local make_mergeability_checks = function()
local lines = {}
for _, check in ipairs(state.MERGEABILITY.mergeability_checks) do
local status = state.settings.mergeability_checks.statuses[check.status]
if status == nil then
u.notify(string.format("Unknown mergeability check status: %s", check.status), vim.log.levels.ERROR)
end
if status then
local description = state.settings.mergeability_checks.checks[check.identifier]
if description == nil then
u.notify(string.format("Unknown mergeability check identifier: %s", check.identifier), vim.log.levels.ERROR)
end
if description then
table.insert(lines, status .. " " .. description)
end
end
end
return lines
end

-- Builds a lua list of strings that contain metadata about the current MR. Only builds the
-- lines that users include in their state.settings.info.fields list.
M.build_info_lines = function()
Expand Down Expand Up @@ -140,6 +161,7 @@ M.build_info_lines = function()
end,
},
web_url = { title = "MR URL", content = info.web_url },
mergeability_checks = { title = "Mergeability checks", content = make_mergeability_checks },
}

local longest_used = ""
Expand All @@ -158,22 +180,26 @@ M.build_info_lines = function()
return string.rep(" ", offset + 3)
end

return List.new(state.settings.info.fields):map(function(v)
local result = {}
for _, v in ipairs(state.settings.info.fields) do
if v == "merge_status" then
v = "detailed_merge_status"
end
local row = options[v]
local line = "* " .. row.title .. row_offset(row.title)
if type(row.content) == "function" then
local content = row.content()
if content ~= nil then
line = line .. row.content()
local title_prefix = "* " .. row.title .. row_offset(row.title)
local content = type(row.content) == "function" and row.content() or row.content
if type(content) == "table" then
-- Multi-line content
local padding = string.rep(" ", #title_prefix)
for i, line in ipairs(#content > 0 and content or { "" }) do
table.insert(result, (i == 1 and title_prefix or padding) .. line)
end
else
line = line .. row.content
-- Single-line content
table.insert(result, title_prefix .. (content or ""))
end
return line
end)
end
return result
end

-- This function will PUT the new description to the Go server
Expand Down
Loading
Loading