Skip to content

Commit ef06194

Browse files
authored
feat(pipeline): display trigger jobs for a pipeline in the pipelines popup (#465)
1 parent 986cfbc commit ef06194

File tree

4 files changed

+174
-77
lines changed

4 files changed

+174
-77
lines changed

.github/CONTRIBUTING.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ It's possible that the feature you want is already implemented, or does not belo
1010

1111
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:
1212

13-
```lua
13+
```lua
1414
{
1515
"harrisoncramer/gitlab.nvim",
1616
dependencies = {
@@ -54,8 +54,8 @@ $ luacheck --globals vim busted --no-max-line-length -- .
5454

5555
4. Make the merge request to the `develop` branch of `.gitlab.nvim`
5656

57-
Please provide a description of the feature, and links to any relevant issues.
57+
Please provide a description of the feature, and links to any relevant issues.
5858

59-
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.
59+
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.
6060

6161
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.

cmd/app/pipeline.go

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,18 @@ type RetriggerPipelineResponse struct {
2020
type PipelineWithJobs struct {
2121
Jobs []*gitlab.Job `json:"jobs"`
2222
LatestPipeline *gitlab.PipelineInfo `json:"latest_pipeline"`
23+
Name string `json:"name"`
2324
}
2425

2526
type GetPipelineAndJobsResponse struct {
2627
SuccessResponse
27-
Pipeline PipelineWithJobs `json:"latest_pipeline"`
28+
Pipelines []PipelineWithJobs `json:"latest_pipeline"`
2829
}
2930

3031
type PipelineManager interface {
3132
ListProjectPipelines(pid interface{}, opt *gitlab.ListProjectPipelinesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.PipelineInfo, *gitlab.Response, error)
3233
ListPipelineJobs(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Job, *gitlab.Response, error)
34+
ListPipelineBridges(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Bridge, *gitlab.Response, error)
3335
RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error)
3436
}
3537

@@ -101,7 +103,6 @@ func (a pipelineService) GetPipelineAndJobs(w http.ResponseWriter, r *http.Reque
101103
}
102104

103105
jobs, res, err := a.client.ListPipelineJobs(a.projectInfo.ProjectId, pipeline.ID, &gitlab.ListJobsOptions{})
104-
105106
if err != nil {
106107
handleError(w, err, "Could not get pipeline jobs", http.StatusInternalServerError)
107108
return
@@ -112,13 +113,51 @@ func (a pipelineService) GetPipelineAndJobs(w http.ResponseWriter, r *http.Reque
112113
return
113114
}
114115

116+
pipelines := []PipelineWithJobs{}
117+
pipelines = append(pipelines, PipelineWithJobs{
118+
Jobs: jobs,
119+
LatestPipeline: pipeline,
120+
Name: "root",
121+
})
122+
123+
bridges, res, err := a.client.ListPipelineBridges(a.projectInfo.ProjectId, pipeline.ID, &gitlab.ListJobsOptions{})
124+
125+
if err != nil {
126+
handleError(w, err, "Could not get pipeline trigger jobs", http.StatusInternalServerError)
127+
return
128+
}
129+
if res.StatusCode >= 300 {
130+
handleError(w, GenericError{r.URL.Path}, "Could not get pipeline trigger jobs", res.StatusCode)
131+
return
132+
}
133+
134+
for _, bridge := range bridges {
135+
if bridge.DownstreamPipeline == nil {
136+
continue
137+
}
138+
139+
pipelineIdInBridge := bridge.DownstreamPipeline.ID
140+
bridgePipelineJobs, res, err := a.client.ListPipelineJobs(bridge.DownstreamPipeline.ProjectID, pipelineIdInBridge, &gitlab.ListJobsOptions{})
141+
if err != nil {
142+
handleError(w, err, "Could not get jobs for a pipeline from a trigger job", http.StatusInternalServerError)
143+
return
144+
}
145+
if res.StatusCode >= 300 {
146+
handleError(w, GenericError{r.URL.Path}, "Could not get jobs for a pipeline from a trigger job", res.StatusCode)
147+
return
148+
}
149+
150+
pipelines = append(pipelines, PipelineWithJobs{
151+
Jobs: bridgePipelineJobs,
152+
LatestPipeline: bridge.DownstreamPipeline,
153+
Name: bridge.Name,
154+
})
155+
}
156+
115157
w.WriteHeader(http.StatusOK)
116158
response := GetPipelineAndJobsResponse{
117159
SuccessResponse: SuccessResponse{Message: "Pipeline retrieved"},
118-
Pipeline: PipelineWithJobs{
119-
LatestPipeline: pipeline,
120-
Jobs: jobs,
121-
},
160+
Pipelines: pipelines,
122161
}
123162

124163
err = json.NewEncoder(w).Encode(response)

cmd/app/pipeline_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ func (f fakePipelineManager) ListPipelineJobs(pid interface{}, pipelineID int, o
2727
return []*gitlab.Job{}, resp, err
2828
}
2929

30+
func (f fakePipelineManager) ListPipelineBridges(pid interface{}, pipelineID int, opts *gitlab.ListJobsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Bridge, *gitlab.Response, error) {
31+
resp, err := f.handleGitlabError()
32+
if err != nil {
33+
return nil, nil, err
34+
}
35+
return []*gitlab.Bridge{}, resp, err
36+
}
37+
3038
func (f fakePipelineManager) RetryPipelineBuild(pid interface{}, pipeline int, options ...gitlab.RequestOptionFunc) (*gitlab.Pipeline, *gitlab.Response, error) {
3139
resp, err := f.handleGitlabError()
3240
if err != nil {

lua/gitlab/actions/pipeline.lua

Lines changed: 118 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -12,94 +12,147 @@ local M = {
1212
pipeline_popup = nil,
1313
}
1414

15-
local function get_latest_pipeline()
16-
local pipeline = state.PIPELINE and state.PIPELINE.latest_pipeline
17-
if type(pipeline) ~= "table" or (type(pipeline) == "table" and u.table_size(pipeline) == 0) then
18-
u.notify("Pipeline not found", vim.log.levels.WARN)
19-
return
15+
local function get_latest_pipelines(count)
16+
count = count or 1 -- Default to 1 if count is not provided
17+
local pipelines = {}
18+
19+
if not state.PIPELINE then
20+
u.notify("Pipeline state is not initialized", vim.log.levels.WARN)
21+
return nil
2022
end
21-
return pipeline
22-
end
2323

24-
local function get_pipeline_jobs()
25-
M.latest_pipeline = get_latest_pipeline()
26-
if not M.latest_pipeline then
27-
return
24+
for i = 1, math.max(count, #state.PIPELINE) do
25+
local pipeline = state.PIPELINE[i].latest_pipeline
26+
if type(pipeline) == "table" and u.table_size(pipeline) > 0 then
27+
table.insert(pipelines, pipeline)
28+
end
29+
end
30+
31+
if #pipelines == 0 then
32+
u.notify("No valid pipelines found", vim.log.levels.WARN)
33+
return nil
2834
end
29-
return u.reverse(type(state.PIPELINE.jobs) == "table" and state.PIPELINE.jobs or {})
35+
return pipelines
36+
end
37+
38+
local function get_pipeline_jobs(idx)
39+
return u.reverse(type(state.PIPELINE[idx].jobs) == "table" and state.PIPELINE[idx].jobs or {})
3040
end
3141

3242
-- The function will render the Pipeline state in a popup
3343
M.open = function()
34-
M.pipeline_jobs = get_pipeline_jobs()
35-
M.latest_pipeline = get_latest_pipeline()
36-
if M.latest_pipeline == nil then
44+
M.latest_pipelines = get_latest_pipelines()
45+
if not M.latest_pipelines then
46+
return
47+
end
48+
if not M.latest_pipelines or #M.latest_pipelines == 0 then
3749
return
3850
end
3951

40-
local width = string.len(M.latest_pipeline.web_url) + 10
41-
local height = 6 + #M.pipeline_jobs + 3
52+
local max_width = 0
53+
local total_height = 0
54+
local pipelines_data = {}
55+
56+
for idx, pipeline in ipairs(M.latest_pipelines) do
57+
local width = string.len(pipeline.web_url) + 10
58+
max_width = math.max(max_width, width)
59+
local pipeline_jobs = get_pipeline_jobs(idx)
60+
local pipeline_status = M.get_pipeline_status(idx, false)
61+
local height = 6 + #pipeline_jobs + 3
62+
total_height = total_height + height
63+
64+
table.insert(pipelines_data, {
65+
pipeline = pipeline,
66+
pipeline_status = pipeline_status,
67+
jobs = pipeline_jobs,
68+
width = width,
69+
height = 6 + #pipeline_jobs + 3,
70+
lines = {},
71+
})
72+
end
4273

4374
local pipeline_popup =
44-
Popup(popup.create_popup_state("Loading Pipeline...", state.settings.popup.pipeline, width, height, 60))
75+
Popup(popup.create_popup_state("Loading Pipelines...", state.settings.popup.pipeline, max_width, total_height, 60))
4576
popup.set_up_autocommands(pipeline_popup, nil, vim.api.nvim_get_current_win())
4677
M.pipeline_popup = pipeline_popup
4778
pipeline_popup:mount()
4879

4980
local bufnr = vim.api.nvim_get_current_buf()
5081
vim.opt_local.wrap = false
5182

52-
local lines = {}
53-
5483
u.switch_can_edit_buf(bufnr, true)
55-
table.insert(lines, "Status: " .. M.get_pipeline_status(false))
56-
table.insert(lines, "")
57-
table.insert(lines, string.format("Last Run: %s", u.time_since(M.latest_pipeline.created_at)))
58-
table.insert(lines, string.format("Url: %s", M.latest_pipeline.web_url))
59-
table.insert(lines, string.format("Triggered By: %s", M.latest_pipeline.source))
60-
61-
table.insert(lines, "")
62-
table.insert(lines, "Jobs:")
63-
64-
local longest_title = u.get_longest_string(u.map(M.pipeline_jobs, function(v)
65-
return v.name
66-
end))
67-
68-
local function row_offset(name)
69-
local offset = longest_title - string.len(name)
70-
local res = string.rep(" ", offset + 5)
71-
return res
72-
end
7384

74-
for _, pipeline_job in ipairs(M.pipeline_jobs) do
75-
local offset = row_offset(pipeline_job.name)
76-
local row = string.format(
77-
"%s%s %s (%s)",
78-
pipeline_job.name,
79-
offset,
80-
state.settings.pipeline[pipeline_job.status] or "*",
81-
pipeline_job.status or ""
82-
)
83-
84-
table.insert(lines, row)
85+
local all_lines = {}
86+
for i, data in ipairs(pipelines_data) do
87+
local pipeline = data.pipeline
88+
local lines = data.lines
89+
90+
table.insert(lines, data.pipeline_status)
91+
table.insert(lines, "")
92+
table.insert(lines, string.format("Last Run: %s", u.time_since(pipeline.created_at)))
93+
table.insert(lines, string.format("Url: %s", pipeline.web_url))
94+
table.insert(lines, string.format("Triggered By: %s", pipeline.source))
95+
table.insert(lines, "")
96+
table.insert(lines, "Jobs:")
97+
98+
local longest_title = u.get_longest_string(u.map(data.jobs, function(v)
99+
return v.name
100+
end))
101+
102+
local function row_offset(name)
103+
local offset = longest_title - string.len(name)
104+
local res = string.rep(" ", offset + 5)
105+
return res
106+
end
107+
108+
for _, pipeline_job in ipairs(data.jobs) do
109+
local offset = row_offset(pipeline_job.name)
110+
local row = string.format(
111+
"%s%s %s (%s)",
112+
pipeline_job.name,
113+
offset,
114+
state.settings.pipeline[pipeline_job.status] or "*",
115+
pipeline_job.status or ""
116+
)
117+
table.insert(lines, row)
118+
end
119+
120+
-- Add separator between pipelines
121+
if i < #pipelines_data then
122+
table.insert(lines, "")
123+
table.insert(lines, string.rep("-", max_width))
124+
table.insert(lines, "")
125+
end
126+
127+
for _, line in ipairs(lines) do
128+
table.insert(all_lines, line)
129+
end
85130
end
86131

87132
vim.schedule(function()
88-
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
89-
M.color_status(M.latest_pipeline.status, bufnr, lines[1], 1)
133+
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, all_lines)
134+
135+
local line_offset = 0
136+
for _, data in ipairs(pipelines_data) do
137+
local pipeline = data.pipeline
138+
local lines = data.lines
90139

91-
for i, pipeline_job in ipairs(M.pipeline_jobs) do
92-
M.color_status(pipeline_job.status, bufnr, lines[7 + i], 7 + i)
140+
M.color_status(pipeline.status, bufnr, all_lines[line_offset + 1], line_offset + 1)
141+
142+
for j, pipeline_job in ipairs(data.jobs) do
143+
M.color_status(pipeline_job.status, bufnr, all_lines[line_offset + 7 + j], line_offset + 7 + j)
144+
end
145+
146+
line_offset = line_offset + #lines
93147
end
94148

95-
pipeline_popup.border:set_text("top", "Pipeline Status", "center")
149+
pipeline_popup.border:set_text("top", "Pipelines Status", "center")
96150
popup.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs)
97151
u.switch_can_edit_buf(bufnr, false)
98152
end)
99153
end
100-
101154
M.retrigger = function()
102-
M.latest_pipeline = get_latest_pipeline()
155+
M.latest_pipeline = get_latest_pipelines()
103156
if not M.latest_pipeline then
104157
return
105158
end
@@ -173,12 +226,8 @@ end
173226
---colorize the pipeline icon.
174227
---@param wrap_with_color boolean
175228
---@return string
176-
M.get_pipeline_icon = function(wrap_with_color)
177-
M.latest_pipeline = get_latest_pipeline()
178-
if not M.latest_pipeline then
179-
return ""
180-
end
181-
local symbol = state.settings.pipeline[M.latest_pipeline.status]
229+
M.get_pipeline_icon = function(idx, wrap_with_color)
230+
local symbol = state.settings.pipeline[state.PIPELINE[idx].latest_pipeline.status]
182231
if not wrap_with_color then
183232
return symbol
184233
end
@@ -196,12 +245,13 @@ end
196245
---colorize the pipeline icon.
197246
---@param wrap_with_color boolean
198247
---@return string
199-
M.get_pipeline_status = function(wrap_with_color)
200-
M.latest_pipeline = get_latest_pipeline()
201-
if not M.latest_pipeline then
202-
return ""
203-
end
204-
return string.format("%s (%s)", M.get_pipeline_icon(wrap_with_color), M.latest_pipeline.status)
248+
M.get_pipeline_status = function(idx, wrap_with_color)
249+
return string.format(
250+
"[%s]: Status: %s (%s)",
251+
state.PIPELINE[idx].name,
252+
M.get_pipeline_icon(idx, wrap_with_color),
253+
state.PIPELINE[idx].latest_pipeline.status
254+
)
205255
end
206256

207257
M.color_status = function(status, bufnr, status_line, linnr)

0 commit comments

Comments
 (0)