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