Feat: See Pipeline Job Logs (#54)

This MR adds the ability to see log traces associated with Gitlab CI jobs, via the new `perform_linewise_action` keybinding in the pipeline popup.
This commit is contained in:
Harrison (Harry) Cramer
2023-09-03 19:44:12 -04:00
committed by GitHub
parent 26a133be44
commit 94fdf5f38a
5 changed files with 158 additions and 23 deletions

View File

@@ -90,7 +90,8 @@ require("gitlab").setup({
popup = { -- The popup for comment creation, editing, and replying popup = { -- The popup for comment creation, editing, and replying
exit = "<Esc>", exit = "<Esc>",
perform_action = "<leader>s", -- Once in normal mode, does action (like saving comment or editing description, etc) perform_action = "<leader>s", -- Once in normal mode, does action (like saving comment or editing description, etc)
}, perform_linewise_action = "<leader>l", -- Once in normal mode, does the linewise action (see logs for this job, etc)
},
discussion_tree = { -- The discussion tree that holds all comments discussion_tree = { -- The discussion tree that holds all comments
blacklist = {}, -- List of usernames to remove from tree (bots, CI, etc) blacklist = {}, -- List of usernames to remove from tree (bots, CI, etc)
jump_to_file = "o", -- Jump to comment location in file jump_to_file = "o", -- Jump to comment location in file
@@ -106,7 +107,7 @@ require("gitlab").setup({
resolved = '', -- Symbol to show next to resolved discussions resolved = '', -- Symbol to show next to resolved discussions
unresolved = '', -- Symbol to show next to unresolved discussions unresolved = '', -- Symbol to show next to unresolved discussions
}, },
review_pane = { -- Specific settings for different reviewers review_pane = { -- Specific settings for different reviewers, only delta currently supported
delta = { delta = {
added_file = "", -- The symbol to show next to added files added_file = "", -- The symbol to show next to added files
modified_file = "", -- The symbol to show next to modified files modified_file = "", -- The symbol to show next to modified files
@@ -143,55 +144,61 @@ git checkout feature-branch
Then open Neovim. The `project_id` you specify in your configuration file must match the project_id of the Gitlab project your terminal is inside of. Then open Neovim. The `project_id` you specify in your configuration file must match the project_id of the Gitlab project your terminal is inside of.
The `summary` command will pull down the MR description into a buffer so that you can read it. To edit the description, edit the buffer and press the `perform_action` keybinding when in normal mode (it's `<leader>s` by default): The `summary` action will pull down the MR description into a buffer so that you can read it. To edit the description, use the `settings.popup.perform_action` keybinding.
```lua ```lua
require("gitlab").summary() require("gitlab").summary()
``` ```
The `review` command will open up view of all the changes that have been made in this MR compared to the target branch in a review pane. You can leave comments on the changes. The `review` action will open a diff of the changes. You can leave comments using the `create_comment` action.
```lua ```lua
require("gitlab").review() require("gitlab").review()
require("gitlab").create_comment() require("gitlab").create_comment()
``` ```
### Discussions and Notes
Gitlab groups threads of comments together into "discussions." Gitlab groups threads of comments together into "discussions."
To display discussions for the current MR, use the `toggle_discussions()` command, which will show the discussions in a split window. To display all discussions for the current MR, use the `toggle_discussions` action, which will show the discussions in a split window.
You can jump to the comment's location the reviewer window by using the `m` key, or the actual file with the 'j' key, when hovering over the line in the tree.
Within the discussion tree, you can delete/edit/reply to comments, or toggle them as resolved or not.
```lua ```lua
require("gitlab").toggle_discussions() require("gitlab").toggle_discussions()
require("gitlab").delete_comment()
require("gitlab").edit_comment()
require("gitlab").reply()
require("gitlab").toggle_resolved()
``` ```
If you'd like to create a note in an MR (like a comment, but not linked to a specific line) call the `create_note()` command. Similar commands are available on the note tree, which is visible next to the discussion tree for comments. You can jump to the comment's location in the reviewer window by using the `state.settings.discussion_tree.jump_to_reviewer` key, or the actual file with the 'state.settings.discussion_tree.jump_to_file' key.
Within the discussion tree, you can delete/edit/reply to comments with the `state.settings.discussion_tree.delete_comment` `state.settings.discussion_tree.edit_comment` and `state.settings.discussion_tree.reply` keys, and toggle them as resolved with the `state.settings.discussion_tree.toggle_resolved` key.
If you'd like to create a note in an MR (like a comment, but not linked to a specific line) use the `create_note` action. The same keybindings for delete/edit/reply are available on the note tree.
```lua ```lua
require("gitlab").create_note() require("gitlab").create_note()
``` ```
You can approve or revoke approval for an MR: ### MR Approvals
You can approve or revoke approval for an MR with the `approve` and `revoke` actions respectively.
```lua ```lua
require("gitlab").approve() require("gitlab").approve()
require("gitlab").revoke() require("gitlab").revoke()
``` ```
You can view the status of the pipeline for the current MR. To re-trigger failed jobs in the pipeline manually, use your `settings.popup.perform_action` keybinding: ### Pipelines
You can view the status of the pipeline for the current MR with the `pipeline` action.
```lua ```lua
require("gitlab").pipeline() require("gitlab").pipeline()
``` ```
The `add_reviewer` and `delete_reviewer` commands, as well as the `add_assignee` and `delete_assignee` functions, will let you choose from a list of users who are availble in the current project: To re-trigger failed jobs in the pipeline manually, use the `settings.popup.perform_action` keybinding. To open the log trace of a job in a new Neovim buffer, use your `settings.popup.perform_linewise_action` keybinding.
### Reviewers and Assignees
The `add_reviewer` and `delete_reviewer` actions, as well as the `add_assignee` and `delete_assignee` functions, will let you choose from a list of users who are availble in the current project:
```lua ```lua
require("gitlab").add_reviewer() require("gitlab").add_reviewer()
@@ -200,7 +207,7 @@ require("gitlab").add_assignee()
require("gitlab").delete_assignee() require("gitlab").delete_assignee()
``` ```
These commands use Neovim's built in picker, which is much nicer if you install <a href="https://github.com/stevearc/dressing.nvim">dressing</a>. If you use Dressing, please enable it: These actions use Neovim's built in picker, which is much nicer if you install <a href="https://github.com/stevearc/dressing.nvim">dressing</a>. If you use Dressing, please enable it:
```lua ```lua
require("dressing").setup({ require("dressing").setup({
@@ -246,9 +253,9 @@ You can directly interact with the Go server like any other process:
curl --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" localhost:21036/info curl --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" localhost:21036/info
``` ```
This is the API call that is happening from within Neovim when you run the `summary` command. This is the API call that is happening from within Neovim when you run the `summary` action.
If you are able to build and start the Go server and hit the endpoint successfully for the command you are trying to run (such as creating a comment or approving a merge request) then something is wrong with the Lua code. In that case, please file a bug report. If you are able to build and start the Go server and hit the endpoint successfully for the action you are trying to run (such as creating a comment or approving a merge request) then something is wrong with the Lua code. In that case, please file a bug report.
This Go server, in turn, writes logs to the log path that is configured in your setup function. These are written by default to `~/.cache/nvim/gitlab.nvim.log` and will be written each time the server reaeches out to Gitlab. This Go server, in turn, writes logs to the log path that is configured in your setup function. These are written by default to `~/.cache/nvim/gitlab.nvim.log` and will be written each time the server reaeches out to Gitlab.

59
cmd/job.go Normal file
View File

@@ -0,0 +1,59 @@
package main
import (
"encoding/json"
"errors"
"io"
"net/http"
)
type JobTraceRequest struct {
JobId int `json:"job_id"`
}
type JobTraceResponse struct {
SuccessResponse
File string `json:"file"`
}
func JobHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
c := r.Context().Value("client").(Client)
if r.Method != http.MethodGet {
w.Header().Set("Allow", http.MethodGet)
c.handleError(w, errors.New("Invalid request type"), "That request type is not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
c.handleError(w, err, "Could not read request body", http.StatusBadRequest)
}
defer r.Body.Close()
var jobTraceRequest JobTraceRequest
err = json.Unmarshal(body, &jobTraceRequest)
if err != nil {
c.handleError(w, err, "Could not unmarshal data from request body", http.StatusBadRequest)
}
reader, _, err := c.git.Jobs.GetTraceFile(c.projectId, jobTraceRequest.JobId)
file, err := io.ReadAll(reader)
if err != nil {
c.handleError(w, err, "Could not read job trace file", http.StatusBadRequest)
}
response := JobTraceResponse{
SuccessResponse: SuccessResponse{
Status: http.StatusOK,
Message: "Log file read",
},
File: string(file),
}
json.NewEncoder(w).Encode(response)
}

View File

@@ -41,6 +41,7 @@ func main() {
m.Handle("/reply", withGitlabContext(http.HandlerFunc(ReplyHandler), c)) m.Handle("/reply", withGitlabContext(http.HandlerFunc(ReplyHandler), c))
m.Handle("/members", withGitlabContext(http.HandlerFunc(ProjectMembersHandler), c)) m.Handle("/members", withGitlabContext(http.HandlerFunc(ProjectMembersHandler), c))
m.Handle("/pipeline", withGitlabContext(http.HandlerFunc(PipelineHandler), c)) m.Handle("/pipeline", withGitlabContext(http.HandlerFunc(PipelineHandler), c))
m.Handle("/job", withGitlabContext(http.HandlerFunc(JobHandler), c))
port := fmt.Sprintf(":%s", os.Args[3]) port := fmt.Sprintf(":%s", os.Args[3])
server := &http.Server{ server := &http.Server{

View File

@@ -5,7 +5,10 @@ local Popup = require("nui.popup")
local state = require("gitlab.state") local state = require("gitlab.state")
local job = require("gitlab.job") local job = require("gitlab.job")
local u = require("gitlab.utils") local u = require("gitlab.utils")
local M = {} local M = {
pipeline_jobs = nil,
pipeline_popup = nil
}
-- The function will render the Pipeline state in a popup -- The function will render the Pipeline state in a popup
M.open = function() M.open = function()
@@ -19,11 +22,13 @@ M.open = function()
local body = { pipeline_id = state.INFO.pipeline.id } local body = { pipeline_id = state.INFO.pipeline.id }
job.run_job("/pipeline", "GET", body, function(data) job.run_job("/pipeline", "GET", body, function(data)
local pipeline_jobs = u.reverse(type(data.Jobs) == "table" and data.Jobs or {}) local pipeline_jobs = u.reverse(type(data.Jobs) == "table" and data.Jobs or {})
M.pipeline_jobs = pipeline_jobs
local width = string.len(pipeline.web_url) + 10 local width = string.len(pipeline.web_url) + 10
local height = 6 + #pipeline_jobs + 3 local height = 6 + #pipeline_jobs + 3
local pipeline_popup = Popup(u.create_popup_state("Loading Pipeline...", width, height)) local pipeline_popup = Popup(u.create_popup_state("Loading Pipeline...", width, height))
M.pipeline_popup = pipeline_popup
pipeline_popup:mount() pipeline_popup:mount()
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
@@ -54,7 +59,7 @@ M.open = function()
end end
pipeline_popup.border:set_text("top", "Pipeline Status", "center") pipeline_popup.border:set_text("top", "Pipeline Status", "center")
state.set_popup_keymaps(pipeline_popup, M.retrigger) state.set_popup_keymaps(pipeline_popup, M.retrigger, M.see_logs)
u.switch_can_edit_buf(bufnr, false) u.switch_can_edit_buf(bufnr, false)
end) end)
end) end)
@@ -73,6 +78,59 @@ M.retrigger = function()
end) end)
end end
M.see_logs = function()
local bufnr = vim.api.nvim_get_current_buf()
local linnr = vim.api.nvim_win_get_cursor(0)[1]
local text = u.get_line_content(bufnr, linnr)
local last_word = u.get_last_chunk(text)
if last_word == nil then
vim.notify("Cannot find job name", vim.log.levels.ERROR)
return
end
local j = nil
for _, pipeline_job in ipairs(M.pipeline_jobs) do
if pipeline_job.name == last_word then j = pipeline_job end
end
if j == nil then
vim.notify("Cannot find job in state", vim.log.levels.ERROR)
return
end
local body = { job_id = j.id }
job.run_job("/job", "GET", body, function(data)
local file = data.file
if file == "" then
vim.notify("Log trace is empty", vim.log.levels.WARN)
return
end
local lines = {}
for line in file:gmatch("[^\n]+") do
table.insert(lines, line)
end
if #lines == 0 then
vim.notify("Log trace lines could not be parsed", vim.log.levels.ERROR)
return
end
M.pipeline_popup:unmount()
vim.cmd.enew()
bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
local job_file_path = string.format("/tmp/gitlab.nvim.job-%d", j.id)
vim.cmd("w! " .. job_file_path)
vim.cmd.bd()
vim.cmd.enew()
vim.cmd("term cat " .. job_file_path)
end)
end
M.color_status = function(status, bufnr, status_line, linnr) M.color_status = function(status, bufnr, status_line, linnr)
local ns_id = vim.api.nvim_create_namespace("GitlabNamespace") local ns_id = vim.api.nvim_create_namespace("GitlabNamespace")
vim.cmd(string.format("highlight default StatusHighlight guifg=%s", state.settings.pipeline[status])) vim.cmd(string.format("highlight default StatusHighlight guifg=%s", state.settings.pipeline[status]))

View File

@@ -14,6 +14,7 @@ M.settings = {
popup = { popup = {
exit = "<Esc>", exit = "<Esc>",
perform_action = "<leader>s", perform_action = "<leader>s",
perform_linewise_action = "<leader>l",
}, },
discussion_tree = { discussion_tree = {
blacklist = {}, blacklist = {},
@@ -110,7 +111,7 @@ local function exit(popup)
end end
-- These keymaps are buffer specific and are set dynamically when popups mount -- These keymaps are buffer specific and are set dynamically when popups mount
M.set_popup_keymaps = function(popup, action) M.set_popup_keymaps = function(popup, action, linewise_action)
vim.keymap.set('n', M.settings.popup.exit, function() exit(popup) end, { buffer = true }) vim.keymap.set('n', M.settings.popup.exit, function() exit(popup) end, { buffer = true })
if action ~= nil then if action ~= nil then
vim.keymap.set('n', M.settings.popup.perform_action, function() vim.keymap.set('n', M.settings.popup.perform_action, function()
@@ -119,6 +120,15 @@ M.set_popup_keymaps = function(popup, action)
action(text) action(text)
end, { buffer = true }) end, { buffer = true })
end end
if linewise_action ~= nil then
vim.keymap.set('n', M.settings.popup.perform_linewise_action, function()
local bufnr = vim.api.nvim_get_current_buf()
local linnr = vim.api.nvim_win_get_cursor(0)[1]
local text = u.get_line_content(bufnr, linnr)
linewise_action(text)
end, { buffer = true })
end
end end
-- Dependencies -- Dependencies