Compare commits
4 Commits
c408cd3d24
...
f46f60b723
| Author | SHA1 | Date | |
|---|---|---|---|
| f46f60b723 | |||
| dd6843091b | |||
| 3d0d721896 | |||
| d8a7da82fe |
@@ -1,3 +1,14 @@
|
||||
project_name: "gitea.nvim"
|
||||
default_prompt_blocks:
|
||||
- "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
|
||||
|
||||
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,168 +1,64 @@
|
||||
local config = require("gitea.config")
|
||||
local auth = require("gitea.auth")
|
||||
local curl = require("plenary.curl")
|
||||
local Job = require('plenary.job')
|
||||
local api = {}
|
||||
|
||||
local M = {}
|
||||
|
||||
local function get_base_url()
|
||||
local server = config.values.server_url
|
||||
if not server or server == "" then
|
||||
-- fallback
|
||||
server = "https://gitea.example.com"
|
||||
end
|
||||
return server
|
||||
end
|
||||
|
||||
local function get_auth_header()
|
||||
local token = auth.get_token()
|
||||
if not token or token == "" then
|
||||
error("[gitea.nvim] Missing Gitea token. Please run :Gitea again or restart after entering a token.")
|
||||
end
|
||||
return "token " .. token
|
||||
end
|
||||
|
||||
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 result = curl.request({
|
||||
url = url,
|
||||
method = method,
|
||||
headers = headers,
|
||||
timeout = 5000, -- we can also read config.values.timeout
|
||||
body = opts.body and vim.json.encode(opts.body) or nil,
|
||||
})
|
||||
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)
|
||||
-- data = { title = "", body = "", labels = {"bug"}, etc. }
|
||||
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
|
||||
|
||||
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 then
|
||||
return vim.json.decode(result.body)
|
||||
end
|
||||
return nil, result and result.status
|
||||
end
|
||||
|
||||
function M.close_issue(owner, repo, number)
|
||||
-- Gitea: state -> "closed"
|
||||
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
|
||||
|
||||
-------------------------------------------------------
|
||||
-- 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)
|
||||
-- data = { head = "branch", base = "master", title = "My PR", body = "..." }
|
||||
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
|
||||
|
||||
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 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)
|
||||
-- merge_style: "merge"|"rebase"|"squash" (in Gitea it's "merge"|"rebase"|"rebase-merge"|"squash" -
|
||||
-- see Gitea docs for specifics; we can map the user’s choice to Gitea’s).
|
||||
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", -- e.g. "merge"
|
||||
MergeTitleField = merge_title or "",
|
||||
MergeMessageField = merge_message or "",
|
||||
-- Helper function for making async requests to Gitea API via curl
|
||||
local function request(method, base_url, endpoint, token, data, callback)
|
||||
local headers = {
|
||||
"Content-Type: application/json",
|
||||
"Authorization: token " .. token
|
||||
}
|
||||
})
|
||||
if result and result.status == 200 then
|
||||
return vim.json.decode(result.body)
|
||||
|
||||
local curl_args = {
|
||||
"-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
|
||||
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
|
||||
|
||||
function M.close_pull_request(owner, repo, number)
|
||||
return M.edit_pull_request(owner, repo, number, { state = "closed" })
|
||||
function api.get(base_url, endpoint, token, cb)
|
||||
request("GET", base_url, endpoint, token, nil, cb)
|
||||
end
|
||||
|
||||
function M.reopen_pull_request(owner, repo, number)
|
||||
return M.edit_pull_request(owner, repo, number, { state = "open" })
|
||||
function api.post(base_url, endpoint, token, data, cb)
|
||||
request("POST", base_url, endpoint, token, data, cb)
|
||||
end
|
||||
|
||||
function M.comment_pull_request(owner, repo, number, body)
|
||||
-- same endpoint as issues for Gitea. A PR is an issue with is_pull = true
|
||||
return M.comment_issue(owner, repo, number, body)
|
||||
function api.patch(base_url, endpoint, token, data, cb)
|
||||
request("PATCH", base_url, endpoint, token, data, cb)
|
||||
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,160 +0,0 @@
|
||||
local config = require("gitea.config")
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
local token_cached = nil
|
||||
|
||||
-- Attempt to read the token file from disk
|
||||
local function read_token_file(path)
|
||||
local fd = uv.fs_open(path, "r", 438) -- 0666 in decimal
|
||||
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
|
||||
local function write_token_file(path, token)
|
||||
local fd = uv.fs_open(path, "w", 384) -- 0600 in 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
|
||||
|
||||
-- Checks if the token file is in .gitignore, if not ask user
|
||||
local function ensure_ignored(token_file, ignore_file)
|
||||
-- Read .gitignore lines if present
|
||||
local file = io.open(ignore_file, "r")
|
||||
local lines = {}
|
||||
if file then
|
||||
for line in file:lines() do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
-- Check if token_file is in .gitignore
|
||||
for _, l in ipairs(lines) do
|
||||
if l == token_file then
|
||||
return
|
||||
end
|
||||
end
|
||||
-- Not in ignore, ask user
|
||||
vim.schedule(function()
|
||||
vim.cmd([[echohl WarningMsg]])
|
||||
vim.cmd([[echo "gitea.nvim: We recommend adding ']]..token_file..[[' to ']]..ignore_file..[['. Add now? (y/N)"]])
|
||||
vim.cmd([[echohl None]])
|
||||
|
||||
local ans = vim.fn.getchar()
|
||||
ans = vim.fn.nr2char(ans)
|
||||
|
||||
if ans == "y" or ans == "Y" then
|
||||
local f = io.open(ignore_file, "a")
|
||||
if f then
|
||||
f:write("\n" .. token_file .. "\n")
|
||||
f:close()
|
||||
vim.notify("Added '"..token_file.."' to '"..ignore_file.."'", vim.log.levels.INFO)
|
||||
else
|
||||
vim.notify("Failed to open '"..ignore_file.."' for appending.", vim.log.levels.ERROR)
|
||||
end
|
||||
else
|
||||
vim.notify("Ok, not adding token file to '"..ignore_file.."'. Be mindful of security!", vim.log.levels.WARN)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Parse .git/config or `git remote get-url origin` to guess the Gitea server
|
||||
local function detect_gitea_server()
|
||||
-- Attempt: 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
|
||||
end
|
||||
-- Example: https://gitea.myserver.com/owner/repo.git
|
||||
-- Strip `.git` suffix if present
|
||||
url = url:gsub("%.git$", "")
|
||||
-- Attempt to extract domain, e.g. "https://gitea.myserver.com"
|
||||
-- We'll do a naive pattern match
|
||||
local protocol, domain_and_path = url:match("^(https?://)(.*)$")
|
||||
if not protocol or not domain_and_path then
|
||||
-- Could be SSH or something else
|
||||
-- For instance: git@gitea.myserver.com:owner/repo.git
|
||||
-- Try a different parse
|
||||
local user_host, path = url:match("^(.-):(.*)$")
|
||||
if user_host and path and user_host:match(".*@") then
|
||||
-- user_host could be "git@gitea.myserver.com"
|
||||
local host = user_host:match("@(.*)")
|
||||
if host then
|
||||
return "https://"..host
|
||||
end
|
||||
end
|
||||
-- Fallback
|
||||
return nil
|
||||
end
|
||||
|
||||
-- domain_and_path might be "gitea.myserver.com/owner/repo"
|
||||
-- just extract "gitea.myserver.com"
|
||||
local server = domain_and_path:match("^([^/]+)")
|
||||
if not server then
|
||||
return nil
|
||||
end
|
||||
return protocol .. server
|
||||
end
|
||||
|
||||
-- Called during plugin setup or first command usage
|
||||
function M.ensure_token()
|
||||
if token_cached then
|
||||
return token_cached
|
||||
end
|
||||
|
||||
local cfg = config.values
|
||||
local token_file = cfg.config_file
|
||||
local ignore_file = cfg.ignore_file
|
||||
|
||||
-- Attempt to detect the Gitea server if not provided
|
||||
if not cfg.server_url or cfg.server_url == "" then
|
||||
cfg.server_url = detect_gitea_server()
|
||||
end
|
||||
if not cfg.server_url then
|
||||
vim.notify("[gitea.nvim] Could not detect Gitea server from .git/config. Please configure manually.", vim.log.levels.WARN)
|
||||
end
|
||||
|
||||
local token_data = read_token_file(token_file)
|
||||
if token_data and token_data ~= "" then
|
||||
token_cached = token_data
|
||||
return token_cached
|
||||
end
|
||||
|
||||
-- If we reach here, no token was found
|
||||
vim.schedule(function()
|
||||
vim.cmd([[echohl WarningMsg]])
|
||||
vim.cmd([[echo "gitea.nvim: No Gitea token found. Please enter your Gitea Personal Access Token:"]])
|
||||
vim.cmd([[echohl None]])
|
||||
|
||||
local user_token = vim.fn.input("Token: ")
|
||||
vim.cmd("redraw")
|
||||
|
||||
if user_token == nil or user_token == "" then
|
||||
vim.notify("[gitea.nvim] No token provided, plugin may not work properly.", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
write_token_file(token_file, user_token)
|
||||
token_cached = user_token
|
||||
vim.notify("Token saved to " .. token_file .. ".", vim.log.levels.INFO)
|
||||
|
||||
-- Offer to add token_file to .gitignore
|
||||
ensure_ignored(token_file, ignore_file)
|
||||
end)
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function M.get_token()
|
||||
return token_cached
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,442 +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 config = require("gitea.config")
|
||||
local api = require("gitea.api")
|
||||
local auth = require("gitea.auth")
|
||||
--------------------------------------------------------------------------
|
||||
-- A simple completion function for the :Gitea command
|
||||
--------------------------------------------------------------------------
|
||||
function M._gitea_cmd_complete(arg_lead, cmd_line, cursor_pos)
|
||||
local tokens = vim.split(cmd_line, "%s+")
|
||||
|
||||
-- Utility: parse "owner/repo" from the remote URL
|
||||
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
|
||||
if not path then
|
||||
return nil, nil
|
||||
end
|
||||
local owner, repo = path:match("^(.-)/(.*)$")
|
||||
return owner, repo
|
||||
-- If the user hasn't typed anything after ":Gitea", suggest top-level words
|
||||
if #tokens <= 1 then
|
||||
return { "issue", "pr" }
|
||||
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
|
||||
local main_sub = tokens[2]
|
||||
|
||||
-------------------------------
|
||||
-- Subcommands
|
||||
-------------------------------
|
||||
local subcommands = {}
|
||||
|
||||
subcommands.issue = {
|
||||
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
|
||||
if #tokens == 2 then
|
||||
local candidates = { "issue", "pr" }
|
||||
local results = {}
|
||||
for _, c in ipairs(candidates) do
|
||||
if c:find("^" .. arg_lead) then
|
||||
table.insert(results, c)
|
||||
end
|
||||
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
|
||||
return results
|
||||
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)
|
||||
-- If "issue" was selected, next subcommands can be list, create
|
||||
if main_sub == "issue" 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
|
||||
})
|
||||
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
end,
|
||||
|
||||
close = function(args)
|
||||
local number = tonumber(args[1])
|
||||
if not number then
|
||||
vim.notify("[gitea.nvim] Usage: :Gitea issue close <number>", vim.log.levels.ERROR)
|
||||
return
|
||||
return results
|
||||
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)
|
||||
end
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
-- Expand with other objects (e.g. 'repo', 'review', etc.) if needed.
|
||||
|
||||
-------------------------------
|
||||
-- Command dispatcher
|
||||
-------------------------------
|
||||
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()
|
||||
|
||||
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.
|
||||
|
||||
if not object then
|
||||
print("Usage: :Gitea <object> <action> [args]")
|
||||
print("Examples:")
|
||||
print(" :Gitea issue list")
|
||||
print(" :Gitea pr list")
|
||||
return
|
||||
end
|
||||
|
||||
if not subcommands[object] 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]
|
||||
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)
|
||||
|
||||
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
|
||||
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 {}
|
||||
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
|
||||
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
local Config = {}
|
||||
local M = {}
|
||||
|
||||
-- Default settings (user can override in setup{})
|
||||
local defaults = {
|
||||
config_file = ".gitea_nvim_token", -- local file containing the token
|
||||
ignore_file = ".gitignore", -- file to which we can append the token file
|
||||
server_url = nil, -- extracted from .git remote if possible
|
||||
M.default_config = {
|
||||
base_url = nil, -- The plugin attempts to detect from .git config or prompt if not set
|
||||
token_file = vim.fn.stdpath('config') .. '/.gitea_token',
|
||||
preview_style = "floating", -- or "split"
|
||||
}
|
||||
|
||||
Config.values = {}
|
||||
|
||||
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
|
||||
return M
|
||||
|
||||
@@ -1,18 +1,243 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- 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 M = {}
|
||||
|
||||
local config = require("gitea.config")
|
||||
local commands = require("gitea.commands")
|
||||
local auth = require("gitea.auth")
|
||||
----------------------------------------------------------------------------
|
||||
-- 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
|
||||
|
||||
-- Main entrypoint
|
||||
function M.setup(user_opts)
|
||||
config.setup(user_opts or {})
|
||||
----------------------------------------------------------------------------
|
||||
-- Internal Utils
|
||||
----------------------------------------------------------------------------
|
||||
local function ensure_dir(dirpath)
|
||||
if vim.fn.isdirectory(dirpath) == 0 then
|
||||
vim.fn.mkdir(dirpath, "p")
|
||||
end
|
||||
end
|
||||
|
||||
-- Attempt to load existing token; if not found, prompt user
|
||||
auth.ensure_token()
|
||||
local function read_git_config_lines()
|
||||
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
|
||||
|
||||
-- Register :Gitea command and subcommands
|
||||
commands.register()
|
||||
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
|
||||
|
||||
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