Feat: Upload Files (#59)
This MR adds the ability to add files to comments, notes, replys, and MR descriptions via a picker. The file will get uploaded to Gitlab and the filepath will be automatically added into the current popup buffer at the current line. You can then save the changes with the normal save functionality.
This commit is contained in:
committed by
GitHub
parent
45329f4d69
commit
4e473dab7e
17
README.md
17
README.md
@@ -15,7 +15,7 @@ https://github.com/harrisoncramer/gitlab.nvim/assets/32515581/ab5a8597-32fa-4a28
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- <a href="https://go.dev/">Go >= v1.19</a>
|
- <a href="https://go.dev/">Go</a> >= v1.19
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -87,6 +87,7 @@ require("gitlab").setup({
|
|||||||
port = 21036, -- The port of the Go server, which runs in the background
|
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
|
log_path = vim.fn.stdpath("cache") .. "/gitlab.nvim.log", -- Log path for the Go server
|
||||||
reviewer = "delta", -- The reviewer type ("delta" or "diffview")
|
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
|
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)
|
||||||
@@ -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.
|
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.
|
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()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Reviewing Diffs
|
||||||
|
|
||||||
The `review` action will open a diff of the changes. You can leave comments using the `create_comment` action.
|
The `review` action will open a diff of the changes. You can leave comments using the `create_comment` action.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
@@ -157,6 +162,8 @@ require("gitlab").review()
|
|||||||
require("gitlab").create_comment()
|
require("gitlab").create_comment()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The reviewer is Delta by default, but you can configure the plugin to use Diffview instead.
|
||||||
|
|
||||||
### Discussions and Notes
|
### Discussions and Notes
|
||||||
|
|
||||||
Gitlab groups threads of comments together into "discussions."
|
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()
|
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
|
### MR Approvals
|
||||||
|
|
||||||
You can approve or revoke approval for an MR with the `approve` and `revoke` actions respectively.
|
You can approve or revoke approval for an MR with the `approve` and `revoke` actions respectively.
|
||||||
|
|||||||
71
cmd/attachment.go
Normal file
71
cmd/attachment.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ func main() {
|
|||||||
|
|
||||||
m := http.NewServeMux()
|
m := http.NewServeMux()
|
||||||
m.Handle("/mr/description", withGitlabContext(http.HandlerFunc(DescriptionHandler), c))
|
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/reviewer", withGitlabContext(http.HandlerFunc(ReviewersHandler), c))
|
||||||
m.Handle("/mr/revisions", withGitlabContext(http.HandlerFunc(RevisionsHandler), c))
|
m.Handle("/mr/revisions", withGitlabContext(http.HandlerFunc(RevisionsHandler), c))
|
||||||
m.Handle("/mr/assignee", withGitlabContext(http.HandlerFunc(AssigneesHandler), c))
|
m.Handle("/mr/assignee", withGitlabContext(http.HandlerFunc(AssigneesHandler), c))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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 discussions = require("gitlab.actions.discussions")
|
local discussions = require("gitlab.actions.discussions")
|
||||||
|
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||||
local reviewer = require("gitlab.reviewer")
|
local reviewer = require("gitlab.reviewer")
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
@@ -17,14 +18,14 @@ M.create_comment = function()
|
|||||||
comment_popup:mount()
|
comment_popup:mount()
|
||||||
state.set_popup_keymaps(comment_popup, function(text)
|
state.set_popup_keymaps(comment_popup, function(text)
|
||||||
M.confirm_create_comment(text)
|
M.confirm_create_comment(text)
|
||||||
end)
|
end, miscellaneous.attach_file)
|
||||||
end
|
end
|
||||||
|
|
||||||
M.create_note = function()
|
M.create_note = function()
|
||||||
note_popup:mount()
|
note_popup:mount()
|
||||||
state.set_popup_keymaps(note_popup, function(text)
|
state.set_popup_keymaps(note_popup, function(text)
|
||||||
M.confirm_create_comment(text, true)
|
M.confirm_create_comment(text, true)
|
||||||
end)
|
end, miscellaneous.attach_file)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- This function (settings.popup.perform_action) will send the comment to the Go server
|
-- This function (settings.popup.perform_action) will send the comment to the Go server
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ local job = require("gitlab.job")
|
|||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
local reviewer = require("gitlab.reviewer")
|
local reviewer = require("gitlab.reviewer")
|
||||||
|
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||||
|
|
||||||
local edit_popup = Popup(u.create_popup_state("Edit Comment", "80%", "80%"))
|
local edit_popup = Popup(u.create_popup_state("Edit Comment", "80%", "80%"))
|
||||||
local reply_popup = Popup(u.create_popup_state("Reply", "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 discussion_node = M.get_root_node(tree, node)
|
||||||
local id = tostring(discussion_node.id)
|
local id = tostring(discussion_node.id)
|
||||||
reply_popup:mount()
|
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
|
end
|
||||||
|
|
||||||
-- This function will send the reply to the Go API
|
-- This function will send the reply to the Go API
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
local M = {}
|
local u = require("gitlab.utils")
|
||||||
|
local job = require("gitlab.job")
|
||||||
|
local M = {}
|
||||||
|
|
||||||
M.open_in_browser = function()
|
M.open_in_browser = function()
|
||||||
local url = state.INFO.web_url
|
local url = state.INFO.web_url
|
||||||
@@ -16,4 +18,33 @@ M.open_in_browser = function()
|
|||||||
end
|
end
|
||||||
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
|
return M
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
local Popup = require("nui.popup")
|
local Popup = require("nui.popup")
|
||||||
local job = require("gitlab.job")
|
local job = require("gitlab.job")
|
||||||
local state = require("gitlab.state")
|
local state = require("gitlab.state")
|
||||||
|
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||||
local u = require("gitlab.utils")
|
local u = require("gitlab.utils")
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ M.summary = function()
|
|||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines)
|
vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines)
|
||||||
descriptionPopup.border:set_text("top", title, "center")
|
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ M.build = function(override)
|
|||||||
|
|
||||||
local command = string.format(cmd, state.settings.bin_path)
|
local command = string.format(cmd, state.settings.bin_path)
|
||||||
local null = u.is_windows() and " >NUL" or " > /dev/null"
|
local null = u.is_windows() and " >NUL" or " > /dev/null"
|
||||||
print(command .. null)
|
|
||||||
local installCode = os.execute(command .. null)
|
local installCode = os.execute(command .. null)
|
||||||
if installCode ~= 0 then
|
if installCode ~= 0 then
|
||||||
vim.notify("Could not install gitlab.nvim!", vim.log.levels.ERROR)
|
vim.notify("Could not install gitlab.nvim!", vim.log.levels.ERROR)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ M.settings = {
|
|||||||
port = 21036,
|
port = 21036,
|
||||||
log_path = (vim.fn.stdpath("cache") .. "/gitlab.nvim.log"),
|
log_path = (vim.fn.stdpath("cache") .. "/gitlab.nvim.log"),
|
||||||
reviewer = "delta",
|
reviewer = "delta",
|
||||||
|
attachment_dir = '',
|
||||||
popup = {
|
popup = {
|
||||||
exit = "<Esc>",
|
exit = "<Esc>",
|
||||||
perform_action = "<leader>s",
|
perform_action = "<leader>s",
|
||||||
|
|||||||
@@ -281,6 +281,37 @@ M.switch_can_edit_buf = function(buf, bool)
|
|||||||
vim.api.nvim_buf_set_option(buf, "readonly", not bool)
|
vim.api.nvim_buf_set_option(buf, "readonly", not bool)
|
||||||
end
|
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)
|
M.reverse = function(list)
|
||||||
local rev = {}
|
local rev = {}
|
||||||
for i = #list, 1, -1 do
|
for i = #list, 1, -1 do
|
||||||
|
|||||||
9
todo.md
9
todo.md
@@ -1,5 +1,8 @@
|
|||||||
## Todo
|
## Todo
|
||||||
|
|
||||||
- [ ] Fix the u.merge function to avoid overwriting settings
|
- Screenshot folder in config (where the images will be kept)
|
||||||
- [ ] Finish the Reply functionality
|
- Within the Summary view, you can call the add_summary_image() command
|
||||||
- [ ] Auto-Pick buffer when Cycling Through Comments
|
- 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
|
||||||
|
|||||||
Reference in New Issue
Block a user