diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d220b816..a92dfb02 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -10,7 +10,7 @@ It's possible that the feature you want is already implemented, or does not belo If you are using Lazy as a plugin manager, the easiest way to work on changes is by setting a specific path for the plugin that points to your repository locally. This is what I do: -```lua +```lua { "harrisoncramer/gitlab.nvim", dependencies = { @@ -54,8 +54,8 @@ $ luacheck --globals vim busted --no-max-line-length -- . 4. Make the merge request to the `develop` branch of `.gitlab.nvim` -Please provide a description of the feature, and links to any relevant issues. +Please provide a description of the feature, and links to any relevant issues. -That's it! I'll try to respond to any incoming merge request in a few days. Once we've reviewed it, it will be merged into the develop branch. +That's it! I'll try to respond to any incoming merge request in a few days. Once we've reviewed it, it will be merged into the develop branch. After some time, if the develop branch is found to be stable, that branch will be merged into `main` and released. When merged into `main` the pipeline will detect whether we're merging in a patch, minor, or major change, and create a new tag (e.g. 1.0.12) and release. diff --git a/cmd/app/pipeline.go b/cmd/app/pipeline.go index 174b4acc..9cf90c61 100644 --- a/cmd/app/pipeline.go +++ b/cmd/app/pipeline.go @@ -20,16 +20,18 @@ type RetriggerPipelineResponse struct { type PipelineWithJobs struct { Jobs []*gitlab.Job `json:"jobs"` LatestPipeline *gitlab.PipelineInfo `json:"latest_pipeline"` + Name string `json:"name"` } type GetPipelineAndJobsResponse struct { SuccessResponse - Pipeline PipelineWithJobs `json:"latest_pipeline"` + Pipelines []PipelineWithJobs `json:"latest_pipeline"` } type PipelineManager interface { ListProjectPipelines(pid interface{}, opt *gitlab.ListProjectPipelinesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.PipelineInfo, *gitlab.Response, error) ListPipelineJobs(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Job, *gitlab.Response, error) + ListPipelineBridges(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Bridge, *gitlab.Response, error) RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) } @@ -101,7 +103,6 @@ func (a pipelineService) GetPipelineAndJobs(w http.ResponseWriter, r *http.Reque } 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) return @@ -112,13 +113,51 @@ func (a pipelineService) GetPipelineAndJobs(w http.ResponseWriter, r *http.Reque return } + pipelines := []PipelineWithJobs{} + pipelines = append(pipelines, PipelineWithJobs{ + Jobs: jobs, + LatestPipeline: pipeline, + Name: "root", + }) + + bridges, res, err := a.client.ListPipelineBridges(a.projectInfo.ProjectId, pipeline.ID, &gitlab.ListJobsOptions{}) + + if err != nil { + handleError(w, err, "Could not get pipeline trigger jobs", http.StatusInternalServerError) + return + } + if res.StatusCode >= 300 { + handleError(w, GenericError{r.URL.Path}, "Could not get pipeline trigger jobs", res.StatusCode) + return + } + + for _, bridge := range bridges { + if bridge.DownstreamPipeline == nil { + continue + } + + pipelineIdInBridge := bridge.DownstreamPipeline.ID + bridgePipelineJobs, res, err := a.client.ListPipelineJobs(bridge.DownstreamPipeline.ProjectID, pipelineIdInBridge, &gitlab.ListJobsOptions{}) + if err != nil { + handleError(w, err, "Could not get jobs for a pipeline from a trigger job", http.StatusInternalServerError) + return + } + if res.StatusCode >= 300 { + handleError(w, GenericError{r.URL.Path}, "Could not get jobs for a pipeline from a trigger job", res.StatusCode) + return + } + + pipelines = append(pipelines, PipelineWithJobs{ + Jobs: bridgePipelineJobs, + LatestPipeline: bridge.DownstreamPipeline, + Name: bridge.Name, + }) + } + w.WriteHeader(http.StatusOK) response := GetPipelineAndJobsResponse{ SuccessResponse: SuccessResponse{Message: "Pipeline retrieved"}, - Pipeline: PipelineWithJobs{ - LatestPipeline: pipeline, - Jobs: jobs, - }, + Pipelines: pipelines, } err = json.NewEncoder(w).Encode(response) diff --git a/cmd/app/pipeline_test.go b/cmd/app/pipeline_test.go index db354946..ba090423 100644 --- a/cmd/app/pipeline_test.go +++ b/cmd/app/pipeline_test.go @@ -27,6 +27,14 @@ func (f fakePipelineManager) ListPipelineJobs(pid interface{}, pipelineID int, o return []*gitlab.Job{}, resp, err } +func (f fakePipelineManager) ListPipelineBridges(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Bridge, *gitlab.Response, error) { + resp, err := f.handleGitlabError() + if err != nil { + return nil, nil, err + } + return []*gitlab.Bridge{}, resp, err +} + func (f fakePipelineManager) RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) { resp, err := f.handleGitlabError() if err != nil { diff --git a/lua/gitlab/actions/pipeline.lua b/lua/gitlab/actions/pipeline.lua index 00dc5c29..bc113c2d 100644 --- a/lua/gitlab/actions/pipeline.lua +++ b/lua/gitlab/actions/pipeline.lua @@ -12,36 +12,67 @@ local M = { pipeline_popup = nil, } -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 +local function get_latest_pipelines(count) + count = count or 1 -- Default to 1 if count is not provided + local pipelines = {} + + if not state.PIPELINE then + u.notify("Pipeline state is not initialized", vim.log.levels.WARN) + return nil end - return pipeline -end -local function get_pipeline_jobs() - M.latest_pipeline = get_latest_pipeline() - if not M.latest_pipeline then - return + for i = 1, math.max(count, #state.PIPELINE) do + local pipeline = state.PIPELINE[i].latest_pipeline + if type(pipeline) == "table" and u.table_size(pipeline) > 0 then + table.insert(pipelines, pipeline) + end + end + + if #pipelines == 0 then + u.notify("No valid pipelines found", vim.log.levels.WARN) + return nil end - return u.reverse(type(state.PIPELINE.jobs) == "table" and state.PIPELINE.jobs or {}) + return pipelines +end + +local function get_pipeline_jobs(idx) + return u.reverse(type(state.PIPELINE[idx].jobs) == "table" and state.PIPELINE[idx].jobs or {}) end -- The function will render the Pipeline state in a popup M.open = function() - M.pipeline_jobs = get_pipeline_jobs() - M.latest_pipeline = get_latest_pipeline() - if M.latest_pipeline == nil then + M.latest_pipelines = get_latest_pipelines() + if not M.latest_pipelines then + return + end + if not M.latest_pipelines or #M.latest_pipelines == 0 then return end - local width = string.len(M.latest_pipeline.web_url) + 10 - local height = 6 + #M.pipeline_jobs + 3 + local max_width = 0 + local total_height = 0 + local pipelines_data = {} + + for idx, pipeline in ipairs(M.latest_pipelines) do + local width = string.len(pipeline.web_url) + 10 + max_width = math.max(max_width, width) + local pipeline_jobs = get_pipeline_jobs(idx) + local pipeline_status = M.get_pipeline_status(idx, false) + local height = 6 + #pipeline_jobs + 3 + total_height = total_height + height + + table.insert(pipelines_data, { + pipeline = pipeline, + pipeline_status = pipeline_status, + jobs = pipeline_jobs, + width = width, + height = 6 + #pipeline_jobs + 3, + lines = {}, + }) + end local pipeline_popup = - Popup(popup.create_popup_state("Loading Pipeline...", state.settings.popup.pipeline, width, height, 60)) + Popup(popup.create_popup_state("Loading Pipelines...", state.settings.popup.pipeline, max_width, total_height, 60)) popup.set_up_autocommands(pipeline_popup, nil, vim.api.nvim_get_current_win()) M.pipeline_popup = pipeline_popup pipeline_popup:mount() @@ -49,57 +80,79 @@ M.open = function() local bufnr = vim.api.nvim_get_current_buf() vim.opt_local.wrap = false - 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)) - - 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 function row_offset(name) - local offset = longest_title - string.len(name) - local res = string.rep(" ", offset + 5) - return res - end - 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) + local all_lines = {} + for i, data in ipairs(pipelines_data) do + local pipeline = data.pipeline + local lines = data.lines + + table.insert(lines, data.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:") + + local longest_title = u.get_longest_string(u.map(data.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 + + for _, pipeline_job in ipairs(data.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 + + -- Add separator between pipelines + if i < #pipelines_data then + table.insert(lines, "") + table.insert(lines, string.rep("-", max_width)) + table.insert(lines, "") + end + + for _, line in ipairs(lines) do + table.insert(all_lines, line) + end 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) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, all_lines) + + local line_offset = 0 + for _, data in ipairs(pipelines_data) do + local pipeline = data.pipeline + local lines = data.lines - for i, pipeline_job in ipairs(M.pipeline_jobs) do - M.color_status(pipeline_job.status, bufnr, lines[7 + i], 7 + i) + M.color_status(pipeline.status, bufnr, all_lines[line_offset + 1], line_offset + 1) + + for j, pipeline_job in ipairs(data.jobs) do + M.color_status(pipeline_job.status, bufnr, all_lines[line_offset + 7 + j], line_offset + 7 + j) + end + + line_offset = line_offset + #lines end - pipeline_popup.border:set_text("top", "Pipeline Status", "center") + pipeline_popup.border:set_text("top", "Pipelines Status", "center") popup.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs) u.switch_can_edit_buf(bufnr, false) end) end - M.retrigger = function() - M.latest_pipeline = get_latest_pipeline() + M.latest_pipeline = get_latest_pipelines() if not M.latest_pipeline then return end @@ -173,12 +226,8 @@ end ---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] +M.get_pipeline_icon = function(idx, wrap_with_color) + local symbol = state.settings.pipeline[state.PIPELINE[idx].latest_pipeline.status] if not wrap_with_color then return symbol end @@ -196,12 +245,13 @@ end ---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) +M.get_pipeline_status = function(idx, wrap_with_color) + return string.format( + "[%s]: Status: %s (%s)", + state.PIPELINE[idx].name, + M.get_pipeline_icon(idx, wrap_with_color), + state.PIPELINE[idx].latest_pipeline.status + ) end M.color_status = function(status, bufnr, status_line, linnr)