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:
committed by
GitHub
parent
26a133be44
commit
94fdf5f38a
47
README.md
47
README.md
@@ -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
59
cmd/job.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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{
|
||||||
|
|||||||
@@ -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]))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user