diff --git a/README.md b/README.md index c607ece..591cf82 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/ab5a8597-32fa-4a28 ## Requirements -- Go >= v1.19 +- Go >= v1.19 ## Quick Start @@ -87,6 +87,7 @@ require("gitlab").setup({ port = 21036, -- The port of the Go server, which runs in the background log_path = vim.fn.stdpath("cache") .. "/gitlab.nvim.log", -- Log path for the Go server reviewer = "delta", -- The reviewer type ("delta" or "diffview") + attachment_dir = nil, -- The local directory for files (see the "summary" section) 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) @@ -144,12 +145,16 @@ 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. +### Summary + 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() ``` +### Reviewing Diffs + The `review` action will open a diff of the changes. You can leave comments using the `create_comment` action. ```lua @@ -157,6 +162,8 @@ require("gitlab").review() require("gitlab").create_comment() ``` +The reviewer is Delta by default, but you can configure the plugin to use Diffview instead. + ### Discussions and Notes Gitlab groups threads of comments together into "discussions." @@ -177,6 +184,14 @@ If you'd like to create a note in an MR (like a comment, but not linked to a spe require("gitlab").create_note() ``` +### Uploading Files + +To attach a file to an MR description, reply, comment, and so forth use the `settings.popup.perform_linewise_action` keybinding when the the popup is open. This will open a picker that will look in the directory you specify in the `settings.attachment_dir` folder (this must be an absolute path) for files. + +When you have picked the file, it will be added to the current buffer at the current line. + +Use the `settings.popup.perform_action` to send the changes to Gitlab. + ### MR Approvals You can approve or revoke approval for an MR with the `approve` and `revoke` actions respectively. diff --git a/cmd/attachment.go b/cmd/attachment.go new file mode 100644 index 0000000..da78a74 --- /dev/null +++ b/cmd/attachment.go @@ -0,0 +1,71 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" +) + +type AttachmentRequest struct { + FilePath string `json:"file_path"` + FileName string `json:"file_name"` +} + +type AttachmentResponse struct { + SuccessResponse + Markdown string `json:"markdown"` + Alt string `json:"alt"` + Url string `json:"url"` +} + +func AttachmentHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + c := r.Context().Value("client").(Client) + w.Header().Set("Content-Type", "application/json") + + var attachmentRequest AttachmentRequest + body, err := io.ReadAll(r.Body) + if err != nil { + c.handleError(w, err, "Could not read request body", http.StatusBadRequest) + return + } + + defer r.Body.Close() + + err = json.Unmarshal(body, &attachmentRequest) + if err != nil { + c.handleError(w, err, "Could not unmarshal JSON", http.StatusBadRequest) + return + } + + file, err := os.Open(attachmentRequest.FilePath) + if err != nil { + c.handleError(w, err, fmt.Sprintf("Could not read %s", attachmentRequest.FilePath), http.StatusBadRequest) + return + } + + defer file.Close() + + projectFile, res, err := c.git.Projects.UploadFile(c.projectId, file, attachmentRequest.FileName) + if err != nil { + c.handleError(w, err, fmt.Sprintf("Could not upload %s to Gitlab", attachmentRequest.FilePath), res.StatusCode) + return + } + + fileResponse := AttachmentResponse{ + SuccessResponse: SuccessResponse{ + Status: http.StatusOK, + Message: "File uploaded successfully", + }, + Markdown: projectFile.Markdown, + Alt: projectFile.Alt, + Url: projectFile.URL, + } + + json.NewEncoder(w).Encode(fileResponse) +} diff --git a/cmd/main.go b/cmd/main.go index b4c097f..f4c67dc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -30,6 +30,7 @@ func main() { m := http.NewServeMux() m.Handle("/mr/description", withGitlabContext(http.HandlerFunc(DescriptionHandler), c)) + m.Handle("/mr/attachment", withGitlabContext(http.HandlerFunc(AttachmentHandler), c)) m.Handle("/mr/reviewer", withGitlabContext(http.HandlerFunc(ReviewersHandler), c)) m.Handle("/mr/revisions", withGitlabContext(http.HandlerFunc(RevisionsHandler), c)) m.Handle("/mr/assignee", withGitlabContext(http.HandlerFunc(AssigneesHandler), c)) diff --git a/lua/gitlab/actions/comment.lua b/lua/gitlab/actions/comment.lua index 50bc1d6..660e5f6 100644 --- a/lua/gitlab/actions/comment.lua +++ b/lua/gitlab/actions/comment.lua @@ -6,6 +6,7 @@ local state = require("gitlab.state") local job = require("gitlab.job") local u = require("gitlab.utils") local discussions = require("gitlab.actions.discussions") +local miscellaneous = require("gitlab.actions.miscellaneous") local reviewer = require("gitlab.reviewer") local M = {} @@ -17,14 +18,14 @@ M.create_comment = function() comment_popup:mount() state.set_popup_keymaps(comment_popup, function(text) M.confirm_create_comment(text) - end) + end, miscellaneous.attach_file) end M.create_note = function() note_popup:mount() state.set_popup_keymaps(note_popup, function(text) M.confirm_create_comment(text, true) - end) + end, miscellaneous.attach_file) end -- This function (settings.popup.perform_action) will send the comment to the Go server diff --git a/lua/gitlab/actions/discussions.lua b/lua/gitlab/actions/discussions.lua index cc0ec98..08f0a0a 100644 --- a/lua/gitlab/actions/discussions.lua +++ b/lua/gitlab/actions/discussions.lua @@ -10,6 +10,7 @@ local job = require("gitlab.job") local u = require("gitlab.utils") local state = require("gitlab.state") local reviewer = require("gitlab.reviewer") +local miscellaneous = require("gitlab.actions.miscellaneous") local edit_popup = Popup(u.create_popup_state("Edit Comment", "80%", "80%")) local reply_popup = Popup(u.create_popup_state("Reply", "80%", "80%")) @@ -72,7 +73,7 @@ M.reply = function(tree) local discussion_node = M.get_root_node(tree, node) local id = tostring(discussion_node.id) reply_popup:mount() - state.set_popup_keymaps(reply_popup, M.send_reply(tree, id)) + state.set_popup_keymaps(reply_popup, M.send_reply(tree, id), miscellaneous.attach_file) end -- This function will send the reply to the Go API diff --git a/lua/gitlab/actions/miscellaneous.lua b/lua/gitlab/actions/miscellaneous.lua index 0f6ea08..42e7bc5 100644 --- a/lua/gitlab/actions/miscellaneous.lua +++ b/lua/gitlab/actions/miscellaneous.lua @@ -1,5 +1,7 @@ -local state = require("gitlab.state") -local M = {} +local state = require("gitlab.state") +local u = require("gitlab.utils") +local job = require("gitlab.job") +local M = {} M.open_in_browser = function() local url = state.INFO.web_url @@ -16,4 +18,33 @@ M.open_in_browser = function() end end +M.attach_file = function() + local attachment_dir = state.settings.attachment_dir + if not attachment_dir or attachment_dir == '' then + vim.notify("Must provide valid attachment_dir in plugin setup", vim.log.levels.ERROR) + return + end + + local files = u.list_files_in_folder(attachment_dir) + + if files == nil then + vim.notify(string.format("Could not list files in %s", attachment_dir), vim.log.levels.ERROR) + return + end + + vim.ui.select(files, { + prompt = 'Choose attachment', + }, function(choice) + if not choice then return end + local full_path = attachment_dir .. (u.is_windows() and "\\" or "/") .. choice + local body = { file_path = full_path, file_name = choice } + job.run_job("/mr/attachment", "POST", body, function(data) + local markdown = data.markdown + local current_line = u.get_current_line_number() + local bufnr = vim.api.nvim_get_current_buf() + vim.api.nvim_buf_set_lines(bufnr, current_line - 1, current_line, false, { markdown }) + end) + end) +end + return M diff --git a/lua/gitlab/actions/summary.lua b/lua/gitlab/actions/summary.lua index 8141f09..2727e20 100644 --- a/lua/gitlab/actions/summary.lua +++ b/lua/gitlab/actions/summary.lua @@ -4,6 +4,7 @@ local Popup = require("nui.popup") local job = require("gitlab.job") local state = require("gitlab.state") +local miscellaneous = require("gitlab.actions.miscellaneous") local u = require("gitlab.utils") local M = {} @@ -23,7 +24,7 @@ M.summary = function() vim.schedule(function() vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines) descriptionPopup.border:set_text("top", title, "center") - state.set_popup_keymaps(descriptionPopup, M.edit_description) + state.set_popup_keymaps(descriptionPopup, M.edit_description, miscellaneous.attach_file) end) end diff --git a/lua/gitlab/server.lua b/lua/gitlab/server.lua index 765bc97..9e3e38b 100644 --- a/lua/gitlab/server.lua +++ b/lua/gitlab/server.lua @@ -59,7 +59,6 @@ M.build = function(override) local command = string.format(cmd, state.settings.bin_path) local null = u.is_windows() and " >NUL" or " > /dev/null" - print(command .. null) local installCode = os.execute(command .. null) if installCode ~= 0 then vim.notify("Could not install gitlab.nvim!", vim.log.levels.ERROR) diff --git a/lua/gitlab/state.lua b/lua/gitlab/state.lua index a84967a..2fb7391 100644 --- a/lua/gitlab/state.lua +++ b/lua/gitlab/state.lua @@ -11,6 +11,7 @@ M.settings = { port = 21036, log_path = (vim.fn.stdpath("cache") .. "/gitlab.nvim.log"), reviewer = "delta", + attachment_dir = '', popup = { exit = "", perform_action = "s", diff --git a/lua/gitlab/utils/init.lua b/lua/gitlab/utils/init.lua index 5a47593..822a3f5 100644 --- a/lua/gitlab/utils/init.lua +++ b/lua/gitlab/utils/init.lua @@ -281,6 +281,37 @@ M.switch_can_edit_buf = function(buf, bool) vim.api.nvim_buf_set_option(buf, "readonly", not bool) end +M.list_files_in_folder = function(folder_path) + if vim.fn.isdirectory(folder_path) == 0 then + return nil + end + + local folder_ok, folder = pcall(vim.fn.readdir, folder_path) + + if not folder_ok then return nil end + + local files = {} + if folder ~= nil then + for _, file in ipairs(folder) do + local file_path = folder_path .. (M.is_windows() and "\\" or '/') .. file + local timestamp = vim.fn.getftime(file_path) + table.insert(files, { name = file, timestamp = timestamp }) + end + end + + -- Sort the table by timestamp in descending order (newest first) + table.sort(files, function(a, b) + return a.timestamp > b.timestamp + end) + + local result = {} + for _, file in ipairs(files) do + table.insert(result, file.name) + end + + return result +end + M.reverse = function(list) local rev = {} for i = #list, 1, -1 do diff --git a/todo.md b/todo.md index eea7fb9..5c498bb 100644 --- a/todo.md +++ b/todo.md @@ -1,5 +1,8 @@ ## Todo -- [ ] Fix the u.merge function to avoid overwriting settings -- [ ] Finish the Reply functionality -- [ ] Auto-Pick buffer when Cycling Through Comments +- Screenshot folder in config (where the images will be kept) +- Within the Summary view, you can call the add_summary_image() command +- This command will open a UI picker to choose the file +- When you choose the file, we pass that file path to an API endpoint which uploads +the file and returns the JSON in the API here (https://docs.gitlab.com/ee/api/projects.html#upload-a-file) +- Then we write that into the Summary buffer at the current cursor