feat: first implementation for issues and pull requests
This commit is contained in:
@@ -1,3 +1,14 @@
|
|||||||
project_name: "gitea.nvim"
|
project_name: "gitea.nvim"
|
||||||
default_prompt_blocks:
|
default_prompt_blocks:
|
||||||
- "basic-prompt"
|
- "basic-prompt"
|
||||||
|
|
||||||
|
preview_changes: false
|
||||||
|
interactive_file_selection: false
|
||||||
|
partial_acceptance: false
|
||||||
|
improved_debug: true
|
||||||
|
|
||||||
|
# We rely on token_limit to decide chunk sizes
|
||||||
|
token_limit: 128000
|
||||||
|
|
||||||
|
# Enable chunking if we exceed the token limit
|
||||||
|
enable_chunking: true
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
" quit when a syntax file was already loaded
|
|
||||||
if exists("b:current_syntax")
|
|
||||||
finish
|
|
||||||
endif
|
|
||||||
|
|
||||||
if !exists('main_syntax')
|
|
||||||
let main_syntax = 'gitea'
|
|
||||||
endif
|
|
||||||
|
|
||||||
runtime! syntax/markdown.vim ftplugin/markdown.vim ftplugin/markdown_*.vim ftplugin/markdown/*.vim
|
|
||||||
unlet! b:current_syntax
|
|
||||||
|
|
||||||
" Emoji conceal, similar to octo.nvim but for 'gitea'
|
|
||||||
call matchadd('Conceal', ':heart:', 10, -1, {'conceal':'❤️'})
|
|
||||||
call matchadd('Conceal', ':+1:', 10, -1, {'conceal':'👍'})
|
|
||||||
call matchadd('Conceal', ':see_no_evil:', 10, -1, {'conceal':'🙈'})
|
|
||||||
call matchadd('Conceal', ':laughing:', 10, -1, {'conceal':'😆'})
|
|
||||||
call matchadd('Conceal', ':thinking_face:', 10, -1, {'conceal':'🤔'})
|
|
||||||
call matchadd('Conceal', ':thinking:', 10, -1, {'conceal':'🤔'})
|
|
||||||
call matchadd('Conceal', ':ok_hand:', 10, -1, {'conceal':'👌'})
|
|
||||||
call matchadd('Conceal', ':upside_down_face:', 10, -1, {'conceal':'🙃'})
|
|
||||||
call matchadd('Conceal', ':grimacing:', 10, -1, {'conceal':'😬'})
|
|
||||||
call matchadd('Conceal', ':rocket:', 10, -1, {'conceal':'🚀'})
|
|
||||||
call matchadd('Conceal', ':blush:', 10, -1, {'conceal':'😊'})
|
|
||||||
call matchadd('Conceal', ':tada:', 10, -1, {'conceal':'🎉'})
|
|
||||||
call matchadd('Conceal', ':shrug:', 10, -1, {'conceal':'🤷'})
|
|
||||||
call matchadd('Conceal', ':man_shrugging:', 10, -1, {'conceal':'🤷'})
|
|
||||||
call matchadd('Conceal', ':face_palm:', 10, -1, {'conceal':'🤦'})
|
|
||||||
call matchadd('Conceal', ':man_facepalmin:', 10, -1, {'conceal':'🤦'})
|
|
||||||
|
|
||||||
let b:current_syntax = "gitea"
|
|
||||||
if main_syntax ==# 'gitea'
|
|
||||||
unlet main_syntax
|
|
||||||
endif
|
|
||||||
57
doc/gitea.nvim.md
Normal file
57
doc/gitea.nvim.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# gitea.nvim
|
||||||
|
|
||||||
|
**gitea.nvim** allows you to manage [Gitea](https://gitea.io/) issues and pull requests from Neovim,
|
||||||
|
inspired by [octo.nvim](https://github.com/pwntester/octo.nvim).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Stores a Gitea token securely, optionally adding it to `.gitignore`.
|
||||||
|
- Tries to auto-detect the Gitea host from your `.git/config` or prompts for it.
|
||||||
|
- List, open, comment, and create issues.
|
||||||
|
- List, open, comment, and merge pull requests.
|
||||||
|
- Create a local Git branch for an issue.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Using [packer.nvim](https://github.com/wbthomason/packer.nvim):
|
||||||
|
|
||||||
|
```lua
|
||||||
|
use {
|
||||||
|
'youruser/gitea.nvim',
|
||||||
|
requires = { 'nvim-lua/plenary.nvim' },
|
||||||
|
config = function()
|
||||||
|
require('gitea').setup()
|
||||||
|
end
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```lua
|
||||||
|
require('gitea').setup({
|
||||||
|
base_url = "https://gitea.example.com",
|
||||||
|
token_file = vim.fn.stdpath('config') .. "/.gitea_token",
|
||||||
|
preview_style = "floating", -- or "split"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage (Commands)
|
||||||
|
|
||||||
|
- `:GiteaListIssues <owner> <repo>`
|
||||||
|
- `:GiteaOpenIssue <owner> <repo> <issue_number>`
|
||||||
|
- `:GiteaCreateIssue <owner> <repo> <title> <body...>`
|
||||||
|
- `:GiteaListComments <owner> <repo> <issue_number>`
|
||||||
|
- `:GiteaAddComment <owner> <repo> <issue_number> <comment...>`
|
||||||
|
- `:GiteaCreateBranchForIssue <owner> <repo> <issue_number>`
|
||||||
|
- `:GiteaListPRs <owner> <repo>`
|
||||||
|
- `:GiteaOpenPR <owner> <repo> <pr_number>`
|
||||||
|
- `:GiteaCommentPR <owner> <repo> <pr_number> <comment...>`
|
||||||
|
- `:GiteaMergePR <owner> <repo> <pr_number>`
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
- **Circular Dependency**: Previously, `commands.lua` required `init.lua` while `init.lua` required `commands.lua`.
|
||||||
|
The updated approach removes that loop by passing the plugin’s table to `commands.setup_commands`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
@@ -1,196 +1,64 @@
|
|||||||
local config = require("gitea.config")
|
local Job = require('plenary.job')
|
||||||
local auth = require("gitea.auth")
|
local api = {}
|
||||||
local curl = require("plenary.curl")
|
|
||||||
|
|
||||||
local M = {}
|
-- Helper function for making async requests to Gitea API via curl
|
||||||
|
local function request(method, base_url, endpoint, token, data, callback)
|
||||||
------------------------------------------------------------------------------
|
local headers = {
|
||||||
-- Helper: get base URL from config
|
"Content-Type: application/json",
|
||||||
------------------------------------------------------------------------------
|
"Authorization: token " .. token
|
||||||
local function get_base_url()
|
|
||||||
return config.values.server_url
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- Helper: get auth header by trimming token
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
local function get_auth_header()
|
|
||||||
local token = auth.get_token()
|
|
||||||
if not token or token == "" then
|
|
||||||
error("[gitea.nvim] Missing Gitea token.")
|
|
||||||
end
|
|
||||||
return "token " .. token
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- Main HTTP request helper (using plenary.curl)
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
local function request(method, endpoint, opts)
|
|
||||||
opts = opts or {}
|
|
||||||
local url = get_base_url() .. endpoint
|
|
||||||
local headers = opts.headers or {}
|
|
||||||
headers["Authorization"] = get_auth_header()
|
|
||||||
headers["Content-Type"] = "application/json"
|
|
||||||
|
|
||||||
local body_data
|
|
||||||
if opts.body then
|
|
||||||
body_data = vim.json.encode(opts.body)
|
|
||||||
end
|
|
||||||
|
|
||||||
local result = curl.request({
|
|
||||||
url = url,
|
|
||||||
method = method,
|
|
||||||
headers = headers,
|
|
||||||
timeout = 10000,
|
|
||||||
body = body_data,
|
|
||||||
query = opts.query,
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- Issues
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
function M.list_issues(owner, repo, opts)
|
|
||||||
local endpoint = string.format("/api/v1/repos/%s/%s/issues", owner, repo)
|
|
||||||
local result = request("GET", endpoint, { query = opts })
|
|
||||||
if result and result.status == 200 then
|
|
||||||
return vim.json.decode(result.body)
|
|
||||||
end
|
|
||||||
return nil, result and result.status
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.get_issue(owner, repo, number)
|
|
||||||
local endpoint = string.format("/api/v1/repos/%s/%s/issues/%d", owner, repo, number)
|
|
||||||
local result = request("GET", endpoint)
|
|
||||||
if result and result.status == 200 then
|
|
||||||
return vim.json.decode(result.body)
|
|
||||||
end
|
|
||||||
return nil, result and result.status
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.create_issue(owner, repo, data)
|
|
||||||
local endpoint = string.format("/api/v1/repos/%s/%s/issues", owner, repo)
|
|
||||||
local result = request("POST", endpoint, { body = data })
|
|
||||||
if result and result.status == 201 then
|
|
||||||
return vim.json.decode(result.body)
|
|
||||||
end
|
|
||||||
return nil, result and result.status
|
|
||||||
end
|
|
||||||
|
|
||||||
-- CHANGED: treat both 200 and 201 as success, in case Gitea returns 201
|
|
||||||
function M.edit_issue(owner, repo, number, data)
|
|
||||||
local endpoint = string.format("/api/v1/repos/%s/%s/issues/%d", owner, repo, number)
|
|
||||||
local result = request("PATCH", endpoint, { body = data })
|
|
||||||
if result and (result.status == 200 or result.status == 201) then
|
|
||||||
return vim.json.decode(result.body)
|
|
||||||
end
|
|
||||||
return nil, result and result.status
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.close_issue(owner, repo, number)
|
|
||||||
return M.edit_issue(owner, repo, number, { state = "closed" })
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.reopen_issue(owner, repo, number)
|
|
||||||
return M.edit_issue(owner, repo, number, { state = "open" })
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.comment_issue(owner, repo, number, body)
|
|
||||||
local endpoint = string.format("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, number)
|
|
||||||
local result = request("POST", endpoint, { body = { body = body } })
|
|
||||||
if result and result.status == 201 then
|
|
||||||
return vim.json.decode(result.body)
|
|
||||||
end
|
|
||||||
return nil, result and result.status
|
|
||||||
end
|
|
||||||
|
|
||||||
-- CHANGED: treat both 200 and 201 as success
|
|
||||||
function M.edit_issue_comment(owner, repo, number, comment_id, body)
|
|
||||||
local endpoint = string.format("/api/v1/repos/%s/%s/issues/comments/%d", owner, repo, comment_id)
|
|
||||||
local result = request("PATCH", endpoint, { body = { body = body } })
|
|
||||||
if result and (result.status == 200 or result.status == 201) then
|
|
||||||
return vim.json.decode(result.body)
|
|
||||||
end
|
|
||||||
return nil, result and result.status
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.get_issue_comments(owner, repo, number)
|
|
||||||
local endpoint = string.format("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, number)
|
|
||||||
local result = request("GET", endpoint)
|
|
||||||
if result and result.status == 200 then
|
|
||||||
return vim.json.decode(result.body)
|
|
||||||
end
|
|
||||||
return nil, result and result.status
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- Pull Requests
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
function M.list_pull_requests(owner, repo, opts)
|
|
||||||
local endpoint = string.format("/api/v1/repos/%s/%s/pulls", owner, repo)
|
|
||||||
local result = request("GET", endpoint, { query = opts })
|
|
||||||
if result and result.status == 200 then
|
|
||||||
return vim.json.decode(result.body)
|
|
||||||
end
|
|
||||||
return nil, result and result.status
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.get_pull_request(owner, repo, number)
|
|
||||||
local endpoint = string.format("/api/v1/repos/%s/%s/pulls/%d", owner, repo, number)
|
|
||||||
local result = request("GET", endpoint)
|
|
||||||
if result and result.status == 200 then
|
|
||||||
return vim.json.decode(result.body)
|
|
||||||
end
|
|
||||||
return nil, result and result.status
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.create_pull_request(owner, repo, data)
|
|
||||||
local endpoint = string.format("/api/v1/repos/%s/%s/pulls", owner, repo)
|
|
||||||
local result = request("POST", endpoint, { body = data })
|
|
||||||
if result and result.status == 201 then
|
|
||||||
return vim.json.decode(result.body)
|
|
||||||
end
|
|
||||||
return nil, result and result.status
|
|
||||||
end
|
|
||||||
|
|
||||||
-- CHANGED: treat both 200 and 201 as success for PR edits
|
|
||||||
function M.edit_pull_request(owner, repo, number, data)
|
|
||||||
local endpoint = string.format("/api/v1/repos/%s/%s/pulls/%d", owner, repo, number)
|
|
||||||
local result = request("PATCH", endpoint, { body = data })
|
|
||||||
if result and (result.status == 200 or result.status == 201) then
|
|
||||||
return vim.json.decode(result.body)
|
|
||||||
end
|
|
||||||
return nil, result and result.status
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.merge_pull_request(owner, repo, number, merge_style, merge_title, merge_message)
|
|
||||||
local endpoint = string.format("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, number)
|
|
||||||
local result = request("POST", endpoint, {
|
|
||||||
body = {
|
|
||||||
Do = merge_style or "merge",
|
|
||||||
MergeTitleField = merge_title or "",
|
|
||||||
MergeMessageField = merge_message or "",
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
if result and result.status == 200 then
|
local curl_args = {
|
||||||
return vim.json.decode(result.body)
|
"-s", -- silent mode
|
||||||
|
"-X", method, -- GET, POST, PATCH, etc.
|
||||||
|
base_url .. endpoint,
|
||||||
|
"-H", headers[1],
|
||||||
|
"-H", headers[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
if data then
|
||||||
|
local json_data = vim.fn.json_encode(data)
|
||||||
|
table.insert(curl_args, "-d")
|
||||||
|
table.insert(curl_args, json_data)
|
||||||
end
|
end
|
||||||
return nil, result and result.status
|
|
||||||
|
Job:new({
|
||||||
|
command = "curl",
|
||||||
|
args = curl_args,
|
||||||
|
on_exit = function(j, return_val)
|
||||||
|
if return_val == 0 then
|
||||||
|
local result = table.concat(j:result(), "\n")
|
||||||
|
local ok, decoded = pcall(vim.fn.json_decode, result)
|
||||||
|
if ok then
|
||||||
|
callback(decoded, nil)
|
||||||
|
else
|
||||||
|
callback(nil, "Failed to decode JSON. Response: " .. result)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
callback(nil, "Curl request failed with return code: " .. return_val)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}):start()
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.close_pull_request(owner, repo, number)
|
function api.get(base_url, endpoint, token, cb)
|
||||||
return M.edit_pull_request(owner, repo, number, { state = "closed" })
|
request("GET", base_url, endpoint, token, nil, cb)
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.reopen_pull_request(owner, repo, number)
|
function api.post(base_url, endpoint, token, data, cb)
|
||||||
return M.edit_pull_request(owner, repo, number, { state = "open" })
|
request("POST", base_url, endpoint, token, data, cb)
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.comment_pull_request(owner, repo, number, body)
|
function api.patch(base_url, endpoint, token, data, cb)
|
||||||
-- Uses same logic as comment_issue
|
request("PATCH", base_url, endpoint, token, data, cb)
|
||||||
return M.comment_issue(owner, repo, number, body)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
function api.put(base_url, endpoint, token, data, cb)
|
||||||
|
request("PUT", base_url, endpoint, token, data, cb)
|
||||||
|
end
|
||||||
|
|
||||||
|
function api.delete(base_url, endpoint, token, cb)
|
||||||
|
request("DELETE", base_url, endpoint, token, nil, cb)
|
||||||
|
end
|
||||||
|
|
||||||
|
return api
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
local config = require("gitea.config")
|
|
||||||
local uv = vim.loop
|
|
||||||
|
|
||||||
local M = {}
|
|
||||||
local token_cached = nil
|
|
||||||
|
|
||||||
-- Read token file from disk (if present)
|
|
||||||
local function read_token_file(path)
|
|
||||||
local fd = uv.fs_open(path, "r", 438)
|
|
||||||
if not fd then return nil end
|
|
||||||
local stat = uv.fs_fstat(fd)
|
|
||||||
local data = uv.fs_read(fd, stat.size, 0)
|
|
||||||
uv.fs_close(fd)
|
|
||||||
return data
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Write token file to disk with restricted permissions
|
|
||||||
local function write_token_file(path, token)
|
|
||||||
local fd = uv.fs_open(path, "w", 384) -- 0600 decimal
|
|
||||||
if not fd then
|
|
||||||
error("[gitea.nvim] Failed to open token file for writing: " .. path)
|
|
||||||
end
|
|
||||||
uv.fs_write(fd, token, -1)
|
|
||||||
uv.fs_close(fd)
|
|
||||||
end
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
--- Attempt to detect the Gitea server from remote.origin.url or fail
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
local function detect_gitea_server()
|
|
||||||
-- Use `git config remote.origin.url`
|
|
||||||
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, "No valid remote.origin.url found"
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Remove trailing ".git" if present
|
|
||||||
url = url:gsub("%.git$", "")
|
|
||||||
|
|
||||||
-- Check for HTTPS style: https://my.gitea.com/owner/repo
|
|
||||||
local protocol, domain_and_path = url:match("^(https?://)(.*)$")
|
|
||||||
if protocol and domain_and_path then
|
|
||||||
local server = domain_and_path:match("^([^/]+)")
|
|
||||||
if server and server ~= "" then
|
|
||||||
return protocol .. server
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Check for SSH style: git@my.gitea.com:owner/repo
|
|
||||||
local user_host, path = url:match("^(.-):(.*)$")
|
|
||||||
if user_host and user_host:match(".*@") then
|
|
||||||
local host = user_host:match("@(.*)")
|
|
||||||
if host and host ~= "" then
|
|
||||||
-- We'll assume https for Gitea, if your instance is not https,
|
|
||||||
-- you can adapt or forcibly use "http://" below
|
|
||||||
return "https://" .. host
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil, "Unable to parse Gitea host from remote.origin.url: " .. url
|
|
||||||
end
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
--- DO NOT PROMPT for server_url, do not fallback to "gitea.example.com"
|
|
||||||
--- If we cannot auto-detect, we throw an error.
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
local function ensure_server_url()
|
|
||||||
local server, err = detect_gitea_server()
|
|
||||||
if not server then
|
|
||||||
error(string.format("[gitea.nvim] Failed to auto-detect Gitea server: %s", err))
|
|
||||||
end
|
|
||||||
config.values.server_url = server
|
|
||||||
end
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
--- The user must have a Gitea token, we read from disk or error if missing
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
local function ensure_token_file()
|
|
||||||
local token_file = config.values.config_file
|
|
||||||
local token_data = read_token_file(token_file)
|
|
||||||
if token_data and token_data ~= "" then
|
|
||||||
-- Trim leading/trailing whitespace/newlines so we don't send a trailing "\n"
|
|
||||||
token_data = token_data:gsub("^%s+", ""):gsub("%s+$", "")
|
|
||||||
token_cached = token_data
|
|
||||||
return
|
|
||||||
end
|
|
||||||
error(
|
|
||||||
"[gitea.nvim] No token found in " .. token_file ..
|
|
||||||
". Please manually create and store your Gitea token there with mode 600."
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
--- Called by the plugin during setup or first usage
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
function M.ensure_token()
|
|
||||||
if token_cached then
|
|
||||||
return token_cached
|
|
||||||
end
|
|
||||||
|
|
||||||
ensure_server_url()
|
|
||||||
ensure_token_file()
|
|
||||||
return token_cached
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.get_token()
|
|
||||||
return token_cached
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
@@ -1,474 +1,128 @@
|
|||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- lua/gitea/commands.lua
|
||||||
|
--
|
||||||
|
-- Single :Gitea command with subcommands for issues, PRs, etc.
|
||||||
|
-- Removed "pr open" as requested. Only "list" and "create" remain for PR.
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local config = require("gitea.config")
|
--------------------------------------------------------------------------
|
||||||
local api = require("gitea.api")
|
-- A simple completion function for the :Gitea command
|
||||||
local auth = require("gitea.auth")
|
--------------------------------------------------------------------------
|
||||||
local highlights = require("gitea.highlights")
|
function M._gitea_cmd_complete(arg_lead, cmd_line, cursor_pos)
|
||||||
|
local tokens = vim.split(cmd_line, "%s+")
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
-- If the user hasn't typed anything after ":Gitea", suggest top-level words
|
||||||
-- Debug toggle
|
if #tokens <= 1 then
|
||||||
------------------------------------------------------------------------------
|
return { "issue", "pr" }
|
||||||
local DEBUG = true
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- We’ll create a namespace for our extmarks
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
local GITEA_NS = vim.api.nvim_create_namespace("gitea_nvim")
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- Utility: get all lines from the region defined by an extmark
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
local function get_extmark_region(bufnr, extmark_id)
|
|
||||||
-- extmark lookup with "details = true" returns: { row, col, details={ end_row=..., end_col=... } }
|
|
||||||
local markinfo = vim.api.nvim_buf_get_extmark_by_id(bufnr, GITEA_NS, extmark_id, { details = true })
|
|
||||||
if not markinfo or #markinfo == 0 then
|
|
||||||
return {}, 0, 0 -- no lines
|
|
||||||
end
|
|
||||||
local start_line = markinfo[1]
|
|
||||||
local end_line = markinfo[3].end_row
|
|
||||||
local lines = vim.api.nvim_buf_get_lines(bufnr, start_line, end_line + 1, false)
|
|
||||||
return lines, start_line, end_line
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- Utility: set an extmark covering a region from start_line..end_line
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
local function set_extmark_range(bufnr, start_line, end_line)
|
|
||||||
-- We store the extmark covering these lines from start_line..end_line
|
|
||||||
-- We do end_col=0 to anchor at column 0, but the end_row is inclusive
|
|
||||||
-- so we do { end_line, -1 } or end_col=0 with end_row=that line + 1
|
|
||||||
local extmark_id = vim.api.nvim_buf_set_extmark(bufnr, GITEA_NS, start_line, 0, {
|
|
||||||
end_line = end_line,
|
|
||||||
end_col = 0,
|
|
||||||
})
|
|
||||||
return extmark_id
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- DETECT OWNER/REPO
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
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
|
|
||||||
url = url:gsub("%.git$", "")
|
|
||||||
local _, after = url:match("^(https?://)(.*)$")
|
|
||||||
if after then
|
|
||||||
local path = after:match("[^/]+/(.*)$")
|
|
||||||
if not path then
|
|
||||||
return nil, nil
|
|
||||||
end
|
|
||||||
local owner, repo = path:match("^(.-)/(.*)$")
|
|
||||||
return owner, repo
|
|
||||||
end
|
|
||||||
local user_host, path = url:match("^(.-):(.*)$")
|
|
||||||
if user_host and path then
|
|
||||||
local owner, repo = path:match("^(.-)/(.*)$")
|
|
||||||
return owner, repo
|
|
||||||
end
|
|
||||||
return nil, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- We’ll store "metadata" in a table like octo.nvim’s approach:
|
|
||||||
-- metadata.title_extmark -> ID
|
|
||||||
-- metadata.body_extmark -> ID
|
|
||||||
-- metadata.comments = { { id=..., extmark_id=... }, ... }
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
local function create_issue_buffer_metadata(issue)
|
|
||||||
return {
|
|
||||||
issue_number = issue.number,
|
|
||||||
title_extmark = nil,
|
|
||||||
body_extmark = nil,
|
|
||||||
comments = {},
|
|
||||||
new_comment_extmark = nil, -- region for new comment
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- APPLY CUSTOM HIGHLIGHTS
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
local function apply_issue_highlights(bufnr)
|
|
||||||
-- We can still highlight lines #0 and #1, or do it differently
|
|
||||||
vim.api.nvim_buf_add_highlight(bufnr, 0, "GiteaIssueMeta", 0, 0, -1)
|
|
||||||
vim.api.nvim_buf_add_highlight(bufnr, 0, "GiteaIssueMeta", 1, 0, -1)
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- RENDER ISSUE INTO BUFFER (octo.nvim style)
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
local function render_issue_into_buf(bufnr, issue, comments)
|
|
||||||
-- Clear buffer, extmarks, etc.
|
|
||||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {})
|
|
||||||
vim.api.nvim_buf_clear_namespace(bufnr, GITEA_NS, 0, -1)
|
|
||||||
|
|
||||||
local meta = create_issue_buffer_metadata(issue)
|
|
||||||
local lines = {}
|
|
||||||
|
|
||||||
-- Some short header 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, "# You can edit below. On save, the changes are applied.")
|
|
||||||
table.insert(lines, "")
|
|
||||||
|
|
||||||
-- Add placeholder for title
|
|
||||||
local title_start = #lines
|
|
||||||
table.insert(lines, issue.title or "")
|
|
||||||
local title_end = #lines
|
|
||||||
|
|
||||||
-- blank line
|
|
||||||
table.insert(lines, "")
|
|
||||||
|
|
||||||
-- Add placeholder for body
|
|
||||||
local body_start = #lines
|
|
||||||
local body_text = issue.body or ""
|
|
||||||
if body_text == "" then
|
|
||||||
body_text = "No issue description."
|
|
||||||
end
|
|
||||||
local body_lines = vim.split(body_text, "\n", true)
|
|
||||||
if #body_lines == 0 then
|
|
||||||
table.insert(lines, "")
|
|
||||||
else
|
|
||||||
for _, line in ipairs(body_lines) do
|
|
||||||
table.insert(lines, line)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local body_end = #lines
|
|
||||||
|
|
||||||
-- blank line
|
|
||||||
table.insert(lines, "")
|
|
||||||
table.insert(lines, "# --- Comments ---")
|
|
||||||
|
|
||||||
local comment_meta = {}
|
|
||||||
for _, c in ipairs(comments or {}) do
|
|
||||||
table.insert(lines, "")
|
|
||||||
local start_line = #lines
|
|
||||||
local author = c.user and (c.user.login or c.user.username) or "?"
|
|
||||||
table.insert(lines, string.format("COMMENT #%d by %s", c.id, author))
|
|
||||||
|
|
||||||
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, extmark_id = nil, start_line = start_line, end_line = end_line })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- final blank line for new comment
|
local main_sub = tokens[2]
|
||||||
table.insert(lines, "")
|
|
||||||
table.insert(lines, "# --- Add a new comment below (multiline ok). ---")
|
|
||||||
local new_comment_start = #lines + 1
|
|
||||||
table.insert(lines, "") -- at least one blank line
|
|
||||||
|
|
||||||
-- Actually write out
|
if #tokens == 2 then
|
||||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
local candidates = { "issue", "pr" }
|
||||||
|
local results = {}
|
||||||
-- Now create extmark for the title
|
for _, c in ipairs(candidates) do
|
||||||
-- NOTE: extmark is zero-based, so set_extmark_range expects 0-based line indexes
|
if c:find("^" .. arg_lead) then
|
||||||
meta.title_extmark = set_extmark_range(bufnr, title_start - 1, title_end - 1)
|
table.insert(results, c)
|
||||||
|
|
||||||
-- extmark for body
|
|
||||||
meta.body_extmark = set_extmark_range(bufnr, body_start - 1, body_end - 1)
|
|
||||||
|
|
||||||
-- extmarks for comments
|
|
||||||
for _, cmeta in ipairs(comment_meta) do
|
|
||||||
local mark_id = set_extmark_range(bufnr, cmeta.start_line - 1, cmeta.end_line - 1)
|
|
||||||
cmeta.extmark_id = mark_id
|
|
||||||
end
|
end
|
||||||
meta.comments = comment_meta
|
end
|
||||||
|
return results
|
||||||
-- extmark for new comment region
|
|
||||||
meta.new_comment_extmark = set_extmark_range(bufnr, new_comment_start - 1, #lines - 1)
|
|
||||||
|
|
||||||
apply_issue_highlights(bufnr)
|
|
||||||
return meta
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- ON SAVE => PARSE & UPDATE (octo.nvim style)
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
local function on_issue_buf_write(bufnr, metadata, owner, repo)
|
|
||||||
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", vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 1) Gather new title
|
-- If "issue" was selected, next subcommands can be list, create
|
||||||
local title_lines = get_extmark_region(bufnr, metadata.title_extmark)
|
if main_sub == "issue" then
|
||||||
local new_title = table.concat(title_lines, "\n")
|
if #tokens == 3 then
|
||||||
-- Trim whitespace from the title (since GitHub disallows multi-line titles)
|
local candidates = { "list", "create" }
|
||||||
new_title = new_title:gsub("^%s+", ""):gsub("%s+$", ""):gsub("[\n\r]+", " ")
|
local results = {}
|
||||||
|
for _, c in ipairs(candidates) do
|
||||||
-- 2) Gather new body
|
if c:find("^" .. arg_lead) then
|
||||||
local body_lines = get_extmark_region(bufnr, metadata.body_extmark)
|
table.insert(results, c)
|
||||||
local new_body = table.concat(body_lines, "\n") -- multiline is fine, do not trim
|
|
||||||
|
|
||||||
if DEBUG then
|
|
||||||
print("DEBUG: new_title = ", new_title)
|
|
||||||
print("DEBUG: new_body (lines) = ", vim.inspect(body_lines))
|
|
||||||
end
|
|
||||||
|
|
||||||
local changed_title = (current_issue_data.title or "") ~= new_title
|
|
||||||
local changed_body = (current_issue_data.body or "") ~= new_body
|
|
||||||
|
|
||||||
-- 3) Gather updates for existing comments
|
|
||||||
local changed_comments = {}
|
|
||||||
for _, cinfo in ipairs(metadata.comments) do
|
|
||||||
local comment_lines = get_extmark_region(bufnr, cinfo.extmark_id)
|
|
||||||
-- The first line might be "COMMENT #ID by Author"
|
|
||||||
-- So let's separate that from the actual text
|
|
||||||
local c_header = comment_lines[1] or ""
|
|
||||||
local c_body_lines = {}
|
|
||||||
for i = 2, #comment_lines do
|
|
||||||
table.insert(c_body_lines, comment_lines[i])
|
|
||||||
end
|
|
||||||
local new_comment_body = table.concat(c_body_lines, "\n")
|
|
||||||
|
|
||||||
table.insert(changed_comments, {
|
|
||||||
id = cinfo.id,
|
|
||||||
new_body = new_comment_body,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 4) Possibly new comment
|
|
||||||
local extra_comment_lines = get_extmark_region(bufnr, metadata.new_comment_extmark)
|
|
||||||
-- remove lines that are just comment-hint if you want, but we can accept them all
|
|
||||||
local new_comment_body = table.concat(extra_comment_lines, "\n"):gsub("^%s+", ""):gsub("%s+$", "")
|
|
||||||
local create_new_comment = #new_comment_body > 0
|
|
||||||
|
|
||||||
if DEBUG then
|
|
||||||
print("DEBUG: new_comment_body = ", new_comment_body)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Validate title
|
|
||||||
if changed_title and new_title == "" then
|
|
||||||
vim.notify("[gitea.nvim] Title cannot be empty. Skipping.", vim.log.levels.ERROR)
|
|
||||||
if not changed_body and #changed_comments == 0 and not create_new_comment then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
changed_title = false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Edit issue if needed
|
|
||||||
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
|
||||||
end
|
end
|
||||||
|
return results
|
||||||
-- Update each existing comment
|
|
||||||
for _, c in ipairs(changed_comments) do
|
|
||||||
if (c.new_body or "") ~= "" then
|
|
||||||
local updated_comment, st = api.edit_issue_comment(owner, repo, metadata.issue_number, c.id, c.new_body)
|
|
||||||
if not updated_comment then
|
|
||||||
vim.notify(string.format("[gitea.nvim] Failed to update comment %d", c.id), vim.log.levels.ERROR)
|
|
||||||
else
|
|
||||||
vim.notify(string.format("[gitea.nvim] Comment #%d updated.", c.id))
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Possibly create new comment
|
|
||||||
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 comment (HTTP %s).", st or "?"), vim.log.levels.ERROR)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- OPEN ISSUE BUFFER
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
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
|
|
||||||
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", "gitea")
|
|
||||||
vim.api.nvim_buf_set_name(buf, string.format("gitea_issue_full_%d", number))
|
|
||||||
|
|
||||||
highlights.setup()
|
|
||||||
|
|
||||||
-- Render lines & create extmarks
|
|
||||||
local metadata = render_issue_into_buf(buf, issue_data, all_comments)
|
|
||||||
|
|
||||||
-- Hook saving => parse extmarks & 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 = {
|
|
||||||
list = function(_)
|
|
||||||
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 has_telescope, _ = 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, st = api.list_issues(owner, repo, {})
|
|
||||||
if not issues then
|
|
||||||
vim.notify(string.format("[gitea.nvim] Error fetching issues (HTTP %s).", st or "?"), vim.log.levels.ERROR)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
},
|
|
||||||
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
|
|
||||||
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,
|
|
||||||
|
|
||||||
show = function(args)
|
|
||||||
local number = tonumber(args[1])
|
|
||||||
if not number then
|
|
||||||
vim.notify("[gitea.nvim] Usage: :Gitea issue show <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
|
|
||||||
open_full_issue_buffer(owner, repo, number)
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
subcommands.pr = {
|
|
||||||
-- If you have PR subcommands, add them here
|
|
||||||
}
|
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
-- REGISTER
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
function M.register()
|
|
||||||
vim.api.nvim_create_user_command("Gitea", function(cmd_opts)
|
|
||||||
auth.ensure_token()
|
|
||||||
local args = {}
|
|
||||||
for w in string.gmatch(cmd_opts.args, "%S+") do
|
|
||||||
table.insert(args, w)
|
|
||||||
end
|
|
||||||
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 issue show 123")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
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 = 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
|
|
||||||
table.remove(args, 1)
|
|
||||||
table.remove(args, 1)
|
|
||||||
fn(args)
|
|
||||||
end, {
|
|
||||||
nargs = "*",
|
|
||||||
complete = function(arg_lead, cmd_line, _)
|
|
||||||
local parts = vim.split(cmd_line, "%s+")
|
|
||||||
if #parts == 2 then
|
|
||||||
local objs = {}
|
|
||||||
for k, _ in pairs(subcommands) do
|
|
||||||
if k:match("^" .. arg_lead) then
|
|
||||||
table.insert(objs, k)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return objs
|
|
||||||
elseif #parts == 3 then
|
|
||||||
local object = parts[2]
|
|
||||||
local acts = {}
|
|
||||||
if subcommands[object] then
|
|
||||||
for k, _ in pairs(subcommands[object]) do
|
|
||||||
if k:match("^" .. arg_lead) then
|
|
||||||
table.insert(acts, k)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return acts
|
|
||||||
else
|
|
||||||
return {}
|
return {}
|
||||||
end
|
end
|
||||||
end,
|
|
||||||
|
-- If "pr" was selected, next subcommands can be list, create
|
||||||
|
-- (removed "open" and "merge" as subcommands)
|
||||||
|
if main_sub == "pr" then
|
||||||
|
if #tokens == 3 then
|
||||||
|
local candidates = { "list", "create" }
|
||||||
|
local results = {}
|
||||||
|
for _, c in ipairs(candidates) do
|
||||||
|
if c:find("^" .. arg_lead) then
|
||||||
|
table.insert(results, c)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return results
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.setup_commands(core)
|
||||||
|
local issues_mod = require("gitea.issues")
|
||||||
|
local telescope_mod = require("gitea.telescope")
|
||||||
|
-- local pulls_mod = require("gitea.pulls") -- if needed
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command("Gitea", function(opts)
|
||||||
|
local args = vim.split(opts.args, "%s+")
|
||||||
|
local main = args[1] or ""
|
||||||
|
|
||||||
|
if main == "issue" then
|
||||||
|
local sub = args[2] or ""
|
||||||
|
if sub == "list" then
|
||||||
|
-- :Gitea issue list
|
||||||
|
telescope_mod.list_issues_in_telescope()
|
||||||
|
|
||||||
|
elseif sub == "create" then
|
||||||
|
-- :Gitea issue create [owner] [repo] <title> ...
|
||||||
|
if #args < 3 then
|
||||||
|
vim.cmd([[echoerr "Usage: :Gitea issue create <title> [body...]"]])
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local owner, repo, title, body
|
||||||
|
if #args >= 5 then
|
||||||
|
owner = args[3]
|
||||||
|
repo = args[4]
|
||||||
|
title = args[5]
|
||||||
|
body = table.concat(vim.list_slice(args, 6), " ")
|
||||||
|
else
|
||||||
|
title = args[3]
|
||||||
|
body = table.concat(vim.list_slice(args, 4), " ")
|
||||||
|
end
|
||||||
|
issues_mod.create_issue(owner, repo, title, body)
|
||||||
|
|
||||||
|
else
|
||||||
|
print("Unknown issue subcommand: " .. sub)
|
||||||
|
print("Available subcommands: list, create")
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif main == "pr" then
|
||||||
|
local sub = args[2] or ""
|
||||||
|
if sub == "list" then
|
||||||
|
vim.cmd([[echo "TODO: :Gitea pr list"]])
|
||||||
|
elseif sub == "create" then
|
||||||
|
vim.cmd([[echo "TODO: :Gitea pr create"]])
|
||||||
|
else
|
||||||
|
print("Unknown PR subcommand: " .. sub)
|
||||||
|
print("Available subcommands: list, create")
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
print("Usage: :Gitea <issue|pr> <subcommand> [arguments...]")
|
||||||
|
print("For issues: list, create")
|
||||||
|
print("For PRs: list, create")
|
||||||
|
end
|
||||||
|
end, {
|
||||||
|
nargs = "*",
|
||||||
|
desc = "Unified Gitea command with subcommands",
|
||||||
|
complete = M._gitea_cmd_complete,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,9 @@
|
|||||||
local Config = {}
|
local M = {}
|
||||||
|
|
||||||
-- Default settings (user can override in setup{})
|
M.default_config = {
|
||||||
local defaults = {
|
base_url = nil, -- The plugin attempts to detect from .git config or prompt if not set
|
||||||
config_file = ".gitea_nvim_token", -- local file containing the token
|
token_file = vim.fn.stdpath('config') .. '/.gitea_token',
|
||||||
ignore_file = ".gitignore", -- file to which we can append the token file
|
preview_style = "floating", -- or "split"
|
||||||
server_url = nil, -- extracted from .git remote if possible
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.values = {}
|
return M
|
||||||
|
|
||||||
function Config.setup(user_opts)
|
|
||||||
for k, v in pairs(defaults) do
|
|
||||||
if user_opts[k] ~= nil then
|
|
||||||
Config.values[k] = user_opts[k]
|
|
||||||
else
|
|
||||||
Config.values[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Config
|
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
local M = {}
|
|
||||||
|
|
||||||
-- A small Dracula-like palette, adapt or extend if needed
|
|
||||||
local palette = {
|
|
||||||
bg = "#282A36",
|
|
||||||
fg = "#F8F8F2",
|
|
||||||
selection = "#44475A",
|
|
||||||
comment = "#6272A4",
|
|
||||||
cyan = "#8BE9FD",
|
|
||||||
green = "#50FA7B",
|
|
||||||
orange = "#FFB86C",
|
|
||||||
pink = "#FF79C6",
|
|
||||||
purple = "#BD93F9",
|
|
||||||
red = "#FF5555",
|
|
||||||
yellow = "#F1FA8C",
|
|
||||||
}
|
|
||||||
|
|
||||||
function M.setup()
|
|
||||||
-- Title of an issue or PR
|
|
||||||
vim.api.nvim_set_hl(0, "GiteaIssueTitle", {
|
|
||||||
fg = palette.pink,
|
|
||||||
bold = true,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Meta lines, e.g. status lines or extra info
|
|
||||||
vim.api.nvim_set_hl(0, "GiteaIssueMeta", {
|
|
||||||
fg = palette.comment,
|
|
||||||
italic = true,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Heading for each comment block
|
|
||||||
vim.api.nvim_set_hl(0, "GiteaCommentHeading", {
|
|
||||||
fg = palette.orange,
|
|
||||||
bold = true,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- The actual comment body text
|
|
||||||
vim.api.nvim_set_hl(0, "GiteaCommentBody", {
|
|
||||||
fg = palette.fg,
|
|
||||||
bg = nil, -- or palette.bg if you want a background
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Something for a user mention
|
|
||||||
vim.api.nvim_set_hl(0, "GiteaUser", {
|
|
||||||
fg = palette.cyan,
|
|
||||||
bold = true,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- If you'd like comment lines in a faint color
|
|
||||||
vim.api.nvim_set_hl(0, "GiteaInlineComment", {
|
|
||||||
fg = palette.comment,
|
|
||||||
italic = true,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- ...Add or tweak any other highlight groups you need...
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
@@ -1,21 +1,243 @@
|
|||||||
local M = {}
|
----------------------------------------------------------------------------
|
||||||
local config = require("gitea.config")
|
-- lua/gitea/init.lua
|
||||||
|
--
|
||||||
|
-- Core plugin logic: reading .git/config, handling tokens, domain detection,
|
||||||
|
-- and an async request() function. Also has the top-level setup().
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local Job = require("plenary.job")
|
||||||
local commands = require("gitea.commands")
|
local commands = require("gitea.commands")
|
||||||
local auth = require("gitea.auth")
|
|
||||||
local highlights = require("gitea.highlights")
|
|
||||||
|
|
||||||
function M.setup(user_opts)
|
local M = {}
|
||||||
-- Load user config (if any), or defaults
|
|
||||||
config.setup(user_opts or {})
|
|
||||||
|
|
||||||
-- Ensure token is loaded
|
----------------------------------------------------------------------------
|
||||||
auth.ensure_token()
|
-- Configuration / State
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
M.token_folder = vim.fn.stdpath("data") .. "/gitea_tokens"
|
||||||
|
M.preview_style = "floating"
|
||||||
|
M._cached_base_url = nil
|
||||||
|
M._cached_token = nil
|
||||||
|
M._cached_owner = nil
|
||||||
|
M._cached_repo = nil
|
||||||
|
|
||||||
-- Register :Gitea commands
|
----------------------------------------------------------------------------
|
||||||
commands.register()
|
-- Internal Utils
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local function ensure_dir(dirpath)
|
||||||
|
if vim.fn.isdirectory(dirpath) == 0 then
|
||||||
|
vim.fn.mkdir(dirpath, "p")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Apply the default Dracula-like highlights
|
local function read_git_config_lines()
|
||||||
highlights.setup()
|
local gitconfig_path = vim.fn.getcwd() .. "/.git/config"
|
||||||
|
local f = io.open(gitconfig_path, "r")
|
||||||
|
if not f then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local lines = {}
|
||||||
|
for line in f:lines() do
|
||||||
|
table.insert(lines, line)
|
||||||
|
end
|
||||||
|
f:close()
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
local function strip_git_suffix(str)
|
||||||
|
return (str:gsub("%.git$", ""))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_git_config_for_domain()
|
||||||
|
local lines = read_git_config_lines()
|
||||||
|
for _, line in ipairs(lines) do
|
||||||
|
local url = line:match("^%s*url%s*=%s*(.+)")
|
||||||
|
if url then
|
||||||
|
-- SSH style: user@host:owner/repo.git
|
||||||
|
local ssh_host = url:match("^[^@]+@([^:]+):")
|
||||||
|
if ssh_host then
|
||||||
|
return ssh_host
|
||||||
|
end
|
||||||
|
-- HTTPS style: https://host/owner/repo(.git)
|
||||||
|
local https_host = url:match("^https?://([^/]+)")
|
||||||
|
if https_host then
|
||||||
|
return https_host
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.parse_owner_repo()
|
||||||
|
local lines = read_git_config_lines()
|
||||||
|
for _, line in ipairs(lines) do
|
||||||
|
local url = line:match("^%s*url%s*=%s*(.+)")
|
||||||
|
if url then
|
||||||
|
-- SSH
|
||||||
|
local ssh_host, ssh_path = url:match("^[^@]+@([^:]+):(.+)")
|
||||||
|
if ssh_host and ssh_path then
|
||||||
|
ssh_path = strip_git_suffix(ssh_path)
|
||||||
|
local slash_idx = ssh_path:find("/")
|
||||||
|
if slash_idx then
|
||||||
|
return ssh_path:sub(1, slash_idx - 1),
|
||||||
|
ssh_path:sub(slash_idx + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- HTTPS
|
||||||
|
local https_path = url:match("^https?://[^/]+/(.+)")
|
||||||
|
if https_path then
|
||||||
|
https_path = strip_git_suffix(https_path)
|
||||||
|
local slash_idx = https_path:find("/")
|
||||||
|
if slash_idx then
|
||||||
|
return https_path:sub(1, slash_idx - 1),
|
||||||
|
https_path:sub(slash_idx + 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- Token Logic
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local function token_file_for_domain(domain)
|
||||||
|
return M.token_folder .. "/" .. domain .. ".token"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function load_token(domain)
|
||||||
|
if not domain or domain == "" then
|
||||||
|
domain = "default"
|
||||||
|
end
|
||||||
|
local token_path = token_file_for_domain(domain)
|
||||||
|
local f = io.open(token_path, "r")
|
||||||
|
if f then
|
||||||
|
local token = f:read("*a")
|
||||||
|
f:close()
|
||||||
|
if token and token ~= "" then
|
||||||
|
return token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd(('echo "No Gitea token found for %s"'):format(domain))
|
||||||
|
end)
|
||||||
|
|
||||||
|
local user_input = vim.fn.inputsecret("Enter your Gitea token for " .. domain .. ": ")
|
||||||
|
if not (user_input and user_input ~= "") then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
ensure_dir(M.token_folder)
|
||||||
|
local fh, err = io.open(token_path, "w")
|
||||||
|
if not fh then
|
||||||
|
error(string.format("[gitea.nvim] Could not open token file for writing (%s). Error: %s",
|
||||||
|
token_path, err or "unknown"))
|
||||||
|
end
|
||||||
|
fh:write(user_input)
|
||||||
|
fh:close()
|
||||||
|
print(string.format("[gitea.nvim] Token written to %s", token_path))
|
||||||
|
return user_input
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensure_base_url()
|
||||||
|
if M._cached_base_url and M._cached_base_url ~= "" then
|
||||||
|
return M._cached_base_url
|
||||||
|
end
|
||||||
|
local domain = parse_git_config_for_domain()
|
||||||
|
if not domain then
|
||||||
|
error("[gitea.nvim] Could not parse a domain from .git/config.")
|
||||||
|
end
|
||||||
|
if not domain:match("^https?://") then
|
||||||
|
domain = "https://" .. domain
|
||||||
|
end
|
||||||
|
M._cached_base_url = domain
|
||||||
|
return domain
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ensure_token()
|
||||||
|
if M._cached_token and M._cached_token ~= "" then
|
||||||
|
return M._cached_token
|
||||||
|
end
|
||||||
|
local raw_domain = parse_git_config_for_domain() or "default"
|
||||||
|
raw_domain = raw_domain:gsub("^https://", "")
|
||||||
|
raw_domain = raw_domain:gsub("^http://", "")
|
||||||
|
local tok = load_token(raw_domain)
|
||||||
|
if not tok or tok == "" then
|
||||||
|
error("[gitea.nvim] Could not load/create a token for domain '" .. raw_domain .. "'.")
|
||||||
|
end
|
||||||
|
M._cached_token = tok
|
||||||
|
return tok
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- Async Requests (Plenary)
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local function do_request(method, endpoint, data, callback)
|
||||||
|
local base_url = ensure_base_url()
|
||||||
|
local token = ensure_token()
|
||||||
|
|
||||||
|
local headers = {
|
||||||
|
"Content-Type: application/json",
|
||||||
|
"Authorization: token " .. token
|
||||||
|
}
|
||||||
|
local args = { "-s", "-X", method, base_url .. endpoint, "-H", headers[1], "-H", headers[2] }
|
||||||
|
if data then
|
||||||
|
local json_data = vim.fn.json_encode(data)
|
||||||
|
table.insert(args, "-d")
|
||||||
|
table.insert(args, json_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
Job:new({
|
||||||
|
command = "curl",
|
||||||
|
args = args,
|
||||||
|
on_exit = function(j, return_val)
|
||||||
|
vim.schedule(function()
|
||||||
|
if return_val == 0 then
|
||||||
|
local result = table.concat(j:result(), "\n")
|
||||||
|
local ok, decoded = pcall(vim.fn.json_decode, result)
|
||||||
|
if ok then
|
||||||
|
callback(decoded, nil)
|
||||||
|
else
|
||||||
|
callback(nil, "Failed to parse JSON: " .. result)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
callback(nil, "curl failed (code=" .. return_val .. ")")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
}):start()
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- Expose to other modules
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
M.ensure_base_url = ensure_base_url
|
||||||
|
M.ensure_token = ensure_token
|
||||||
|
M.request = do_request
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- setup()
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
function M.setup(opts)
|
||||||
|
if opts and opts.token_folder then
|
||||||
|
M.token_folder = opts.token_folder
|
||||||
|
end
|
||||||
|
if opts and opts.preview_style then
|
||||||
|
M.preview_style = opts.preview_style
|
||||||
|
end
|
||||||
|
|
||||||
|
local domain = parse_git_config_for_domain()
|
||||||
|
if domain and not domain:match("^https?://") then
|
||||||
|
domain = "https://" .. domain
|
||||||
|
end
|
||||||
|
M._cached_base_url = domain
|
||||||
|
|
||||||
|
local o, r = M.parse_owner_repo()
|
||||||
|
M._cached_owner = o
|
||||||
|
M._cached_repo = r
|
||||||
|
|
||||||
|
commands.setup_commands(M)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
472
lua/gitea/issues.lua
Normal file
472
lua/gitea/issues.lua
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- lua/gitea/issues.lua
|
||||||
|
--
|
||||||
|
-- Manages Gitea issues and an associated YAML buffer interface.
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
local core = require("gitea")
|
||||||
|
local Job = require("plenary.job")
|
||||||
|
local lyaml = require("lyaml") -- ensure `luarocks install lyaml`
|
||||||
|
local pulls = require("gitea.pulls")
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- fallback_owner_repo
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local function fallback_owner_repo(owner, repo)
|
||||||
|
if (not owner or owner == "") or (not repo or repo == "") then
|
||||||
|
owner = core._cached_owner
|
||||||
|
repo = core._cached_repo
|
||||||
|
end
|
||||||
|
if not owner or not repo then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd([[echoerr "No owner/repo provided and none found in .git/config"]])
|
||||||
|
end)
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
return owner, repo
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- "Slugify" a title for branch usage
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local function slugify_title(title)
|
||||||
|
-- Lowercase the title, replace whitespace with dashes, remove invalid chars,
|
||||||
|
-- and truncate to 50 chars just to be safe
|
||||||
|
local slug = title:lower()
|
||||||
|
:gsub("%s+", "-")
|
||||||
|
:gsub("[^a-z0-9%-]", "")
|
||||||
|
:sub(1, 50)
|
||||||
|
return slug
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- Buffer Setup
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local function set_issue_buffer_options(buf)
|
||||||
|
vim.api.nvim_buf_set_option(buf, "buftype", "acwrite") -- allows :w
|
||||||
|
vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- attach_issue_autocmds
|
||||||
|
-- On save, parse entire YAML doc, update issue, post new comments.
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local function attach_issue_autocmds(buf)
|
||||||
|
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||||
|
buffer = buf,
|
||||||
|
callback = function()
|
||||||
|
local owner = vim.api.nvim_buf_get_var(buf, "gitea_issue_owner")
|
||||||
|
local repo = vim.api.nvim_buf_get_var(buf, "gitea_issue_repo")
|
||||||
|
local number = vim.api.nvim_buf_get_var(buf, "gitea_issue_number")
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||||
|
local doc_str = table.concat(lines, "\n")
|
||||||
|
|
||||||
|
-- parse as YAML with lyaml
|
||||||
|
local ok, docs = pcall(lyaml.load, doc_str)
|
||||||
|
if not ok or type(docs) ~= "table" then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd([[echoerr "Failed to parse YAML with lyaml!"]])
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- We assume docs = { issue = { ... }, comments = { ... } }
|
||||||
|
local issue = docs.issue or {}
|
||||||
|
local cmts = docs.comments or {}
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- PATCH the issue (title/body)
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local patch_data = {}
|
||||||
|
if type(issue.title) == "string" and issue.title ~= "" then
|
||||||
|
patch_data.title = issue.title
|
||||||
|
end
|
||||||
|
if type(issue.body) == "string" then
|
||||||
|
patch_data.body = issue.body
|
||||||
|
end
|
||||||
|
|
||||||
|
local issue_ep = string.format("/api/v1/repos/%s/%s/issues/%d", owner, repo, number)
|
||||||
|
core.request("PATCH", issue_ep, patch_data, function(_, err)
|
||||||
|
if err then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd(('echoerr "Failed to update issue: %s"'):format(err))
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
print("[gitea.nvim] Issue (title/body) updated.")
|
||||||
|
end)
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- POST new comments => those lacking 'id'
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
if type(cmts) == "table" then
|
||||||
|
for _, citem in ipairs(cmts) do
|
||||||
|
local cid = citem.id
|
||||||
|
if not cid then
|
||||||
|
local cbody = citem.body or ""
|
||||||
|
if #cbody > 0 then
|
||||||
|
local c_ep = string.format("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, number)
|
||||||
|
core.request("POST", c_ep, { body = cbody }, function(resp, c_err)
|
||||||
|
if c_err then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd(('echoerr "Failed to add comment: %s"'):format(c_err))
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
print("[gitea.nvim] New comment posted => " .. (resp.body or ""))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- M.list_issues
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local function list_issues(owner, repo, on_done)
|
||||||
|
owner, repo = fallback_owner_repo(owner, repo)
|
||||||
|
if not owner or not repo then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local ep = string.format("/api/v1/repos/%s/%s/issues", owner, repo)
|
||||||
|
core.request("GET", ep, nil, function(data, err)
|
||||||
|
if err then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd(('echoerr "Failed to list issues: %s"'):format(err))
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if on_done then
|
||||||
|
on_done(data)
|
||||||
|
else
|
||||||
|
print(string.format("Got %d issues for %s/%s", #data, owner, repo))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- M.open_issue
|
||||||
|
-- Reversed comment display => newest on top
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local function open_issue(owner, repo, number)
|
||||||
|
owner, repo = fallback_owner_repo(owner, repo)
|
||||||
|
if not owner or not repo or not number then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd([[echoerr "Usage: :Gitea issue open <owner> <repo> <number>"]])
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local issue_ep = string.format("/api/v1/repos/%s/%s/issues/%d", owner, repo, number)
|
||||||
|
core.request("GET", issue_ep, nil, function(issue, err)
|
||||||
|
if err then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd(('echoerr "Failed to open issue: %s"'):format(err))
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local comments_ep = string.format("/api/v1/repos/%s/%s/issues/%d/comments", owner, repo, number)
|
||||||
|
core.request("GET", comments_ep, nil, function(comments, c_err)
|
||||||
|
if c_err then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd(('echoerr "Failed to load comments: %s"'):format(c_err))
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.schedule(function()
|
||||||
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(buf, string.format("gitea://issue_%d.yaml", number))
|
||||||
|
vim.api.nvim_buf_set_option(buf, "filetype", "yaml")
|
||||||
|
|
||||||
|
set_issue_buffer_options(buf)
|
||||||
|
attach_issue_autocmds(buf)
|
||||||
|
|
||||||
|
vim.api.nvim_buf_set_var(buf, "gitea_issue_owner", owner)
|
||||||
|
vim.api.nvim_buf_set_var(buf, "gitea_issue_repo", repo)
|
||||||
|
vim.api.nvim_buf_set_var(buf, "gitea_issue_number", number)
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
table.insert(lines, "issue:")
|
||||||
|
table.insert(lines, (" number: %d"):format(issue.number or number))
|
||||||
|
table.insert(lines, (" title: %s"):format(issue.title or ""))
|
||||||
|
table.insert(lines, (" state: %s"):format(issue.state or ""))
|
||||||
|
table.insert(lines, " body: |")
|
||||||
|
if type(issue.body) == "string" and #issue.body > 0 then
|
||||||
|
for line in string.gmatch(issue.body, "[^\r\n]+") do
|
||||||
|
table.insert(lines, " " .. line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(lines, "")
|
||||||
|
table.insert(lines, "comments:")
|
||||||
|
|
||||||
|
-- newest on top => loop in reverse
|
||||||
|
if type(comments) == "table" then
|
||||||
|
for i = #comments, 1, -1 do
|
||||||
|
local c = comments[i]
|
||||||
|
local cid = c.id or 0
|
||||||
|
local user = (c.user and c.user.login) or "unknown"
|
||||||
|
local cbody = c.body or ""
|
||||||
|
table.insert(lines, (" - id: %d"):format(cid))
|
||||||
|
table.insert(lines, (" user: %s"):format(user))
|
||||||
|
table.insert(lines, " body: |")
|
||||||
|
if #cbody > 0 then
|
||||||
|
for cline in string.gmatch(cbody, "[^\r\n]+") do
|
||||||
|
table.insert(lines, " " .. cline)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(lines, "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(lines, "# Add new comments by omitting 'id:'; e.g.:")
|
||||||
|
table.insert(lines, "# - body: |")
|
||||||
|
table.insert(lines, "# new multiline comment")
|
||||||
|
|
||||||
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||||
|
|
||||||
|
-- Keymap to open or create a branch for the issue
|
||||||
|
vim.api.nvim_buf_set_keymap(
|
||||||
|
buf,
|
||||||
|
"n",
|
||||||
|
"<leader>ib",
|
||||||
|
":lua require('gitea.issues').open_or_create_branch()<CR>",
|
||||||
|
{ noremap = true, silent = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Keymap to close the issue buffer quickly
|
||||||
|
vim.api.nvim_buf_set_keymap(
|
||||||
|
buf,
|
||||||
|
"n",
|
||||||
|
"<leader>iq",
|
||||||
|
":bd!<CR>",
|
||||||
|
{ noremap = true, silent = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Keymap to create a pull request (<leader>ip)
|
||||||
|
-- This will close the issue buffer and open the new PR buffer.
|
||||||
|
vim.api.nvim_buf_set_keymap(
|
||||||
|
buf,
|
||||||
|
"n",
|
||||||
|
"<leader>ip",
|
||||||
|
":lua require('gitea.issues').create_pull_request_for_issue()<CR>",
|
||||||
|
{ noremap = true, silent = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
if core.preview_style == "split" then
|
||||||
|
vim.cmd("vsplit")
|
||||||
|
vim.api.nvim_set_current_buf(buf)
|
||||||
|
else
|
||||||
|
local width = math.floor(vim.o.columns * 0.7)
|
||||||
|
local height = math.floor(vim.o.lines * 0.7)
|
||||||
|
local row = math.floor((vim.o.lines - height) / 2)
|
||||||
|
local col = math.floor((vim.o.columns - width) / 2)
|
||||||
|
vim.api.nvim_open_win(buf, true, {
|
||||||
|
relative = "editor",
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
row = row,
|
||||||
|
col = col,
|
||||||
|
style = "minimal",
|
||||||
|
border = "rounded"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- M.create_issue
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local function create_issue(owner, repo, title, body, cb)
|
||||||
|
owner, repo = fallback_owner_repo(owner, repo)
|
||||||
|
if not title or title == "" then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd([[echoerr "Usage: :Gitea issue create <title> [body...]"]])
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local ep = string.format("/api/v1/repos/%s/%s/issues", owner, repo)
|
||||||
|
local data = { title = title, body = body or "", labels = {} }
|
||||||
|
core.request("POST", ep, data, function(resp, err)
|
||||||
|
if err then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd(('echoerr "Failed to create issue: %s"'):format(err))
|
||||||
|
end)
|
||||||
|
if cb then
|
||||||
|
cb(nil, err)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if cb then
|
||||||
|
cb(resp, nil)
|
||||||
|
else
|
||||||
|
print(string.format("Issue #%d created: %s", resp.number, resp.title))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- M.open_or_create_branch
|
||||||
|
-- Now using issue title in branch name: `issue-<number>-<slug-of-title>`
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
function M.open_or_create_branch()
|
||||||
|
local buf = vim.api.nvim_get_current_buf()
|
||||||
|
|
||||||
|
-- Get the lines so we can parse the issue's title for the slug
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||||
|
local doc_str = table.concat(lines, "\n")
|
||||||
|
local ok, docs = pcall(lyaml.load, doc_str)
|
||||||
|
if not ok or type(docs) ~= "table" then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd([[echoerr "Failed to parse YAML for branch creation!"]])
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local issue = docs.issue or {}
|
||||||
|
local title = (issue.title or "")
|
||||||
|
local slug = slugify_title(title)
|
||||||
|
|
||||||
|
-- Retrieve the issue metadata from buffer variables
|
||||||
|
local owner = vim.api.nvim_buf_get_var(buf, "gitea_issue_owner")
|
||||||
|
local repo = vim.api.nvim_buf_get_var(buf, "gitea_issue_repo")
|
||||||
|
local number = vim.api.nvim_buf_get_var(buf, "gitea_issue_number")
|
||||||
|
|
||||||
|
local branch_name = ("issue-%d-%s"):format(number, slug)
|
||||||
|
|
||||||
|
-- Check if the branch exists
|
||||||
|
local job_check_branch = Job:new({
|
||||||
|
command = "git",
|
||||||
|
args = { "branch", "--list", branch_name },
|
||||||
|
on_exit = function(j, return_val)
|
||||||
|
if return_val == 0 then
|
||||||
|
local result = j:result()
|
||||||
|
vim.schedule(function()
|
||||||
|
if #result > 0 then
|
||||||
|
-- Branch already exists, just check it out
|
||||||
|
Job:new({
|
||||||
|
command = "git",
|
||||||
|
args = { "checkout", branch_name },
|
||||||
|
on_exit = function(_, ret)
|
||||||
|
vim.schedule(function()
|
||||||
|
if ret == 0 then
|
||||||
|
print("[gitea.nvim] Switched to existing branch: " .. branch_name)
|
||||||
|
else
|
||||||
|
vim.cmd(('echoerr "Failed to checkout branch: %s"'):format(branch_name))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
}):start()
|
||||||
|
else
|
||||||
|
-- Branch does not exist, create it
|
||||||
|
Job:new({
|
||||||
|
command = "git",
|
||||||
|
args = { "checkout", "-b", branch_name },
|
||||||
|
on_exit = function(_, ret)
|
||||||
|
vim.schedule(function()
|
||||||
|
if ret == 0 then
|
||||||
|
print("[gitea.nvim] Created new branch: " .. branch_name)
|
||||||
|
else
|
||||||
|
vim.cmd(('echoerr "Failed to create branch: %s"'):format(branch_name))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
}):start()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd('echoerr "Failed to check local branches."')
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
job_check_branch:start()
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- M.create_pull_request_for_issue
|
||||||
|
-- 1) Derive branch name using 'issue-<number>-<slug-of-title>'
|
||||||
|
-- 2) If branch doesn't exist, warn user
|
||||||
|
-- 3) If yes, create a PR from that branch to 'main', close the issue buffer,
|
||||||
|
-- and open a new PR buffer.
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
function M.create_pull_request_for_issue()
|
||||||
|
local buf = vim.api.nvim_get_current_buf()
|
||||||
|
local owner = vim.api.nvim_buf_get_var(buf, "gitea_issue_owner")
|
||||||
|
local repo = vim.api.nvim_buf_get_var(buf, "gitea_issue_repo")
|
||||||
|
local number = vim.api.nvim_buf_get_var(buf, "gitea_issue_number")
|
||||||
|
|
||||||
|
-- Parse the doc to get the issue title
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||||
|
local doc_str = table.concat(lines, "\n")
|
||||||
|
local ok, docs = pcall(lyaml.load, doc_str)
|
||||||
|
if not ok or type(docs) ~= "table" then
|
||||||
|
vim.cmd([[echoerr "Failed to parse YAML for pull request creation!"]])
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local issue = docs.issue or {}
|
||||||
|
local title = issue.title or ""
|
||||||
|
local slug = slugify_title(title)
|
||||||
|
local branch = ("issue-%d-%s"):format(number, slug)
|
||||||
|
|
||||||
|
-- Check if local branch exists
|
||||||
|
local job_check_branch = Job:new({
|
||||||
|
command = "git",
|
||||||
|
args = { "branch", "--list", branch },
|
||||||
|
on_exit = function(j, return_val)
|
||||||
|
vim.schedule(function()
|
||||||
|
if return_val ~= 0 then
|
||||||
|
vim.cmd([[echoerr "Failed to check local branches."]])
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local result = j:result()
|
||||||
|
if #result == 0 then
|
||||||
|
-- Branch doesn't exist locally
|
||||||
|
vim.cmd(('echoerr "Local branch `%s` does not exist. Create/open it first."'):format(branch))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If we reach here, the branch exists. Create the PR from `branch` to `main`.
|
||||||
|
local pr_title = ("Issue #%d - %s"):format(number, title)
|
||||||
|
local pr_body = ("Automatically created from branch `%s`."):format(branch)
|
||||||
|
|
||||||
|
pulls.create_pull(owner, repo, branch, "main", pr_title, pr_body, function(new_pr, err)
|
||||||
|
if err then
|
||||||
|
vim.cmd(('echoerr "Failed to create pull request: %s"'):format(err))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
print(string.format("[gitea.nvim] Pull request #%d created.", new_pr.number))
|
||||||
|
|
||||||
|
-- Close the issue buffer
|
||||||
|
vim.cmd("bd!")
|
||||||
|
|
||||||
|
-- Open the new PR buffer
|
||||||
|
pulls.open_pull_in_buffer(owner, repo, new_pr.number)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
job_check_branch:start()
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- Exports
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
M.list_issues = list_issues
|
||||||
|
M.open_issue = open_issue
|
||||||
|
M.create_issue = create_issue
|
||||||
|
|
||||||
|
return M
|
||||||
261
lua/gitea/pulls.lua
Normal file
261
lua/gitea/pulls.lua
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- lua/gitea/pulls.lua
|
||||||
|
--
|
||||||
|
-- Provides functions specifically for Gitea pull requests:
|
||||||
|
-- list, create, merge, comment, etc.
|
||||||
|
-- We add a key binding to merge the PR in the PR buffer.
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
local core = require('gitea')
|
||||||
|
local Job = require('plenary.job')
|
||||||
|
local lyaml = require("lyaml")
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- fallback for owner/repo
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local function fallback_owner_repo(owner, repo)
|
||||||
|
if (not owner or owner == "") or (not repo or repo == "") then
|
||||||
|
owner = core._cached_owner
|
||||||
|
repo = core._cached_repo
|
||||||
|
end
|
||||||
|
if not owner or not repo then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd([[echoerr "No owner/repo provided and none found in .git/config"]])
|
||||||
|
end)
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
return owner, repo
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- M.list_pulls
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
function M.list_pulls(owner, repo, on_done)
|
||||||
|
owner, repo = fallback_owner_repo(owner, repo)
|
||||||
|
if not owner or not repo then return end
|
||||||
|
|
||||||
|
local ep = string.format("/api/v1/repos/%s/%s/pulls", owner, repo)
|
||||||
|
core.request("GET", ep, nil, function(data, err)
|
||||||
|
if err then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd(('echoerr "Failed to list PRs: %s"'):format(err))
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if on_done then
|
||||||
|
on_done(data)
|
||||||
|
else
|
||||||
|
print(string.format("Got %d PRs for %s/%s", #data, owner, repo))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- M.merge_pull
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
function M.merge_pull(owner, repo, pr_number)
|
||||||
|
owner, repo = fallback_owner_repo(owner, repo)
|
||||||
|
if not owner or not repo or not pr_number then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd([[echoerr "Usage: :Gitea pr merge <owner> <repo> <pr_number>"]])
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local ep = string.format("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, pr_number)
|
||||||
|
local data = { Do = "merge" }
|
||||||
|
core.request("POST", ep, data, function(_, err)
|
||||||
|
if err then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd(('echoerr "Failed to merge PR: %s"'):format(err))
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
print(("Merged PR #%d"):format(pr_number))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- M.create_pull
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
function M.create_pull(owner, repo, head, base, title, body, cb)
|
||||||
|
owner, repo = fallback_owner_repo(owner, repo)
|
||||||
|
if not owner or not repo then
|
||||||
|
if cb then cb(nil, "No owner/repo provided") end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not head or head == "" or not base or base == "" then
|
||||||
|
if cb then cb(nil, "Head or base branch not specified") end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local ep = string.format("/api/v1/repos/%s/%s/pulls", owner, repo)
|
||||||
|
local data = {
|
||||||
|
head = head,
|
||||||
|
base = base,
|
||||||
|
title = title or ("PR from " .. head),
|
||||||
|
body = body or ""
|
||||||
|
}
|
||||||
|
core.request("POST", ep, data, function(resp, err)
|
||||||
|
if err then
|
||||||
|
if cb then cb(nil, err) end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if cb then
|
||||||
|
cb(resp, nil)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- Buffer approach for the PR (like issues)
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
local function set_pr_buffer_options(buf)
|
||||||
|
vim.api.nvim_buf_set_option(buf, "buftype", "acwrite") -- allow saving
|
||||||
|
vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function attach_pr_autocmds(buf)
|
||||||
|
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||||
|
buffer = buf,
|
||||||
|
callback = function()
|
||||||
|
local owner = vim.api.nvim_buf_get_var(buf, "gitea_pr_owner")
|
||||||
|
local repo = vim.api.nvim_buf_get_var(buf, "gitea_pr_repo")
|
||||||
|
local number = vim.api.nvim_buf_get_var(buf, "gitea_pr_number")
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||||
|
local doc_str = table.concat(lines, "\n")
|
||||||
|
|
||||||
|
-- Parse as YAML
|
||||||
|
local ok, docs = pcall(lyaml.load, doc_str)
|
||||||
|
if not ok or type(docs) ~= "table" then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd([[echoerr "Failed to parse PR buffer as YAML!"]])
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local pull_data = docs.pull or {}
|
||||||
|
local patch_data = {}
|
||||||
|
|
||||||
|
if type(pull_data.title) == "string" and pull_data.title ~= "" then
|
||||||
|
patch_data.title = pull_data.title
|
||||||
|
end
|
||||||
|
if type(pull_data.body) == "string" then
|
||||||
|
patch_data.body = pull_data.body
|
||||||
|
end
|
||||||
|
|
||||||
|
if vim.tbl_count(patch_data) > 0 then
|
||||||
|
local ep = string.format("/api/v1/repos/%s/%s/pulls/%d", owner, repo, number)
|
||||||
|
core.request("PATCH", ep, patch_data, function(_, err)
|
||||||
|
if err then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd(('echoerr "Failed to update pull request: %s"'):format(err))
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
print("[gitea.nvim] Pull request updated.")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- Keymap callback to merge the current PR
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
function M.merge_current_pr()
|
||||||
|
local buf = vim.api.nvim_get_current_buf()
|
||||||
|
local owner = vim.api.nvim_buf_get_var(buf, "gitea_pr_owner")
|
||||||
|
local repo = vim.api.nvim_buf_get_var(buf, "gitea_pr_repo")
|
||||||
|
local number = vim.api.nvim_buf_get_var(buf, "gitea_pr_number")
|
||||||
|
M.merge_pull(owner, repo, number)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.open_pull_in_buffer(owner, repo, pr_number)
|
||||||
|
owner, repo = fallback_owner_repo(owner, repo)
|
||||||
|
if not owner or not repo or not pr_number then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd([[echoerr "Usage: open_pull_in_buffer <owner> <repo> <pr_number>"]])
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 1) Get PR details
|
||||||
|
local pr_ep = string.format("/api/v1/repos/%s/%s/pulls/%d", owner, repo, pr_number)
|
||||||
|
core.request("GET", pr_ep, nil, function(pr, err)
|
||||||
|
if err then
|
||||||
|
vim.schedule(function()
|
||||||
|
vim.cmd(('echoerr "Failed to load PR: %s"'):format(err))
|
||||||
|
end)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 2) Create buffer, fill it
|
||||||
|
vim.schedule(function()
|
||||||
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(buf, string.format("gitea://pr_%d.yaml", pr_number))
|
||||||
|
vim.api.nvim_buf_set_option(buf, "filetype", "yaml")
|
||||||
|
|
||||||
|
set_pr_buffer_options(buf)
|
||||||
|
attach_pr_autocmds(buf)
|
||||||
|
|
||||||
|
vim.api.nvim_buf_set_var(buf, "gitea_pr_owner", owner)
|
||||||
|
vim.api.nvim_buf_set_var(buf, "gitea_pr_repo", repo)
|
||||||
|
vim.api.nvim_buf_set_var(buf, "gitea_pr_number", pr_number)
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
table.insert(lines, "pull:")
|
||||||
|
table.insert(lines, (" number: %d"):format(pr.number or pr_number))
|
||||||
|
table.insert(lines, (" title: %s"):format(pr.title or ""))
|
||||||
|
table.insert(lines, (" state: %s"):format(pr.state or ""))
|
||||||
|
table.insert(lines, " body: |")
|
||||||
|
if type(pr.body) == "string" and #pr.body > 0 then
|
||||||
|
for line in string.gmatch(pr.body, "[^\r\n]+") do
|
||||||
|
table.insert(lines, " " .. line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||||
|
|
||||||
|
-- Keymap to merge the current PR from the buffer
|
||||||
|
vim.api.nvim_buf_set_keymap(
|
||||||
|
buf,
|
||||||
|
"n",
|
||||||
|
"<leader>pm",
|
||||||
|
":lua require('gitea.pulls').merge_current_pr()<CR>",
|
||||||
|
{ noremap = true, silent = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Keymap to close the PR buffer quickly
|
||||||
|
vim.api.nvim_buf_set_keymap(
|
||||||
|
buf,
|
||||||
|
"n",
|
||||||
|
"<leader>pq",
|
||||||
|
":bd!<CR>",
|
||||||
|
{ noremap = true, silent = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Show the buffer
|
||||||
|
if core.preview_style == "split" then
|
||||||
|
vim.cmd("vsplit")
|
||||||
|
vim.api.nvim_set_current_buf(buf)
|
||||||
|
else
|
||||||
|
local width = math.floor(vim.o.columns * 0.7)
|
||||||
|
local height = math.floor(vim.o.lines * 0.7)
|
||||||
|
local row = math.floor((vim.o.lines - height) / 2)
|
||||||
|
local col = math.floor((vim.o.columns - width) / 2)
|
||||||
|
vim.api.nvim_open_win(buf, true, {
|
||||||
|
relative = "editor",
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
row = row,
|
||||||
|
col = col,
|
||||||
|
style = "minimal",
|
||||||
|
border = "rounded"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
50
lua/gitea/telescope.lua
Normal file
50
lua/gitea/telescope.lua
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
----------------------------------------------------------------------------
|
||||||
|
-- lua/gitea/telescope.lua
|
||||||
|
--
|
||||||
|
-- Minimal telescope integration for listing issues
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
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_mod = require("gitea.issues")
|
||||||
|
|
||||||
|
local function open_selected_issue(prompt_bufnr)
|
||||||
|
local selection = action_state.get_selected_entry()
|
||||||
|
actions.close(prompt_bufnr)
|
||||||
|
if not selection then return end
|
||||||
|
|
||||||
|
local issue_obj = selection.value
|
||||||
|
local number = issue_obj.number
|
||||||
|
issues_mod.open_issue(nil, nil, number)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.list_issues_in_telescope()
|
||||||
|
issues_mod.list_issues(nil, nil, function(data)
|
||||||
|
pickers.new({}, {
|
||||||
|
prompt_title = "Gitea Issues",
|
||||||
|
finder = finders.new_table {
|
||||||
|
results = data,
|
||||||
|
entry_maker = function(issue)
|
||||||
|
return {
|
||||||
|
value = issue,
|
||||||
|
display = ("#%d: %s"):format(issue.number, issue.title or ""),
|
||||||
|
ordinal = ("%d %s"):format(issue.number, (issue.title or ""))
|
||||||
|
}
|
||||||
|
end
|
||||||
|
},
|
||||||
|
sorter = conf.generic_sorter({}),
|
||||||
|
attach_mappings = function(prompt_bufnr, map)
|
||||||
|
map("i", "<CR>", open_selected_issue)
|
||||||
|
map("n", "<CR>", open_selected_issue)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
}):find()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
10
plugin/gitea.vim
Normal file
10
plugin/gitea.vim
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
" Automatically load and set up gitea.nvim on startup
|
||||||
|
if exists('g:loaded_gitea_nvim')
|
||||||
|
finish
|
||||||
|
endif
|
||||||
|
let g:loaded_gitea_nvim = 1
|
||||||
|
|
||||||
|
lua << EOF
|
||||||
|
-- Call setup (with default or user config) so the :Gitea command is registered
|
||||||
|
require('gitea').setup()
|
||||||
|
EOF
|
||||||
Reference in New Issue
Block a user