diff --git a/README.md b/README.md index 5a6e3b9..c3a86d9 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ require("gitlab").setup({ 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) +}, discussion_tree = { -- The discussion tree that holds all comments blacklist = {}, -- List of usernames to remove from tree (bots, CI, etc) 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 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 = { added_file = "", -- The symbol to show next to added 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. -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 `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 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 require("gitlab").review() require("gitlab").create_comment() ``` +### Discussions and Notes + 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. - -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. +To display all discussions for the current MR, use the `toggle_discussions` action, which will show the discussions in a split window. ```lua 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 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 require("gitlab").approve() 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 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 require("gitlab").add_reviewer() @@ -200,7 +207,7 @@ require("gitlab").add_assignee() require("gitlab").delete_assignee() ``` -These commands use Neovim's built in picker, which is much nicer if you install dressing. If you use Dressing, please enable it: +These actions use Neovim's built in picker, which is much nicer if you install dressing. If you use Dressing, please enable it: ```lua 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 ``` -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. diff --git a/cmd/job.go b/cmd/job.go new file mode 100644 index 0000000..de3fb34 --- /dev/null +++ b/cmd/job.go @@ -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) +} diff --git a/cmd/main.go b/cmd/main.go index 5105caa..b4c097f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -41,6 +41,7 @@ func main() { m.Handle("/reply", withGitlabContext(http.HandlerFunc(ReplyHandler), c)) m.Handle("/members", withGitlabContext(http.HandlerFunc(ProjectMembersHandler), c)) m.Handle("/pipeline", withGitlabContext(http.HandlerFunc(PipelineHandler), c)) + m.Handle("/job", withGitlabContext(http.HandlerFunc(JobHandler), c)) port := fmt.Sprintf(":%s", os.Args[3]) server := &http.Server{ diff --git a/lua/gitlab/actions/pipeline.lua b/lua/gitlab/actions/pipeline.lua index 2b2cca2..be425d0 100644 --- a/lua/gitlab/actions/pipeline.lua +++ b/lua/gitlab/actions/pipeline.lua @@ -5,7 +5,10 @@ local Popup = require("nui.popup") local state = require("gitlab.state") local job = require("gitlab.job") 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 M.open = function() @@ -19,11 +22,13 @@ M.open = function() local body = { pipeline_id = state.INFO.pipeline.id } job.run_job("/pipeline", "GET", body, function(data) 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 height = 6 + #pipeline_jobs + 3 local pipeline_popup = Popup(u.create_popup_state("Loading Pipeline...", width, height)) + M.pipeline_popup = pipeline_popup pipeline_popup:mount() local bufnr = vim.api.nvim_get_current_buf() @@ -54,7 +59,7 @@ M.open = function() end 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) end) end) @@ -73,6 +78,59 @@ M.retrigger = function() 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) 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/state.lua b/lua/gitlab/state.lua index fab95db..a84967a 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -14,6 +14,7 @@ M.settings = { popup = { exit = "", perform_action = "s", + perform_linewise_action = "l", }, discussion_tree = { blacklist = {}, @@ -110,7 +111,7 @@ local function exit(popup) end -- 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 }) if action ~= nil then vim.keymap.set('n', M.settings.popup.perform_action, function() @@ -119,6 +120,15 @@ M.set_popup_keymaps = function(popup, action) action(text) end, { buffer = true }) 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 -- Dependencies