Merge pull request #532 from jakubbortlik/feat/implement-mergeability-checks

feat: add mergeability checks to summary view
This commit is contained in:
Harrison (Harry) Cramer
2026-03-17 20:32:21 -04:00
committed by GitHub
12 changed files with 415 additions and 33 deletions

View File

@@ -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
} }

View 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
}

View 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())
})
}

View File

@@ -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),

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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),

View File

@@ -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",

View File

@@ -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