diff --git a/README.md b/README.md index 7972a9b..a9b200e 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,6 @@ require("gitlab").setup({ }, help = "g?", -- Opens a help popup for local keymaps when a relevant view is focused (popup, discussion panel, etc) popup = { -- The popup for comment creation, editing, and replying - exit = "", perform_action = "s", -- Once in normal mode, does action (like saving comment or editing description, etc) perform_linewise_action = "l", -- Once in normal mode, does the linewise action (see logs for this job, etc) width = "40%", @@ -128,7 +127,6 @@ require("gitlab").setup({ pipeline = nil, reply = nil, squash_message = nil, - backup_register = nil, }, discussion_tree = { -- The discussion tree that holds all comments auto_open = true, -- Automatically open when the reviewer is opened diff --git a/cmd/git.go b/cmd/git.go index 354f69e..fc282fc 100644 --- a/cmd/git.go +++ b/cmd/git.go @@ -86,10 +86,6 @@ func GetCurrentBranchNameFromNativeGitCmd() (res string, e error) { branchName := strings.TrimSpace(string(output)) - if branchName == "main" || branchName == "master" { - return "", fmt.Errorf("Cannot run on %s branch", branchName) - } - return branchName, nil } diff --git a/cmd/label.go b/cmd/label.go index 9c185f1..ca7beb9 100644 --- a/cmd/label.go +++ b/cmd/label.go @@ -99,7 +99,7 @@ func (a *api) updateLabels(w http.ResponseWriter, r *http.Request) { return } - var labels = gitlab.Labels(labelUpdateRequest.Labels) + var labels = gitlab.LabelOptions(labelUpdateRequest.Labels) mr, res, err := a.client.UpdateMergeRequest(a.projectInfo.ProjectId, a.projectInfo.MergeId, &gitlab.UpdateMergeRequestOptions{ Labels: &labels, }) diff --git a/cmd/pipeline.go b/cmd/pipeline.go index f76818e..e14e3c8 100644 --- a/cmd/pipeline.go +++ b/cmd/pipeline.go @@ -12,12 +12,17 @@ import ( type RetriggerPipelineResponse struct { SuccessResponse - Pipeline *gitlab.Pipeline + LatestPipeline *gitlab.Pipeline `json:"latest_pipeline"` } -type GetJobsResponse struct { +type PipelineWithJobs struct { + Jobs []*gitlab.Job `json:"jobs"` + LatestPipeline *gitlab.Pipeline `json:"latest_pipeline"` +} + +type GetPipelineAndJobsResponse struct { SuccessResponse - Jobs []*gitlab.Job + Pipeline PipelineWithJobs `json:"latest_pipeline"` } /* @@ -27,7 +32,7 @@ about a given job in a pipeline, see the jobHandler function func (a *api) pipelineHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - a.GetJobs(w, r) + a.GetPipelineAndJobs(w, r) case http.MethodPost: a.RetriggerPipeline(w, r) default: @@ -37,18 +42,29 @@ func (a *api) pipelineHandler(w http.ResponseWriter, r *http.Request) { } } -func (a *api) GetJobs(w http.ResponseWriter, r *http.Request) { +func (a *api) GetPipelineAndJobs(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - id := strings.TrimPrefix(r.URL.Path, "/pipeline/") - idInt, err := strconv.Atoi(id) + pipeline, res, err := a.client.GetLatestPipeline(a.projectInfo.ProjectId, &gitlab.GetLatestPipelineOptions{ + Ref: &a.gitInfo.BranchName, + }) if err != nil { - handleError(w, err, "Could not convert pipeline ID to integer", http.StatusBadRequest) + handleError(w, err, fmt.Sprintf("Gitlab failed to get latest pipeline for %s branch", a.gitInfo.BranchName), http.StatusInternalServerError) return } - jobs, res, err := a.client.ListPipelineJobs(a.projectInfo.ProjectId, idInt, &gitlab.ListJobsOptions{}) + if res.StatusCode >= 300 { + handleError(w, GenericError{endpoint: "/pipeline"}, fmt.Sprintf("Could not get latest pipeline for %s branch", a.gitInfo.BranchName), res.StatusCode) + return + } + + if pipeline == nil { + handleError(w, GenericError{endpoint: "/pipeline"}, fmt.Sprintf("No pipeline found for %s branch", a.gitInfo.BranchName), res.StatusCode) + return + } + + jobs, res, err := a.client.ListPipelineJobs(a.projectInfo.ProjectId, pipeline.ID, &gitlab.ListJobsOptions{}) if err != nil { handleError(w, err, "Could not get pipeline jobs", http.StatusInternalServerError) @@ -61,12 +77,15 @@ func (a *api) GetJobs(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusOK) - response := GetJobsResponse{ + response := GetPipelineAndJobsResponse{ SuccessResponse: SuccessResponse{ Status: http.StatusOK, - Message: "Pipeline jobs retrieved", + Message: "Pipeline retrieved", + }, + Pipeline: PipelineWithJobs{ + LatestPipeline: pipeline, + Jobs: jobs, }, - Jobs: jobs, } err = json.NewEncoder(w).Encode(response) @@ -78,7 +97,7 @@ func (a *api) GetJobs(w http.ResponseWriter, r *http.Request) { func (a *api) RetriggerPipeline(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - id := strings.TrimPrefix(r.URL.Path, "/pipeline/") + id := strings.TrimPrefix(r.URL.Path, "/pipeline/trigger/") idInt, err := strconv.Atoi(id) if err != nil { @@ -104,7 +123,7 @@ func (a *api) RetriggerPipeline(w http.ResponseWriter, r *http.Request) { Message: "Pipeline retriggered", Status: http.StatusOK, }, - Pipeline: pipeline, + LatestPipeline: pipeline, } err = json.NewEncoder(w).Encode(response) diff --git a/cmd/pipeline_test.go b/cmd/pipeline_test.go index 344d506..5f940d3 100644 --- a/cmd/pipeline_test.go +++ b/cmd/pipeline_test.go @@ -32,54 +32,79 @@ func retryPipelineBuildNon200(pid interface{}, pipeline int, options ...gitlab.R return nil, makeResponse(http.StatusSeeOther), nil } +func getLatestPipeline200(pid interface{}, opts *gitlab.GetLatestPipelineOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) { + return &gitlab.Pipeline{ID: 1}, makeResponse(http.StatusOK), nil +} + func TestPipelineHandler(t *testing.T) { t.Run("Gets all pipeline jobs", func(t *testing.T) { - request := makeRequest(t, http.MethodGet, "/pipeline/1", nil) - server, _ := createRouterAndApi(fakeClient{listPipelineJobs: listPipelineJobs}) - data := serveRequest(t, server, request, GetJobsResponse{}) - assert(t, data.SuccessResponse.Message, "Pipeline jobs retrieved") + request := makeRequest(t, http.MethodGet, "/pipeline", nil) + server, _ := createRouterAndApi(fakeClient{ + listPipelineJobs: listPipelineJobs, + getLatestPipeline: getLatestPipeline200, + }) + data := serveRequest(t, server, request, GetPipelineAndJobsResponse{}) + assert(t, data.SuccessResponse.Message, "Pipeline retrieved") assert(t, data.SuccessResponse.Status, http.StatusOK) }) t.Run("Disallows non-GET, non-POST methods", func(t *testing.T) { - request := makeRequest(t, http.MethodPatch, "/pipeline/1", nil) - server, _ := createRouterAndApi(fakeClient{listPipelineJobs: listPipelineJobs}) + request := makeRequest(t, http.MethodPatch, "/pipeline", nil) + server, _ := createRouterAndApi(fakeClient{ + listPipelineJobs: listPipelineJobs, + getLatestPipeline: getLatestPipeline200, + }) data := serveRequest(t, server, request, ErrorResponse{}) checkBadMethod(t, *data, http.MethodGet, http.MethodPost) }) t.Run("Handles errors from Gitlab client", func(t *testing.T) { - request := makeRequest(t, http.MethodGet, "/pipeline/1", nil) - server, _ := createRouterAndApi(fakeClient{listPipelineJobs: listPipelineJobsErr}) + request := makeRequest(t, http.MethodGet, "/pipeline", nil) + server, _ := createRouterAndApi(fakeClient{ + listPipelineJobs: listPipelineJobsErr, + getLatestPipeline: getLatestPipeline200, + }) data := serveRequest(t, server, request, ErrorResponse{}) checkErrorFromGitlab(t, *data, "Could not get pipeline jobs") }) t.Run("Handles non-200s from Gitlab client", func(t *testing.T) { - request := makeRequest(t, http.MethodGet, "/pipeline/1", nil) - server, _ := createRouterAndApi(fakeClient{listPipelineJobs: listPipelineJobsNon200}) + request := makeRequest(t, http.MethodGet, "/pipeline", nil) + server, _ := createRouterAndApi(fakeClient{ + listPipelineJobs: listPipelineJobsNon200, + getLatestPipeline: getLatestPipeline200, + }) data := serveRequest(t, server, request, ErrorResponse{}) checkNon200(t, *data, "Could not get pipeline jobs", "/pipeline") }) - t.Run("Retriggers pipeline", func(t *testing.T) { - request := makeRequest(t, http.MethodPost, "/pipeline/1", nil) - server, _ := createRouterAndApi(fakeClient{retryPipelineBuild: retryPipelineBuild}) - data := serveRequest(t, server, request, GetJobsResponse{}) - assert(t, data.SuccessResponse.Message, "Pipeline retriggered") - assert(t, data.SuccessResponse.Status, http.StatusOK) - }) - t.Run("Handles errors from Gitlab client", func(t *testing.T) { - request := makeRequest(t, http.MethodPost, "/pipeline/1", nil) - server, _ := createRouterAndApi(fakeClient{retryPipelineBuild: retryPipelineBuildErr}) + request := makeRequest(t, http.MethodPost, "/pipeline/trigger/1", nil) + server, _ := createRouterAndApi(fakeClient{ + retryPipelineBuild: retryPipelineBuildErr, + getLatestPipeline: getLatestPipeline200, + }) data := serveRequest(t, server, request, ErrorResponse{}) checkErrorFromGitlab(t, *data, "Could not retrigger pipeline") }) + t.Run("Retriggers pipeline", func(t *testing.T) { + request := makeRequest(t, http.MethodPost, "/pipeline/trigger/1", nil) + server, _ := createRouterAndApi(fakeClient{ + retryPipelineBuild: retryPipelineBuild, + getLatestPipeline: getLatestPipeline200, + }) + data := serveRequest(t, server, request, GetPipelineAndJobsResponse{}) + assert(t, data.SuccessResponse.Message, "Pipeline retriggered") + assert(t, data.SuccessResponse.Status, http.StatusOK) + }) + t.Run("Handles non-200s from Gitlab client on retrigger", func(t *testing.T) { - request := makeRequest(t, http.MethodPost, "/pipeline/1", nil) - server, _ := createRouterAndApi(fakeClient{retryPipelineBuild: retryPipelineBuildNon200}) + request := makeRequest(t, http.MethodPost, "/pipeline/trigger/1", nil) + server, _ := createRouterAndApi(fakeClient{ + retryPipelineBuild: retryPipelineBuildNon200, + getLatestPipeline: getLatestPipeline200, + }) data := serveRequest(t, server, request, ErrorResponse{}) checkNon200(t, *data, "Could not retrigger pipeline", "/pipeline") }) diff --git a/cmd/server.go b/cmd/server.go index 07d7df7..ae2a8a0 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -132,11 +132,12 @@ func createRouterAndApi(client ClientInterface, optFuncs ...optFunc) (*http.Serv m.HandleFunc("/mr/revoke", a.withMr(a.revokeHandler)) m.HandleFunc("/mr/awardable/note/", a.withMr(a.emojiNoteHandler)) + m.HandleFunc("/pipeline", a.pipelineHandler) + m.HandleFunc("/pipeline/trigger/", a.pipelineHandler) m.HandleFunc("/users/me", a.meHandler) m.HandleFunc("/attachment", a.attachmentHandler) m.HandleFunc("/create_mr", a.createMr) m.HandleFunc("/job", a.jobHandler) - m.HandleFunc("/pipeline/", a.pipelineHandler) m.HandleFunc("/project/members", a.projectMembersHandler) m.HandleFunc("/shutdown", a.shutdownHandler) diff --git a/cmd/test.go b/cmd/test.go index 33cc5d2..085009b 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -35,6 +35,7 @@ type fakeClient struct { listAllProjectMembers func(pid interface{}, opt *gitlab.ListProjectMembersOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.ProjectMember, *gitlab.Response, error) retryPipelineBuild func(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) listPipelineJobs func(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Job, *gitlab.Response, error) + getLatestPipeline func(pid interface{}, opt *gitlab.GetLatestPipelineOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) getTraceFile func(pid interface{}, jobID int, options ...gitlab.RequestOptionFunc) (*bytes.Reader, *gitlab.Response, error) listLabels func(pid interface{}, opt *gitlab.ListLabelsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Label, *gitlab.Response, error) listMergeRequestAwardEmojiOnNote func(pid interface{}, mergeRequestIID, noteID int, opt *gitlab.ListAwardEmojiOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.AwardEmoji, *gitlab.Response, error) @@ -120,6 +121,10 @@ func (f fakeClient) ListPipelineJobs(pid interface{}, pipelineID int, opts *gitl return f.listPipelineJobs(pid, pipelineID, opts, options...) } +func (f fakeClient) GetLatestPipeline(pid interface{}, opts *gitlab.GetLatestPipelineOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) { + return f.getLatestPipeline(pid, opts, options...) +} + func (f fakeClient) GetTraceFile(pid interface{}, jobID int, options ...gitlab.RequestOptionFunc) (*bytes.Reader, *gitlab.Response, error) { return f.getTraceFile(pid, jobID, options...) } diff --git a/cmd/types.go b/cmd/types.go index 70f3d2e..871112a 100644 --- a/cmd/types.go +++ b/cmd/types.go @@ -53,6 +53,7 @@ type ClientInterface interface { ListAllProjectMembers(pid interface{}, opt *gitlab.ListProjectMembersOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.ProjectMember, *gitlab.Response, error) RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) ListPipelineJobs(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Job, *gitlab.Response, error) + GetLatestPipeline(pid interface{}, opt *gitlab.GetLatestPipelineOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) GetTraceFile(pid interface{}, jobID int, options ...gitlab.RequestOptionFunc) (*bytes.Reader, *gitlab.Response, error) ListLabels(pid interface{}, opt *gitlab.ListLabelsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Label, *gitlab.Response, error) ListMergeRequestAwardEmojiOnNote(pid interface{}, mergeRequestIID int, noteID int, opt *gitlab.ListAwardEmojiOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.AwardEmoji, *gitlab.Response, error) diff --git a/doc/gitlab.nvim.txt b/doc/gitlab.nvim.txt index eb75a16..45c737b 100644 --- a/doc/gitlab.nvim.txt +++ b/doc/gitlab.nvim.txt @@ -146,7 +146,6 @@ you call this function with no values the defaults will be used: }, help = "g?", -- Opens a help popup for local keymaps when a relevant view is focused (popup, discussion panel, etc) popup = { -- The popup for comment creation, editing, and replying - exit = "", perform_action = "s", -- Once in normal mode, does action (like saving comment or editing description, etc) perform_linewise_action = "l", -- Once in normal mode, does the linewise action (see logs for this job, etc) width = "40%", @@ -159,7 +158,6 @@ you call this function with no values the defaults will be used: pipeline = nil, reply = nil, squash_message = nil, - backup_register = nil, }, discussion_tree = { -- The discussion tree that holds all comments auto_open = true, -- Automatically open when the reviewer is opened @@ -305,22 +303,6 @@ code block with prefilled code from the visual selection. Just like the summary, all the different kinds of comments are saved via the `settings.popup.perform_action` keybinding. -BACKUP REGISTER *gitlab.nvim.backup-register* - -Sometimes, the action triggered by `settings.popup.perform_action` can fail. -To prevent losing your carefully crafted note/comment/suggestion you can set -`settings.popup.backup_register` to a writable register (see |registers|) to -which the contents of the popup window will be saved just before the action is -performed. A practical setting is `settings.popup.backup_register = "+"` which -saves to the system clipboard (see |quoteplus|). This lets you easily apply -the action on Gitlab in a browser, if it keeps failing in `gitlab.nvim`. - -If you experience such problems, please first read the -|gitlab.nvim.troubleshooting| section. If it does not help, see if there are -any relevant known . If -there are none, please open one and provide any error messages that -`gitlab.nvim` may be showing. - DISCUSSIONS AND NOTES *gitlab.nvim.discussions-and-notes* Gitlab groups threads of comments together into "discussions." @@ -751,4 +733,38 @@ Merges the merge request into the target branch >lua require("gitlab").merge() +gitlab.data({ opts }, cb) *gitlab.nvim.data* + +The data function can be used to integrate `gitlab.nvim` with other plugins and tooling, by fetching +raw data about the current MR, including the summary information (title, description, etc); +reviewers, assignees, pipeline status. +>lua + require("gitlab").data({ + { type = "info", refresh = false }, + { type = "user", refresh = false } }, function (data) + vim.print("The info data is: ", data.info) + vim.print("The user data is: ", data.user) + end) + +If the resources have not yet been fetched from Gitlab, this function will +perform API calls for them. Once the data has been fetched, the callback will +execute and passed the data as an argument. + +Parameters: ~ + • {resources} (table) A list of resource blocks to fetch. + • {resource} (table) A resource to fetch, such as job information, etc. + • {resource.type}: (string) The type of resource, either: "user" + "labels", "project_members", "pipeline," or "revisions"." The types are: + • {user}: Information about the currently authenticated user + • {labels}: The labels available in the current project + • {project_members}: The list of current project members + • {revisions}: Revision information about the MR + • {pipeline}: Information about the current branch's pipeline. Returns + and object with `latest_pipeline` and `jobs` as fields. + • {resource.refresh}: (bool) Whether to re-fetch the data from Gitlab + or use the cached data locally, if available. + • {cb} (function) The callback function that runs after all of the + resources have been fetched. Will be passed a table with the data, + with each resource as a key-value pair, with the key being it's type. + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/go.mod b/go.mod index 57f8fe2..16a7329 100644 --- a/go.mod +++ b/go.mod @@ -2,13 +2,15 @@ module gitlab.com/harrisoncramer/gitlab.nvim go 1.19 -require github.com/xanzy/go-gitlab v0.93.2 +require ( + github.com/hashicorp/go-retryablehttp v0.7.2 + github.com/xanzy/go-gitlab v0.102.0 +) require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-retryablehttp v0.7.2 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/oauth2 v0.6.0 // indirect golang.org/x/time v0.3.0 // indirect diff --git a/go.sum b/go.sum index 1bd7d24..c00df94 100644 --- a/go.sum +++ b/go.sum @@ -19,10 +19,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/xanzy/go-gitlab v0.83.0 h1:37p0MpTPNbsTMKX/JnmJtY8Ch1sFiJzVF342+RvZEGw= -github.com/xanzy/go-gitlab v0.83.0/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw= -github.com/xanzy/go-gitlab v0.93.2 h1:kNNf3BYNYn/Zkig0B89fma12l36VLcYSGu7OnaRlRDg= -github.com/xanzy/go-gitlab v0.93.2/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw= +github.com/xanzy/go-gitlab v0.102.0 h1:ExHuJ1OTQ2yt25zBMMj0G96ChBirGYv8U7HyUiYkZ+4= +github.com/xanzy/go-gitlab v0.102.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= diff --git a/lua/gitlab/actions/create_mr.lua b/lua/gitlab/actions/create_mr.lua index 9e51dc7..932c53b 100644 --- a/lua/gitlab/actions/create_mr.lua +++ b/lua/gitlab/actions/create_mr.lua @@ -196,10 +196,6 @@ M.add_title = function(mr) mr.title = value end, }) - input:map("n", "", function() - input:unmount() - end, { noremap = true }) - input:mount() end diff --git a/lua/gitlab/actions/data.lua b/lua/gitlab/actions/data.lua new file mode 100644 index 0000000..3f1d98f --- /dev/null +++ b/lua/gitlab/actions/data.lua @@ -0,0 +1,45 @@ +local u = require("gitlab.utils") +local async = require("gitlab.async") +local state = require("gitlab.state") +local M = {} + +local user = state.dependencies.user +local info = state.dependencies.info +local labels = state.dependencies.labels +local project_members = state.dependencies.project_members +local revisions = state.dependencies.revisions +local latest_pipeline = state.dependencies.latest_pipeline + +M.data = function(resources, cb) + if type(resources) ~= "table" or type(cb) ~= "function" then + u.notify("The data function must be passed a resources table and a callback function", vim.log.levels.ERROR) + return + end + + local all_resources = { + info = info, + user = user, + labels = labels, + project_members = project_members, + revisions = revisions, + pipeline = latest_pipeline, + } + + local api_calls = {} + for _, resource in ipairs(resources) do + local api_call = all_resources[resource.type] + table.insert(api_calls, u.merge(api_call, { refresh = resource.refresh })) + end + + -- TODO: Build an async "parallel" that fetches the resources + -- in parallel where possible to speed up this API + return async.sequence(api_calls, function() + local data = {} + for k, v in pairs(all_resources) do + data[k] = state[v.state] + end + cb(data) + end)() +end + +return M diff --git a/lua/gitlab/actions/discussions/init.lua b/lua/gitlab/actions/discussions/init.lua index 11896a9..dc3a7a4 100644 --- a/lua/gitlab/actions/discussions/init.lua +++ b/lua/gitlab/actions/discussions/init.lua @@ -398,10 +398,6 @@ local function get_new_line(node) return node.new_line end - if range.start.new_line ~= nil then - return range.start.new_line - end - local _, start_new_line = common.parse_line_code(range.start.line_code) return start_new_line end @@ -417,10 +413,6 @@ local function get_old_line(node) return node.old_line end - if range.start.old_line ~= nil then - return range.start.old_line - end - local start_old_line, _ = common.parse_line_code(range.start.line_code) return start_old_line end diff --git a/lua/gitlab/actions/labels.lua b/lua/gitlab/actions/labels.lua index 3b25016..da9447f 100644 --- a/lua/gitlab/actions/labels.lua +++ b/lua/gitlab/actions/labels.lua @@ -15,18 +15,11 @@ M.delete_label = function() end local refresh_label_state = function(labels) - state.INFO.labels = List.new(labels):reduce(function(agg, label) - return agg .. "," .. label - end, "") + state.INFO.labels = labels end local get_current_labels = function() - local label_string = state.INFO.labels - local current_labels = {} - for value in label_string:gmatch("[^,]+") do - table.insert(current_labels, value) - end - return current_labels + return state.INFO.labels end local get_all_labels = function() @@ -45,16 +38,11 @@ M.add_popup = function(type) if not choice then return end - local label_string = state.INFO.labels - local new_labels = {} - for value in label_string:gmatch("[^,]+") do - table.insert(new_labels, value) - end - - table.insert(new_labels, choice) - local body = { labels = new_labels } + table.insert(current_labels, choice) + local body = { labels = current_labels } job.run_job("/mr/" .. type, "PUT", body, function(data) u.notify(data.message, vim.log.levels.INFO) + refresh_label_state(data.labels) end) end) diff --git a/lua/gitlab/actions/pipeline.lua b/lua/gitlab/actions/pipeline.lua index 11c41d4..045fa73 100644 --- a/lua/gitlab/actions/pipeline.lua +++ b/lua/gitlab/actions/pipeline.lua @@ -7,12 +7,12 @@ local job = require("gitlab.job") local u = require("gitlab.utils") local M = { pipeline_jobs = nil, + latest_pipeline = nil, pipeline_popup = nil, } -local function get_pipeline() - local pipeline = state.INFO.head_pipeline or state.INFO.pipeline - +local function get_latest_pipeline() + local pipeline = state.PIPELINE and state.PIPELINE.latest_pipeline if type(pipeline) ~= "table" or (type(pipeline) == "table" and u.table_size(pipeline) == 0) then u.notify("Pipeline not found", vim.log.levels.WARN) return @@ -20,97 +20,93 @@ local function get_pipeline() return pipeline end -M.get_pipeline_status = function() - local pipeline = get_pipeline() - if pipeline == nil then - return nil +local function get_pipeline_jobs() + M.latest_pipeline = get_latest_pipeline() + if not M.latest_pipeline then + return end - return string.format("%s (%s)", state.settings.pipeline[pipeline.status], pipeline.status) + return u.reverse(type(state.PIPELINE.jobs) == "table" and state.PIPELINE.jobs or {}) end -- The function will render the Pipeline state in a popup M.open = function() - local pipeline = get_pipeline() - if not pipeline then + M.pipeline_jobs = get_pipeline_jobs() + M.latest_pipeline = get_latest_pipeline() + if M.latest_pipeline == nil then return end - job.run_job("/pipeline/" .. pipeline.id, "GET", nil, function(data) - local pipeline_jobs = u.reverse(type(data.Jobs) == "table" and data.Jobs or {}) - M.pipeline_jobs = pipeline_jobs + local width = string.len(M.latest_pipeline.web_url) + 10 + local height = 6 + #M.pipeline_jobs + 3 - local width = string.len(pipeline.web_url) + 10 - local height = 6 + #pipeline_jobs + 3 + local pipeline_popup = + Popup(u.create_popup_state("Loading Pipeline...", state.settings.popup.pipeline, width, height, 60)) + M.pipeline_popup = pipeline_popup + pipeline_popup:mount() - local pipeline_popup = - Popup(u.create_popup_state("Loading Pipeline...", state.settings.popup.pipeline, width, height, 60)) - M.pipeline_popup = pipeline_popup - pipeline_popup:mount() + local bufnr = vim.api.nvim_get_current_buf() + vim.opt_local.wrap = false - local bufnr = vim.api.nvim_get_current_buf() - vim.opt_local.wrap = false + local lines = {} - local lines = {} + u.switch_can_edit_buf(bufnr, true) + table.insert(lines, "Status: " .. M.get_pipeline_status(false)) + table.insert(lines, "") + table.insert(lines, string.format("Last Run: %s", u.time_since(M.latest_pipeline.created_at))) + table.insert(lines, string.format("Url: %s", M.latest_pipeline.web_url)) + table.insert(lines, string.format("Triggered By: %s", M.latest_pipeline.source)) - u.switch_can_edit_buf(bufnr, true) - table.insert(lines, "Status: " .. M.get_pipeline_status()) - table.insert(lines, "") - table.insert(lines, string.format("Last Run: %s", u.time_since(pipeline.created_at))) - table.insert(lines, string.format("Url: %s", pipeline.web_url)) - table.insert(lines, string.format("Triggered By: %s", pipeline.source)) + table.insert(lines, "") + table.insert(lines, "Jobs:") - table.insert(lines, "") - table.insert(lines, "Jobs:") + local longest_title = u.get_longest_string(u.map(M.pipeline_jobs, function(v) + return v.name + end)) - local longest_title = u.get_longest_string(u.map(pipeline_jobs, function(v) - return v.name - end)) + local function row_offset(name) + local offset = longest_title - string.len(name) + local res = string.rep(" ", offset + 5) + return res + end - local function row_offset(name) - local offset = longest_title - string.len(name) - local res = string.rep(" ", offset + 5) - return res + for _, pipeline_job in ipairs(M.pipeline_jobs) do + local offset = row_offset(pipeline_job.name) + local row = string.format( + "%s%s %s (%s)", + pipeline_job.name, + offset, + state.settings.pipeline[pipeline_job.status] or "*", + pipeline_job.status or "" + ) + + table.insert(lines, row) + end + + vim.schedule(function() + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + M.color_status(M.latest_pipeline.status, bufnr, lines[1], 1) + + for i, pipeline_job in ipairs(M.pipeline_jobs) do + M.color_status(pipeline_job.status, bufnr, lines[7 + i], 7 + i) end - for _, pipeline_job in ipairs(pipeline_jobs) do - local offset = row_offset(pipeline_job.name) - local row = string.format( - "%s%s %s (%s)", - pipeline_job.name, - offset, - state.settings.pipeline[pipeline_job.status] or "*", - pipeline_job.status or "" - ) - - table.insert(lines, row) - end - - vim.schedule(function() - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) - M.color_status(pipeline.status, bufnr, lines[1], 1) - - for i, pipeline_job in ipairs(pipeline_jobs) do - M.color_status(pipeline_job.status, bufnr, lines[7 + i], 7 + i) - end - - pipeline_popup.border:set_text("top", "Pipeline Status", "center") - state.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs) - u.switch_can_edit_buf(bufnr, false) - end) + pipeline_popup.border:set_text("top", "Pipeline Status", "center") + state.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs) + u.switch_can_edit_buf(bufnr, false) end) end M.retrigger = function() - local pipeline = get_pipeline() - if not pipeline then + M.latest_pipeline = get_latest_pipeline() + if not M.latest_pipeline then return end - if pipeline.status ~= "failed" then + if M.latest_pipeline.status ~= "failed" then u.notify("Pipeline is not in a failed state!", vim.log.levels.WARN) return end - job.run_job("/pipeline/" .. pipeline.id, "POST", nil, function() + job.run_job("/pipeline/" .. M.latest_pipeline.id, "POST", nil, function() u.notify("Pipeline re-triggered!", vim.log.levels.INFO) end) end @@ -173,6 +169,42 @@ M.see_logs = function() end) end +---Returns the user-defined symbol representing the status +---of the current pipeline. Takes an optional argument to +---colorize the pipeline icon. +---@param wrap_with_color boolean +---@return string +M.get_pipeline_icon = function(wrap_with_color) + M.latest_pipeline = get_latest_pipeline() + if not M.latest_pipeline then + return "" + end + local symbol = state.settings.pipeline[M.latest_pipeline.status] + if not wrap_with_color then + return symbol + end + if M.latest_pipeline.status == "failed" then + return "%#DiagnosticError#" .. symbol + end + if M.latest_pipeline.status == "success" then + return "%#DiagnosticOk#" .. symbol + end + return "%#DiagnosticWarn#" .. symbol +end + +---Returns the status of the latest pipeline and the symbol +--representing the status of the current pipeline. Takes an optional argument to +---colorize the pipeline icon. +---@param wrap_with_color boolean +---@return string +M.get_pipeline_status = function(wrap_with_color) + M.latest_pipeline = get_latest_pipeline() + if not M.latest_pipeline then + return "" + end + return string.format("%s (%s)", M.get_pipeline_icon(wrap_with_color), M.latest_pipeline.status) +end + M.color_status = function(status, bufnr, status_line, linnr) local ns_id = vim.api.nvim_create_namespace("GitlabNamespace") vim.cmd(string.format("highlight default StatusHighlight guifg=%s", state.settings.pipeline[status])) diff --git a/lua/gitlab/actions/summary.lua b/lua/gitlab/actions/summary.lua index ac10ce7..8f1f00c 100644 --- a/lua/gitlab/actions/summary.lua +++ b/lua/gitlab/actions/summary.lua @@ -8,7 +8,6 @@ local u = require("gitlab.utils") local List = require("gitlab.utils.list") local state = require("gitlab.state") local miscellaneous = require("gitlab.actions.miscellaneous") -local pipeline = require("gitlab.actions.pipeline") local M = { layout_visible = false, @@ -134,12 +133,16 @@ M.build_info_lines = function() assignees = { title = "Assignees", content = u.make_readable_list(info.assignees, "name") }, reviewers = { title = "Reviewers", content = u.make_readable_list(info.reviewers, "name") }, branch = { title = "Branch", content = info.source_branch }, - labels = { title = "Labels", content = u.make_comma_separated_readable(info.labels) }, + labels = { title = "Labels", content = table.concat(info.labels, ", ") }, target_branch = { title = "Target Branch", content = state.INFO.target_branch }, pipeline = { title = "Pipeline Status", content = function() - return pipeline.get_pipeline_status() + local pipeline = state.INFO.pipeline + if type(pipeline) ~= "table" or (type(pipeline) == "table" and u.table_size(pipeline) == 0) then + return "" + end + return pipeline.status end, }, } diff --git a/lua/gitlab/init.lua b/lua/gitlab/init.lua index 4a2ee71..1e9add6 100644 --- a/lua/gitlab/init.lua +++ b/lua/gitlab/init.lua @@ -8,6 +8,7 @@ local reviewer = require("gitlab.reviewer") local discussions = require("gitlab.actions.discussions") local merge = require("gitlab.actions.merge") local summary = require("gitlab.actions.summary") +local data = require("gitlab.actions.data") local assignees_and_reviewers = require("gitlab.actions.assignees_and_reviewers") local comment = require("gitlab.actions.comment") local pipeline = require("gitlab.actions.pipeline") @@ -19,6 +20,7 @@ local user = state.dependencies.user local info = state.dependencies.info local labels_dep = state.dependencies.labels local project_members = state.dependencies.project_members +local latest_pipeline = state.dependencies.latest_pipeline local revisions = state.dependencies.revisions return { @@ -34,7 +36,10 @@ return { emoji.init() -- Read in emojis for lookup purposes end, -- Global Actions 🌎 - summary = async.sequence({ u.merge(info, { refresh = true }), labels_dep }, summary.summary), + summary = async.sequence({ + u.merge(info, { refresh = true }), + labels_dep, + }, summary.summary), approve = async.sequence({ info }, approvals.approve), revoke = async.sequence({ info }, approvals.revoke), add_reviewer = async.sequence({ info, project_members }, assignees_and_reviewers.add_reviewer), @@ -55,7 +60,7 @@ return { close_review = function() reviewer.close() end, - pipeline = async.sequence({ info }, pipeline.open), + pipeline = async.sequence({ latest_pipeline }, pipeline.open), merge = async.sequence({ u.merge(info, { refresh = true }) }, merge.merge), -- Discussion Tree Actions 🌴 toggle_discussions = async.sequence({ info, user }, discussions.toggle), @@ -65,6 +70,7 @@ return { reply = async.sequence({ info }, discussions.reply), -- Other functions 🤷 state = state, + data = data.data, print_settings = state.print_settings, open_in_browser = async.sequence({ info }, function() if state.INFO.web_url == nil then diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index 1b70711..797efe7 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -26,7 +26,6 @@ M.settings = { attachment_dir = "", help = "g?", popup = { - exit = "", perform_action = "s", perform_linewise_action = "l", width = "40%", @@ -40,7 +39,6 @@ M.settings = { help = nil, pipeline = nil, squash_message = nil, - backup_register = nil, }, discussion_tree = { auto_open = true, @@ -270,10 +268,6 @@ M.set_popup_keymaps = function(popup, action, linewise_action, opts) if opts == nil then opts = {} end - vim.keymap.set("n", M.settings.popup.exit, function() - exit(popup, opts) - end, { buffer = popup.bufnr, desc = "Exit popup" }) - if action ~= "Help" then -- Don't show help on the help popup vim.keymap.set("n", M.settings.help, function() local help = require("gitlab.actions.help") @@ -283,9 +277,6 @@ M.set_popup_keymaps = function(popup, action, linewise_action, opts) if action ~= nil then vim.keymap.set("n", M.settings.popup.perform_action, function() local text = u.get_buffer_text(popup.bufnr) - if M.settings.popup.backup_register ~= nil then - vim.cmd("0,$yank " .. M.settings.popup.backup_register) - end if opts.action_before_close then action(text, popup.bufnr) exit(popup, opts) @@ -304,6 +295,13 @@ M.set_popup_keymaps = function(popup, action, linewise_action, opts) linewise_action(text) end, { buffer = popup.bufnr, desc = "Perform linewise action" }) end + + vim.api.nvim_create_autocmd("BufUnload", { + buffer = popup.bufnr, + callback = function() + exit(popup, opts) + end, + }) end -- Dependencies @@ -314,6 +312,7 @@ end M.dependencies = { user = { endpoint = "/users/me", key = "user", state = "USER", refresh = false }, info = { endpoint = "/mr/info", key = "info", state = "INFO", refresh = false }, + latest_pipeline = { endpoint = "/pipeline", key = "latest_pipeline", state = "PIPELINE", refresh = true }, labels = { endpoint = "/mr/label", key = "labels", state = "LABELS", refresh = false }, revisions = { endpoint = "/mr/revisions", key = "Revisions", state = "MR_REVISIONS", refresh = false }, project_members = {