Merge pull request #532 from jakubbortlik/feat/implement-mergeability-checks
feat: add mergeability checks to summary view
This commit is contained in:
@@ -32,6 +32,7 @@ type Client struct {
|
|||||||
gitlab.UsersServiceInterface
|
gitlab.UsersServiceInterface
|
||||||
gitlab.DraftNotesServiceInterface
|
gitlab.DraftNotesServiceInterface
|
||||||
gitlab.ProjectMarkdownUploadsServiceInterface
|
gitlab.ProjectMarkdownUploadsServiceInterface
|
||||||
|
gitlab.GraphQLInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NewClient parses and validates the project settings and initializes the Gitlab client. */
|
/* NewClient parses and validates the project settings and initializes the Gitlab client. */
|
||||||
@@ -100,6 +101,7 @@ func NewClient() (*Client, error) {
|
|||||||
client.Users,
|
client.Users,
|
||||||
client.DraftNotes,
|
client.DraftNotes,
|
||||||
client.ProjectMarkdownUploads,
|
client.ProjectMarkdownUploads,
|
||||||
|
client.GraphQL,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
83
cmd/app/mergeability_checks.go
Normal file
83
cmd/app/mergeability_checks.go
Normal file
@@ -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
cmd/app/mergeability_checks_test.go
Normal file
121
cmd/app/mergeability_checks_test.go
Normal file
@@ -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -134,6 +134,11 @@ func CreateRouter(gitlabClient *Client, projectInfo *ProjectInfo, s *shutdownSer
|
|||||||
withMr(d, gitlabClient),
|
withMr(d, gitlabClient),
|
||||||
withMethodCheck(http.MethodGet),
|
withMethodCheck(http.MethodGet),
|
||||||
))
|
))
|
||||||
|
m.HandleFunc("/mr/info/mergeability", middleware(
|
||||||
|
mergeabilityChecksService{d, gitlabClient},
|
||||||
|
withMr(d, gitlabClient),
|
||||||
|
withMethodCheck(http.MethodGet),
|
||||||
|
))
|
||||||
m.HandleFunc("/mr/assignee", middleware(
|
m.HandleFunc("/mr/assignee", middleware(
|
||||||
assigneesService{d, gitlabClient},
|
assigneesService{d, gitlabClient},
|
||||||
withMr(d, gitlabClient),
|
withMr(d, gitlabClient),
|
||||||
|
|||||||
@@ -312,6 +312,39 @@ you call this function with no values the defaults will be used:
|
|||||||
"squash",
|
"squash",
|
||||||
"labels",
|
"labels",
|
||||||
"web_url",
|
"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 = {
|
discussion_signs = {
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ local M = {}
|
|||||||
|
|
||||||
local refresh_status_state = function(data)
|
local refresh_status_state = function(data)
|
||||||
u.notify(data.message, vim.log.levels.INFO)
|
u.notify(data.message, vim.log.levels.INFO)
|
||||||
state.load_new_state("info", function()
|
state.load_new_state("mergeability", function()
|
||||||
require("gitlab.actions.summary").update_summary_details()
|
state.load_new_state("info", function()
|
||||||
|
require("gitlab.actions.summary").update_summary_details()
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ local M = {}
|
|||||||
local user = state.dependencies.user
|
local user = state.dependencies.user
|
||||||
local info = state.dependencies.info
|
local info = state.dependencies.info
|
||||||
local labels = state.dependencies.labels
|
local labels = state.dependencies.labels
|
||||||
|
local mergeability = state.dependencies.mergeability
|
||||||
local project_members = state.dependencies.project_members
|
local project_members = state.dependencies.project_members
|
||||||
local revisions = state.dependencies.revisions
|
local revisions = state.dependencies.revisions
|
||||||
local latest_pipeline = state.dependencies.latest_pipeline
|
local latest_pipeline = state.dependencies.latest_pipeline
|
||||||
@@ -21,6 +22,7 @@ M.data = function(resources, cb)
|
|||||||
info = info,
|
info = info,
|
||||||
user = user,
|
user = user,
|
||||||
labels = labels,
|
labels = labels,
|
||||||
|
mergeability = mergeability,
|
||||||
project_members = project_members,
|
project_members = project_members,
|
||||||
revisions = revisions,
|
revisions = revisions,
|
||||||
pipeline = latest_pipeline,
|
pipeline = latest_pipeline,
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ local job = require("gitlab.job")
|
|||||||
local common = require("gitlab.actions.common")
|
local common = require("gitlab.actions.common")
|
||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
local popup = require("gitlab.popup")
|
local popup = require("gitlab.popup")
|
||||||
local List = require("gitlab.utils.list")
|
|
||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||||
|
|
||||||
|
-- No-break space used in summary details to make matching different parts of the line more robust
|
||||||
|
local nbsp = " "
|
||||||
|
|
||||||
local M = {
|
local M = {
|
||||||
layout_visible = false,
|
layout_visible = false,
|
||||||
layout = nil,
|
layout = nil,
|
||||||
@@ -108,6 +110,28 @@ M.update_details_popup = function(bufnr, info_lines)
|
|||||||
M.color_details(bufnr) -- Color values in details popup
|
M.color_details(bufnr) -- Color values in details popup
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Return the mergeability checks statuses and descriptions
|
||||||
|
---@return string[]
|
||||||
|
local make_mergeability_checks = function()
|
||||||
|
local lines = {}
|
||||||
|
for _, check in ipairs(state.MERGEABILITY) 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
|
-- 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.
|
-- lines that users include in their state.settings.info.fields list.
|
||||||
M.build_info_lines = function()
|
M.build_info_lines = function()
|
||||||
@@ -132,7 +156,7 @@ M.build_info_lines = function()
|
|||||||
pipeline = {
|
pipeline = {
|
||||||
title = "Pipeline Status",
|
title = "Pipeline Status",
|
||||||
content = function()
|
content = function()
|
||||||
local pipeline = state.INFO.pipeline
|
local pipeline = info.head_pipeline ~= vim.NIL and info.head_pipeline or info.pipeline
|
||||||
if type(pipeline) ~= "table" or (type(pipeline) == "table" and u.table_size(pipeline) == 0) then
|
if type(pipeline) ~= "table" or (type(pipeline) == "table" and u.table_size(pipeline) == 0) then
|
||||||
return ""
|
return ""
|
||||||
end
|
end
|
||||||
@@ -140,6 +164,7 @@ M.build_info_lines = function()
|
|||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
web_url = { title = "MR URL", content = info.web_url },
|
web_url = { title = "MR URL", content = info.web_url },
|
||||||
|
mergeability_checks = { title = "Mergeability checks", content = make_mergeability_checks },
|
||||||
}
|
}
|
||||||
|
|
||||||
local longest_used = ""
|
local longest_used = ""
|
||||||
@@ -147,33 +172,41 @@ M.build_info_lines = function()
|
|||||||
if v == "merge_status" then
|
if v == "merge_status" then
|
||||||
v = "detailed_merge_status"
|
v = "detailed_merge_status"
|
||||||
end -- merge_status was deprecated, see https://gitlab.com/gitlab-org/gitlab/-/issues/3169#note_1162532204
|
end -- merge_status was deprecated, see https://gitlab.com/gitlab-org/gitlab/-/issues/3169#note_1162532204
|
||||||
local title = options[v].title
|
if options[v] == nil then
|
||||||
if string.len(title) > string.len(longest_used) then
|
u.notify(string.format("Invalid field in settings.info.fields: '%s'", v), vim.log.levels.ERROR)
|
||||||
longest_used = title
|
else
|
||||||
|
local title = options[v].title
|
||||||
|
if vim.fn.strcharlen(title) > vim.fn.strcharlen(longest_used) then
|
||||||
|
longest_used = title
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function row_offset(row)
|
local function row_offset(row)
|
||||||
local offset = string.len(longest_used) - string.len(row)
|
local offset = vim.fn.strcharlen(longest_used) - vim.fn.strcharlen(row)
|
||||||
return string.rep(" ", offset + 3)
|
return string.rep(nbsp, offset + 3)
|
||||||
end
|
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
|
if v == "merge_status" then
|
||||||
v = "detailed_merge_status"
|
v = "detailed_merge_status"
|
||||||
end
|
end
|
||||||
local row = options[v]
|
local row = options[v]
|
||||||
local line = "* " .. row.title .. row_offset(row.title)
|
local title_prefix = "* " .. row.title .. row_offset(row.title)
|
||||||
if type(row.content) == "function" then
|
local content = type(row.content) == "function" and row.content() or row.content
|
||||||
local content = row.content()
|
if type(content) == "table" then
|
||||||
if content ~= nil then
|
-- Multi-line content
|
||||||
line = line .. row.content()
|
local padding = string.rep(nbsp, vim.fn.strcharlen(title_prefix)) -- no-break space
|
||||||
|
for i, line in ipairs(#content > 0 and content or { "" }) do
|
||||||
|
table.insert(result, (i == 1 and title_prefix or padding) .. line)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
line = line .. row.content
|
-- Single-line content
|
||||||
|
table.insert(result, title_prefix .. (content or ""))
|
||||||
end
|
end
|
||||||
return line
|
end
|
||||||
end)
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
-- This function will PUT the new description to the Go server
|
-- This function will PUT the new description to the Go server
|
||||||
@@ -260,24 +293,57 @@ end
|
|||||||
|
|
||||||
M.color_details = function(bufnr)
|
M.color_details = function(bufnr)
|
||||||
local details_namespace = vim.api.nvim_create_namespace("Details")
|
local details_namespace = vim.api.nvim_create_namespace("Details")
|
||||||
for i, v in ipairs(state.settings.info.fields) do
|
for i, line in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) do
|
||||||
if v == "labels" then
|
if line:match("^* Labels") then
|
||||||
local line_content = u.get_line_content(bufnr, i)
|
|
||||||
for j, label in ipairs(state.LABELS) do
|
for j, label in ipairs(state.LABELS) do
|
||||||
local start_idx, end_idx = line_content:find(label.Name)
|
local start_idx, end_idx = line:find(label.Name, 1, true)
|
||||||
if start_idx ~= nil and end_idx ~= nil then
|
if start_idx ~= nil and end_idx ~= nil then
|
||||||
vim.cmd("highlight " .. "label" .. j .. " guifg=white")
|
vim.cmd("highlight " .. "label" .. j .. " guifg=white")
|
||||||
vim.api.nvim_set_hl(0, ("label" .. j), { fg = label.Color })
|
vim.api.nvim_set_hl(0, ("label" .. j), { fg = label.Color })
|
||||||
vim.api.nvim_buf_add_highlight(bufnr, details_namespace, ("label" .. j), i - 1, start_idx - 1, end_idx)
|
vim.hl.range(bufnr, details_namespace, ("label" .. j), { i - 1, start_idx - 1 }, { i - 1, end_idx })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif v == "delete_branch" or v == "squash" or v == "draft" or v == "conflicts" then
|
elseif line:match("^* Status") then
|
||||||
local line_content = u.get_line_content(bufnr, i)
|
local status = line:match("[^" .. nbsp .. "]-$")
|
||||||
local start_idx, end_idx = line_content:find("%S-$")
|
local hl = ({
|
||||||
if start_idx ~= nil and end_idx ~= nil then
|
blocked_status = "DiagnosticError",
|
||||||
vim.api.nvim_set_hl(0, "boolean", { link = "Constant" })
|
broken_status = "DiagnosticError",
|
||||||
vim.api.nvim_buf_add_highlight(bufnr, details_namespace, "boolean", i - 1, start_idx - 1, end_idx)
|
checking = "DiagnosticInfo",
|
||||||
end
|
ci_must_pass = "DiagnosticWarn",
|
||||||
|
ci_still_running = "DiagnosticInfo",
|
||||||
|
discussions_not_resolved = "DiagnosticWarn",
|
||||||
|
draft_status = "Comment",
|
||||||
|
external_status_checks = "DiagnosticHint",
|
||||||
|
mergeable = "DiagnosticOK",
|
||||||
|
not_approved = "DiagnosticWarn",
|
||||||
|
not_open = "NonText",
|
||||||
|
policies_denied = "DiagnosticError",
|
||||||
|
unchecked = "NonText",
|
||||||
|
})[status] or "Normal"
|
||||||
|
local start_idx, end_idx = line:find("[^" .. nbsp .. "]-$")
|
||||||
|
vim.hl.range(bufnr, details_namespace, hl, { i - 1, start_idx - 1 }, { i - 1, end_idx })
|
||||||
|
elseif line:match("^* Branch") or line:match("^* Target Branch") then
|
||||||
|
local start_idx, end_idx = line:find("[^" .. nbsp .. "]-$")
|
||||||
|
vim.hl.range(bufnr, details_namespace, "Title", { i - 1, start_idx - 1 }, { i - 1, end_idx })
|
||||||
|
elseif line:match("^* Pipeline") then
|
||||||
|
local status = line:match("[^" .. nbsp .. "]-$")
|
||||||
|
local hl = ({
|
||||||
|
canceled = "DiagnosticWarn",
|
||||||
|
created = "DiagnosticInfo",
|
||||||
|
failed = "DiagnosticError",
|
||||||
|
manual = "DiagnosticHint",
|
||||||
|
pending = "DiagnosticWarn",
|
||||||
|
running = "DiagnosticInfo",
|
||||||
|
skipped = "Comment",
|
||||||
|
success = "DiagnosticOK",
|
||||||
|
unknown = "NonText",
|
||||||
|
})[status] or "Normal"
|
||||||
|
local start_idx, end_idx = line:find("[^" .. nbsp .. "]-$")
|
||||||
|
vim.hl.range(bufnr, details_namespace, hl, { i - 1, start_idx - 1 }, { i - 1, end_idx })
|
||||||
|
elseif line:match(nbsp .. "No$") or line:match(nbsp .. "Yes$") then
|
||||||
|
local start_idx, end_idx = line:find("[^" .. nbsp .. "]-$")
|
||||||
|
vim.api.nvim_set_hl(0, "boolean", { link = "Constant" })
|
||||||
|
vim.hl.range(bufnr, details_namespace, "boolean", { i - 1, start_idx - 1 }, { i - 1, end_idx })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -159,6 +159,7 @@
|
|||||||
---@field discussion_tree? DiscussionSettings -- Settings for the popup windows
|
---@field discussion_tree? DiscussionSettings -- Settings for the popup windows
|
||||||
---@field choose_merge_request? ChooseMergeRequestSettings -- Default settings when choosing a merge request
|
---@field choose_merge_request? ChooseMergeRequestSettings -- Default settings when choosing a merge request
|
||||||
---@field info? InfoSettings -- Settings for the "info" or "summary" view
|
---@field info? InfoSettings -- Settings for the "info" or "summary" view
|
||||||
|
---@field mergeability_checks? MergeabilityChecksSettings -- Settings for the mergeability checks in the "summary" view
|
||||||
---@field discussion_signs? DiscussionSigns -- The settings for discussion signs/diagnostics
|
---@field discussion_signs? DiscussionSigns -- The settings for discussion signs/diagnostics
|
||||||
---@field pipeline? PipelineSettings -- The settings for the pipeline popup
|
---@field pipeline? PipelineSettings -- The settings for the pipeline popup
|
||||||
---@field create_mr? CreateMrSettings -- The settings when creating an MR
|
---@field create_mr? CreateMrSettings -- The settings when creating an MR
|
||||||
@@ -252,7 +253,37 @@
|
|||||||
|
|
||||||
---@class InfoSettings
|
---@class InfoSettings
|
||||||
---@field horizontal? boolean -- Display metadata to the left of the summary rather than underneath
|
---@field horizontal? boolean -- Display metadata to the left of the summary rather than underneath
|
||||||
---@field fields? ("author" | "created_at" | "updated_at" | "merge_status" | "draft" | "conflicts" | "assignees" | "reviewers" | "pipeline" | "branch" | "target_branch" | "delete_branch" | "squash" | "labels")[]
|
---@field fields? ("author" | "created_at" | "updated_at" | "merge_status" | "draft" | "conflicts" | "assignees" | "reviewers" | "pipeline" | "branch" | "target_branch" | "delete_branch" | "squash" | "labels" | "web_url" | "mergeability_checks")[]
|
||||||
|
|
||||||
|
---@class MergeabilityChecksSettings
|
||||||
|
---@field statuses MergeabilityStatuses
|
||||||
|
---@field checks MergeabilityChecks
|
||||||
|
|
||||||
|
---@class MergeabilityStatuses
|
||||||
|
---@field SUCCESS string|false
|
||||||
|
---@field CHECKING string|false
|
||||||
|
---@field FAILED string|false
|
||||||
|
---@field WARNING string|false
|
||||||
|
---@field INACTIVE string|false
|
||||||
|
|
||||||
|
---@class MergeabilityChecks
|
||||||
|
---@field CI_MUST_PASS string|false
|
||||||
|
---@field COMMITS_STATUS string|false
|
||||||
|
---@field CONFLICT string|false
|
||||||
|
---@field DISCUSSIONS_NOT_RESOLVED string|false
|
||||||
|
---@field DRAFT_STATUS string|false
|
||||||
|
---@field JIRA_ASSOCIATION_MISSING string|false
|
||||||
|
---@field LOCKED_LFS_FILES string|false
|
||||||
|
---@field LOCKED_PATHS string|false
|
||||||
|
---@field MERGE_REQUEST_BLOCKED string|false
|
||||||
|
---@field MERGE_TIME string|false
|
||||||
|
---@field NEED_REBASE string|false
|
||||||
|
---@field NOT_APPROVED string|false
|
||||||
|
---@field NOT_OPEN string|false
|
||||||
|
---@field REQUESTED_CHANGES string|false
|
||||||
|
---@field SECURITY_POLICY_VIOLATIONS string|false
|
||||||
|
---@field STATUS_CHECKS_MUST_PASS string|false
|
||||||
|
---@field TITLE_REGEX string|false
|
||||||
|
|
||||||
---@class DiscussionSettings: table
|
---@class DiscussionSettings: table
|
||||||
---@field expanders? ExpanderOpts -- Customize the expander icons in the discussion tree
|
---@field expanders? ExpanderOpts -- Customize the expander icons in the discussion tree
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ local health = require("gitlab.health")
|
|||||||
|
|
||||||
local user = state.dependencies.user
|
local user = state.dependencies.user
|
||||||
local info = state.dependencies.info
|
local info = state.dependencies.info
|
||||||
|
local mergeability = state.dependencies.mergeability
|
||||||
local labels_dep = state.dependencies.labels
|
local labels_dep = state.dependencies.labels
|
||||||
local project_members = state.dependencies.project_members
|
local project_members = state.dependencies.project_members
|
||||||
local latest_pipeline = state.dependencies.latest_pipeline
|
local latest_pipeline = state.dependencies.latest_pipeline
|
||||||
@@ -62,6 +63,7 @@ return {
|
|||||||
setup = setup,
|
setup = setup,
|
||||||
summary = async.sequence({
|
summary = async.sequence({
|
||||||
u.merge(info, { refresh = true }),
|
u.merge(info, { refresh = true }),
|
||||||
|
u.merge(mergeability, { refresh = true }),
|
||||||
labels_dep,
|
labels_dep,
|
||||||
}, summary.summary),
|
}, summary.summary),
|
||||||
approve = async.sequence({ info }, approvals.approve),
|
approve = async.sequence({ info }, approvals.approve),
|
||||||
|
|||||||
@@ -218,6 +218,35 @@ M.settings = {
|
|||||||
"squash",
|
"squash",
|
||||||
"labels",
|
"labels",
|
||||||
"web_url",
|
"web_url",
|
||||||
|
"mergeability_checks",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mergeability_checks = {
|
||||||
|
statuses = {
|
||||||
|
SUCCESS = "✅",
|
||||||
|
CHECKING = "🔁",
|
||||||
|
FAILED = "❌",
|
||||||
|
WARNING = "⚠️",
|
||||||
|
INACTIVE = "💤",
|
||||||
|
},
|
||||||
|
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 = {
|
discussion_signs = {
|
||||||
@@ -467,6 +496,12 @@ M.dependencies = {
|
|||||||
state = "INFO",
|
state = "INFO",
|
||||||
refresh = false,
|
refresh = false,
|
||||||
},
|
},
|
||||||
|
mergeability = {
|
||||||
|
endpoint = "/mr/info/mergeability",
|
||||||
|
key = "mergeability_checks",
|
||||||
|
state = "MERGEABILITY",
|
||||||
|
refresh = false,
|
||||||
|
},
|
||||||
latest_pipeline = {
|
latest_pipeline = {
|
||||||
endpoint = "/pipeline",
|
endpoint = "/pipeline",
|
||||||
key = "latest_pipeline",
|
key = "latest_pipeline",
|
||||||
|
|||||||
@@ -309,8 +309,8 @@ end
|
|||||||
M.get_longest_string = function(list)
|
M.get_longest_string = function(list)
|
||||||
local longest = 0
|
local longest = 0
|
||||||
for _, v in pairs(list) do
|
for _, v in pairs(list) do
|
||||||
if string.len(v) > longest then
|
if vim.fn.strcharlen(v) > longest then
|
||||||
longest = string.len(v)
|
longest = vim.fn.strcharlen(v)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return longest
|
return longest
|
||||||
|
|||||||
Reference in New Issue
Block a user