This commit is contained in:
2024-12-23 19:09:51 +01:00
parent c408cd3d24
commit d8a7da82fe
4 changed files with 426 additions and 474 deletions

View File

@@ -4,42 +4,287 @@ local config = require("gitea.config")
local api = require("gitea.api")
local auth = require("gitea.auth")
-- Utility: parse "owner/repo" from the remote URL
-- We'll keep detect_owner_repo() from before
local function detect_owner_repo()
local cmd = "git config remote.origin.url"
local url = vim.fn.systemlist(cmd)[1]
if vim.v.shell_error ~= 0 or not url or url == "" then
return nil, nil
end
-- Remove trailing ".git"
url = url:gsub("%.git$", "")
-- Try to parse: e.g. https://my.gitea.com/owner/repo
local _, after = url:match("^(https?://)(.*)$")
if after then
-- after might be "my.gitea.com/owner/repo"
-- let's capture the part after the first slash
local path = after:match("[^/]+/(.*)$") -- skip domain
local path = after:match("[^/]+/(.*)$")
if not path then
return nil, nil
end
local owner, repo = path:match("^(.-)/(.*)$")
return owner, repo
end
-- SSH style: git@my.gitea.com:owner/repo
local user_host, path = url:match("^(.-):(.*)$")
if user_host and path then
-- user_host might be "git@my.gitea.com"
local owner, repo = path:match("^(.-)/(.*)$")
return owner, repo
end
return nil, nil
end
-------------------------------
-- Subcommands
-------------------------------
--------------------------------------------------------------------------------
--- Data Structures to track what's in the ephemeral buffer
--------------------------------------------------------------------------------
-- We'll store some metadata for each "segment" in the buffer:
-- - The issue's title line range
-- - The issue's body line range
-- - Each existing comment (with start/end line range)
-- - Possibly space for a new comment
--
-- On save (:w), we compare what's changed and do the appropriate API calls.
--------------------------------------------------------------------------------
local function create_issue_buffer_metadata(issue)
-- track lines for the title, the body, each comment
local meta = {
issue_number = issue.number,
-- will fill in fields once we place them in the buffer
title_start = nil,
title_end = nil,
body_start = nil,
body_end = nil,
comments = {}, -- array of { id, start, end }
new_comment_start = nil, -- where user can type a new comment
}
return meta
end
--------------------------------------------------------------------------------
--- RENDER ISSUE INTO BUFFER
--------------------------------------------------------------------------------
local function render_issue_into_buf(bufnr, issue, comments)
-- We assume the buffer is empty and ephemeral.
local lines = {}
table.insert(lines, string.format("# Gitea Issue #%d: %s", issue.number, issue.title))
table.insert(lines, "# STATE: " .. (issue.state or "unknown"))
table.insert(lines, "# Edit the title below, then the body, then any comments.")
table.insert(lines, "# On save (:w), changes to the title/body will be updated; changes to comment lines will be updated, and new text at the bottom becomes a new comment.")
table.insert(lines, "")
-- store line index references for metadata
local meta = create_issue_buffer_metadata(issue)
-- place the "title line"
meta.title_start = #lines+1
table.insert(lines, issue.title or "")
meta.title_end = #lines
-- a blank line
table.insert(lines, "")
-- place the "body lines"
meta.body_start = #lines+1
if issue.body then
local body_lines = vim.split(issue.body, "\n", true)
if #body_lines == 0 then
table.insert(lines, "")
else
for _, l in ipairs(body_lines) do
table.insert(lines, l)
end
end
else
table.insert(lines, "No issue description.")
end
meta.body_end = #lines
-- spacing
table.insert(lines, "")
table.insert(lines, "# --- Comments ---")
local line_count = #lines
local comment_meta = {}
for _, c in ipairs(comments) do
table.insert(lines, "")
local start_line = #lines+1
-- We'll place a small header: e.g. "Comment (ID=12345) by username"
local author = c.user and c.user.login or c.user and c.user.username or c.user or "?"
table.insert(lines, string.format("COMMENT #%d by %s", c.id, author))
-- Then the actual comment body lines
local c_body_lines = vim.split(c.body or "", "\n", true)
if #c_body_lines == 0 then
table.insert(lines, "")
else
for _, cb in ipairs(c_body_lines) do
table.insert(lines, cb)
end
end
local end_line = #lines
table.insert(comment_meta, {
id = c.id,
start_line = start_line,
end_line = end_line,
})
end
-- place a blank line for user to add a new comment
table.insert(lines, "")
table.insert(lines, "# --- Add a new comment below this line. If you leave it blank, no new comment is created. ---")
local new_comment_start = #lines+1
table.insert(lines, "") -- an empty line to start from
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
-- store metadata
local b_meta = {
issue_number = issue.number,
title_start = meta.title_start,
title_end = meta.title_end,
body_start = meta.body_start,
body_end = meta.body_end,
comments = comment_meta,
new_comment_start = new_comment_start,
}
return b_meta
end
--------------------------------------------------------------------------------
--- Parse buffer changes and do API updates on save
--------------------------------------------------------------------------------
local function on_issue_buf_write(bufnr, metadata, owner, repo)
-- read all buffer lines
local new_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
----------------- Title update check -----------------
local new_title = table.concat(vim.list_slice(new_lines, metadata.title_start, metadata.title_end), "\n")
-- remove trailing newlines just in case
new_title = new_title:gsub("%s+$", "")
-- we do a get_issue call to see the current title
local current_issue_data = api.get_issue(owner, repo, metadata.issue_number)
if not current_issue_data then
vim.notify("[gitea.nvim] Could not re-fetch the issue to verify updates", vim.log.levels.ERROR)
return
end
local changed_title = false
if (current_issue_data.title or "") ~= new_title then
changed_title = true
end
----------------- Body update check ------------------
local new_body_lines = vim.list_slice(new_lines, metadata.body_start, metadata.body_end)
local new_body = table.concat(new_body_lines, "\n")
local changed_body = false
if (current_issue_data.body or "") ~= new_body then
changed_body = true
end
----------------- Comments update check -------------
local changed_comments = {}
for _, cmeta in ipairs(metadata.comments) do
local c_lines = vim.list_slice(new_lines, cmeta.start_line, cmeta.end_line)
-- The first line is "COMMENT #<id> by <author>"
-- The rest is the actual body
-- e.g. `COMMENT #112 by alice`
local first_line = c_lines[1] or ""
local c_body = {}
for i=2,#c_lines do
table.insert(c_body, c_lines[i])
end
local new_comment_body = table.concat(c_body, "\n")
-- We fetch the current comment from the server to check if changed
-- But Gitea's issue comment API does not necessarily let us get a single comment by ID easily.
-- Instead, we do "issue_data, with comments" or store old body in memory.
-- For simplicity, let's store the old body in an extmark or in a table. But let's just do a naive approach:
-- We'll assume if the line is changed, we want to patch it. We have no big reference of old body, so let's do it.
table.insert(changed_comments, {
id = cmeta.id,
new_body = new_comment_body
})
end
----------------- New comment check ------------------
local new_comment_body_lines = {}
for i=metadata.new_comment_start, #new_lines do
table.insert(new_comment_body_lines, new_lines[i])
end
local new_comment_body = table.concat(new_comment_body_lines, "\n"):gsub("^%s+", ""):gsub("%s+$", "")
local create_new_comment = (#new_comment_body > 0)
-------------- Perform the updates --------------
if changed_title or changed_body then
local updated, st = api.edit_issue(owner, repo, metadata.issue_number, {
title = changed_title and new_title or current_issue_data.title,
body = changed_body and new_body or current_issue_data.body,
})
if updated then
vim.notify("[gitea.nvim] Issue updated!")
else
vim.notify(string.format("[gitea.nvim] Failed to update issue (HTTP %s).", st or "?"), vim.log.levels.ERROR)
end
end
-- for each changed comment, attempt to patch
-- We do not do a diff, so we always do a "PATCH" if we suspect changes
-- => If the user changed nothing, Gitea might respond 200 anyway, no big deal
for _, cupd in ipairs(changed_comments) do
local updated_comment, st = api.edit_issue_comment(owner, repo, metadata.issue_number, cupd.id, cupd.new_body)
if not updated_comment then
vim.notify(string.format("[gitea.nvim] Failed to update comment %d. (HTTP %s)", cupd.id, st or "?"), vim.log.levels.ERROR)
else
vim.notify(string.format("[gitea.nvim] Comment #%d updated.", cupd.id))
end
end
if create_new_comment then
local created, st = api.comment_issue(owner, repo, metadata.issue_number, new_comment_body)
if created then
vim.notify("[gitea.nvim] New comment posted.")
else
vim.notify(string.format("[gitea.nvim] Failed to create new comment (HTTP %s).", st or "?"), vim.log.levels.ERROR)
end
end
end
--------------------------------------------------------------------------------
--- Open a single ephemeral buffer for the entire Issue with comments
--------------------------------------------------------------------------------
local function open_full_issue_buffer(owner, repo, number)
local issue_data, status = api.get_issue(owner, repo, number)
if not issue_data then
vim.notify(string.format("[gitea.nvim] Error retrieving issue #%d (HTTP %s).", number, status or "?"), vim.log.levels.ERROR)
return
end
-- get all existing comments from the Gitea API
-- for Gitea, we can do: GET /repos/{owner}/{repo}/issues/{index}/comments
-- The plugin's `api` might have a function for that, or we can add it quickly
local all_comments, cstatus = api.get_issue_comments(owner, repo, number)
if not all_comments then
vim.notify(string.format("[gitea.nvim] Error retrieving issue #%d comments (HTTP %s).", number, cstatus or "?"), vim.log.levels.ERROR)
return
end
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_option(buf, "buftype", "acwrite")
vim.api.nvim_buf_set_option(buf, "filetype", "markdown")
vim.api.nvim_buf_set_name(buf, string.format("gitea_issue_full_%d", number))
local metadata = render_issue_into_buf(buf, issue_data, all_comments)
-- We create an autocmd on BufWriteCmd to parse changes and do updates
vim.api.nvim_create_autocmd("BufWriteCmd", {
buffer = buf,
callback = function()
on_issue_buf_write(buf, metadata, owner, repo)
end
})
vim.api.nvim_set_current_buf(buf)
end
--------------------------------------------------------------------------------
--- SUBCOMMANDS
--------------------------------------------------------------------------------
local subcommands = {}
subcommands.issue = {
@@ -49,372 +294,120 @@ subcommands.issue = {
vim.notify("[gitea.nvim] Could not detect owner/repo.", vim.log.levels.ERROR)
return
end
-- use the same telescope approach as before:
local has_telescope, tele = pcall(require, "telescope")
if not has_telescope then
vim.notify("[gitea.nvim] telescope.nvim is not installed", vim.log.levels.ERROR)
return
end
local pickers = require("telescope.pickers")
local finders = require("telescope.finders")
local conf = require("telescope.config").values
local actions = require("telescope.actions")
local action_state = require("telescope.actions.state")
local issues, status = api.list_issues(owner, repo, {})
if not issues then
vim.notify(string.format("[gitea.nvim] Error fetching issues (HTTP %s).", status or "?"), vim.log.levels.ERROR)
return
end
-- For now, print to message area
print("Issues for " .. owner .. "/" .. repo .. ":")
for _, issue in ipairs(issues) do
print(string.format("#%d %s", issue.number, issue.title))
end
end,
create = function(args)
local owner, repo = detect_owner_repo()
if not owner or not repo then
vim.notify("[gitea.nvim] Could not detect owner/repo.", vim.log.levels.ERROR)
return
end
-- We'll open a scratch buffer for user to fill in Title/Body
-- Then upon writing, we create the issue.
-- For brevity, let's do a simpler approach: ask input in command line
local title = vim.fn.input("Issue title: ")
local body = vim.fn.input("Issue body: ")
local created, status = api.create_issue(owner, repo, {
title = title,
body = body,
})
if created then
vim.notify("[gitea.nvim] Created issue #" .. created.number .. "!")
else
vim.notify(string.format("[gitea.nvim] Failed to create issue (HTTP %s).", status or "?"), vim.log.levels.ERROR)
end
end,
edit = function(args)
local number = tonumber(args[1])
if not number then
vim.notify("[gitea.nvim] Usage: :Gitea issue edit <number>", vim.log.levels.ERROR)
return
end
local owner, repo = detect_owner_repo()
if not owner or not repo then
vim.notify("[gitea.nvim] Could not detect owner/repo.", vim.log.levels.ERROR)
return
end
-- For a minimal approach: fetch issue, then open in a scratch buffer
-- with title/body. On write, update it. We'll do a simple approach:
local issue_data, status = api.get_issue(owner, repo, number)
if not issue_data then
vim.notify(string.format("[gitea.nvim] Error retrieving issue (HTTP %s).", status or "?"), vim.log.levels.ERROR)
return
end
-- Create a new scratch buffer
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_option(buf, "filetype", "markdown")
vim.api.nvim_buf_set_name(buf, "gitea_issue_"..number)
local lines = {
"# Issue #"..issue_data.number,
"Title: "..(issue_data.title or ""),
"",
issue_data.body or "",
}
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
-- Attach an autocmd on BufWriteCmd to push changes
vim.api.nvim_create_autocmd("BufWriteCmd", {
buffer = buf,
callback = function()
local new_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
if #new_lines < 2 then return end
local new_title = new_lines[1]:gsub("^Title:%s*", "")
local new_body = table.concat({unpack(new_lines, 3)}, "\n")
local updated, st = api.edit_issue(owner, repo, number, {
title = new_title,
body = new_body,
})
if updated then
vim.notify("[gitea.nvim] Issue updated successfully!")
else
vim.notify(string.format("[gitea.nvim] Failed to update issue (HTTP %s).", st or "?"), vim.log.levels.ERROR)
pickers.new({}, {
prompt_title = string.format("Issues: %s/%s", owner, repo),
finder = finders.new_table {
results = issues,
entry_maker = function(issue)
return {
value = issue,
display = string.format("#%d %s", issue.number, issue.title),
ordinal = issue.number .. " " .. issue.title
}
end
end
})
vim.api.nvim_set_current_buf(buf)
},
sorter = conf.generic_sorter({}),
attach_mappings = function(prompt_bufnr, map)
local function select_issue()
local selection = action_state.get_selected_entry()
actions.close(prompt_bufnr)
if not selection or not selection.value then
return
end
-- Instead of just editing, we now open the full timeline buffer
open_full_issue_buffer(owner, repo, selection.value.number)
end
map("i", "<CR>", select_issue)
map("n", "<CR>", select_issue)
return true
end,
}):find()
end,
close = function(args)
show = function(args)
-- Alternative: show a single issue by number
local number = tonumber(args[1])
if not number then
vim.notify("[gitea.nvim] Usage: :Gitea issue close <number>", vim.log.levels.ERROR)
vim.notify("[gitea.nvim] Usage: :Gitea issue show <number>", vim.log.levels.ERROR)
return
end
local owner, repo = detect_owner_repo()
local res, status = api.close_issue(owner, repo, number)
if res then
vim.notify("[gitea.nvim] Issue #"..number.." closed.")
else
vim.notify(string.format("[gitea.nvim] Failed to close issue (HTTP %s).", status or "?"), vim.log.levels.ERROR)
if not owner or not repo then
vim.notify("[gitea.nvim] Could not detect owner/repo.", vim.log.levels.ERROR)
return
end
open_full_issue_buffer(owner, repo, number)
end,
reopen = function(args)
local number = tonumber(args[1])
if not number then
vim.notify("[gitea.nvim] Usage: :Gitea issue reopen <number>", vim.log.levels.ERROR)
return
end
local owner, repo = detect_owner_repo()
local res, status = api.reopen_issue(owner, repo, number)
if res then
vim.notify("[gitea.nvim] Issue #"..number.." reopened.")
else
vim.notify(string.format("[gitea.nvim] Failed to reopen issue (HTTP %s).", status or "?"), vim.log.levels.ERROR)
end
end,
comment = function(args)
local number = tonumber(args[1])
if not number then
vim.notify("[gitea.nvim] Usage: :Gitea issue comment <number>", vim.log.levels.ERROR)
return
end
local owner, repo = detect_owner_repo()
local body = vim.fn.input("Comment: ")
if not body or body == "" then
return
end
local res, status = api.comment_issue(owner, repo, number, body)
if res then
vim.notify("[gitea.nvim] Comment added to issue #"..number..".")
else
vim.notify(string.format("[gitea.nvim] Failed to add comment (HTTP %s).", status or "?"), vim.log.levels.ERROR)
end
end,
-- The rest of your commands can remain or be removed as needed
-- but presumably we can just use "show" for everything
-- or keep an existing minimal create command, etc.
}
subcommands.pr = {
list = function(args)
local owner, repo = detect_owner_repo()
if not owner or not repo then
vim.notify("[gitea.nvim] Could not detect owner/repo.", vim.log.levels.ERROR)
return
end
local prs, status = api.list_pull_requests(owner, repo, {})
if not prs then
vim.notify(string.format("[gitea.nvim] Error fetching PRs (HTTP %s).", status or "?"), vim.log.levels.ERROR)
return
end
print("Pull Requests for " .. owner .. "/" .. repo .. ":")
for _, pr in ipairs(prs) do
print(string.format("#%d %s (state=%s)", pr.number, pr.title, pr.state))
end
end,
create = function(args)
local owner, repo = detect_owner_repo()
if not owner or not repo then
vim.notify("[gitea.nvim] Could not detect owner/repo.", vim.log.levels.ERROR)
return
end
local title = vim.fn.input("PR title: ")
local body = vim.fn.input("PR body: ")
local base = vim.fn.input("Base branch (e.g. main): ")
local head = vim.fn.input("Head branch (e.g. feature/my-branch): ")
local created, status = api.create_pull_request(owner, repo, {
title = title,
body = body,
base = base,
head = head,
})
if created then
vim.notify("[gitea.nvim] Created PR #" .. created.number .. ".")
else
vim.notify(string.format("[gitea.nvim] Failed to create PR (HTTP %s).", status or "?"), vim.log.levels.ERROR)
end
end,
edit = function(args)
local number = tonumber(args[1])
if not number then
vim.notify("[gitea.nvim] Usage: :Gitea pr edit <number>", vim.log.levels.ERROR)
return
end
local owner, repo = detect_owner_repo()
local pr_data, status = api.get_pull_request(owner, repo, number)
if not pr_data then
vim.notify(string.format("[gitea.nvim] Could not get PR (HTTP %s).", status or "?"), vim.log.levels.ERROR)
return
end
-- Similar scratch buffer approach
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_option(buf, "filetype", "markdown")
vim.api.nvim_buf_set_name(buf, "gitea_pr_"..number)
local lines = {
"# PR #"..pr_data.number,
"Title: "..(pr_data.title or ""),
"",
pr_data.body or "",
}
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.api.nvim_create_autocmd("BufWriteCmd", {
buffer = buf,
callback = function()
local new_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
if #new_lines < 2 then return end
local new_title = new_lines[1]:gsub("^Title:%s*", "")
local new_body = table.concat({unpack(new_lines, 3)}, "\n")
local updated, st = api.edit_pull_request(owner, repo, number, {
title = new_title,
body = new_body,
})
if updated then
vim.notify("[gitea.nvim] PR updated successfully!")
else
vim.notify(string.format("[gitea.nvim] Failed to update PR (HTTP %s).", st or "?"), vim.log.levels.ERROR)
end
end
})
vim.api.nvim_set_current_buf(buf)
end,
merge = function(args)
local number = tonumber(args[1])
if not number then
vim.notify("[gitea.nvim] Usage: :Gitea pr merge <number> [merge|rebase|squash] [delete|nodelete]", vim.log.levels.ERROR)
return
end
local merge_style = args[2] or "merge" -- "merge", "rebase", "squash"
local delete_arg = args[3] or "nodelete"
local owner, repo = detect_owner_repo()
local merged, status = api.merge_pull_request(owner, repo, number, merge_style)
if merged then
vim.notify(string.format("[gitea.nvim] PR #%d merged via %s.", number, merge_style))
if delete_arg == "delete" then
-- The Gitea API doesn't directly delete the branch here,
-- but you can do so with the Git CLI or Gitea API call.
-- Deleting the branch example:
-- os.execute(string.format("git push origin --delete <branch>"))
vim.notify("[gitea.nvim] Branch deletion not implemented. Use `git push origin --delete <branch>`.")
end
else
vim.notify(string.format("[gitea.nvim] Failed to merge PR #%d (HTTP %s).", number, status or "?"), vim.log.levels.ERROR)
end
end,
close = function(args)
local number = tonumber(args[1])
if not number then
vim.notify("[gitea.nvim] Usage: :Gitea pr close <number>", vim.log.levels.ERROR)
return
end
local owner, repo = detect_owner_repo()
local res, status = api.close_pull_request(owner, repo, number)
if res then
vim.notify("[gitea.nvim] PR #"..number.." closed.")
else
vim.notify(string.format("[gitea.nvim] Failed to close PR (HTTP %s).", status or "?"), vim.log.levels.ERROR)
end
end,
reopen = function(args)
local number = tonumber(args[1])
if not number then
vim.notify("[gitea.nvim] Usage: :Gitea pr reopen <number>", vim.log.levels.ERROR)
return
end
local owner, repo = detect_owner_repo()
local res, status = api.reopen_pull_request(owner, repo, number)
if res then
vim.notify("[gitea.nvim] PR #"..number.." reopened.")
else
vim.notify(string.format("[gitea.nvim] Failed to reopen PR (HTTP %s).", status or "?"), vim.log.levels.ERROR)
end
end,
comment = function(args)
local number = tonumber(args[1])
if not number then
vim.notify("[gitea.nvim] Usage: :Gitea pr comment <number>", vim.log.levels.ERROR)
return
end
local owner, repo = detect_owner_repo()
local body = vim.fn.input("Comment: ")
if not body or body == "" then
return
end
local res, status = api.comment_pull_request(owner, repo, number, body)
if res then
vim.notify("[gitea.nvim] Comment added to PR #"..number..".")
else
vim.notify(string.format("[gitea.nvim] Failed to add comment (HTTP %s).", status or "?"), vim.log.levels.ERROR)
end
end,
-- We'll leave your existing PR commands as is or remove for brevity
}
-- Expand with other objects (e.g. 'repo', 'review', etc.) if needed.
-------------------------------
-- Command dispatcher
-------------------------------
--------------------------------------------------------------------------------
--- REGISTER
--------------------------------------------------------------------------------
function M.register()
vim.api.nvim_create_user_command("Gitea", function(cmd_opts)
-- Ensure token is loaded (in case user runs a command after startup)
local _ = require("gitea.auth").ensure_token()
auth.ensure_token()
local args = {}
for w in string.gmatch(cmd_opts.args, "%S+") do
table.insert(args, w)
end
local object = args[1] -- "issue", "pr", etc.
local action = args[2] -- "list", "create", etc.
local object = args[1]
local action = args[2]
if not object then
print("Usage: :Gitea <object> <action> [args]")
print("Examples:")
print(" :Gitea issue list")
print(" :Gitea pr list")
print(" :Gitea issue show 123")
return
end
if not subcommands[object] then
local sc = subcommands[object]
if not sc then
vim.notify("[gitea.nvim] Unknown object: "..object, vim.log.levels.ERROR)
return
end
if not action then
vim.notify("[gitea.nvim] Please specify an action for "..object, vim.log.levels.ERROR)
return
end
local fn = subcommands[object][action]
local fn = sc[action]
if not fn then
vim.notify(string.format("[gitea.nvim] Unknown action '%s' for object '%s'", action, object), vim.log.levels.ERROR)
return
end
-- remove first two tokens
table.remove(args, 1)
table.remove(args, 1)
table.remove(args, 1) -- remove object
table.remove(args, 1) -- remove action
fn(args)
end, {
nargs = "*",
complete = function(arg_lead, cmd_line, cursor_pos)
-- Very basic completion
local parts = vim.split(cmd_line, "%s+")
if #parts == 2 then
-- user typed: :Gitea <object-partial>
local objs = {}
for k, _ in pairs(subcommands) do
if k:match("^"..arg_lead) then
@@ -440,4 +433,4 @@ function M.register()
})
end
return M
return M