initial commit
This commit is contained in:
3
.chatgpt_config.yaml
Normal file
3
.chatgpt_config.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
project_name: "gitea.nvim"
|
||||
default_prompt_blocks:
|
||||
- "basic-prompt"
|
||||
72
README.md
Normal file
72
README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# gitea.nvim
|
||||
|
||||
A Neovim plugin to manage [Gitea](https://gitea.io) issues and pull requests right from your editor, inspired by [octo.nvim](https://github.com/pwntester/octo.nvim).
|
||||
|
||||
## Features
|
||||
- Browse, open, and edit issues
|
||||
- Create new issues (including adding labels, assignees, etc.)
|
||||
- Browse, open, and edit pull requests
|
||||
- Merge, squash, or rebase pull requests
|
||||
- Add/modify/delete comments on issues and PRs
|
||||
- Inline PR reviews and comments
|
||||
- Add/remove labels, reviewers, assignees, etc.
|
||||
- And more (mirroring much of octo.nvim’s functionality)
|
||||
|
||||
## Requirements
|
||||
- **Neovim 0.10+**
|
||||
- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) for async utilities
|
||||
- A running Gitea server
|
||||
- A Personal Access Token (PAT) for Gitea
|
||||
|
||||
## Installation
|
||||
Install using your favorite plugin manager, for example with **packer**:
|
||||
|
||||
```lua
|
||||
use {
|
||||
"your-username/gitea.nvim",
|
||||
requires = {"nvim-lua/plenary.nvim"}
|
||||
}
|
||||
```
|
||||
|
||||
## Setup & First Run
|
||||
|
||||
There's no need to store your Gitea server or token directly in your `init.lua`.
|
||||
The plugin will:
|
||||
1. Parse your `.git/config` to attempt to detect your Gitea server URL (e.g. from `git config remote.origin.url`).
|
||||
2. Check for a local token file (by default `.gitea_nvim_token`) in your repo directory.
|
||||
3. If no token file is found, `gitea.nvim` will prompt you for one. The plugin will then offer to add that token file to your `.gitignore` (highly recommended).
|
||||
|
||||
Example usage in your config:
|
||||
```lua
|
||||
require("gitea").setup({
|
||||
-- No need to provide a hostname or token here;
|
||||
-- but you can override defaults if needed:
|
||||
-- e.g. config_file = ".my_custom_token_file"
|
||||
-- or ignore_file = ".my_custom_gitignore"
|
||||
})
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Once installed and configured, you can use the `:Gitea` command with subcommands:
|
||||
- `:Gitea issue list` — Lists issues for the current repo.
|
||||
- `:Gitea issue create` — Opens a scratch buffer to create a new issue.
|
||||
- `:Gitea issue edit <number>` — Opens an issue buffer for `<number>`.
|
||||
- `:Gitea pr list` — Lists pull requests for the current repo.
|
||||
- `:Gitea pr edit <number>` — Opens a PR buffer for `<number>`.
|
||||
- `:Gitea pr merge [commit|rebase|squash] [delete|nodelete]` — Merges a PR with the selected method.
|
||||
- `:Gitea pr create` — Creates a new pull request from your current branch to the base branch.
|
||||
- More subcommands (close, reopen, comment, reaction, etc.) will be added similarly to octo.nvim.
|
||||
|
||||
When you open an issue or PR buffer, simply edit the title, body, or add new comments. When you save (`:w`), changes are pushed to Gitea.
|
||||
|
||||
## Roadmap
|
||||
- Inline PR reviews (open diff views, comment on lines, etc.)
|
||||
- Advanced searching and filtering (like `:Gitea issue search <query>`)
|
||||
- Further config options (like picking a fuzzy-finder: telescope or fzf-lua)
|
||||
|
||||
## Contributing
|
||||
Pull requests and issue reports are welcome! The goal is to replicate and adapt the core [octo.nvim](https://github.com/pwntester/octo.nvim) workflow for Gitea.
|
||||
|
||||
## License
|
||||
[MIT](https://choosealicense.com/licenses/mit/)
|
||||
168
lua/gitea/api.lua
Normal file
168
lua/gitea/api.lua
Normal file
@@ -0,0 +1,168 @@
|
||||
local config = require("gitea.config")
|
||||
local auth = require("gitea.auth")
|
||||
local curl = require("plenary.curl")
|
||||
|
||||
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 "",
|
||||
}
|
||||
})
|
||||
if result and result.status == 200 then
|
||||
return vim.json.decode(result.body)
|
||||
end
|
||||
return nil, result and result.status
|
||||
end
|
||||
|
||||
function M.close_pull_request(owner, repo, number)
|
||||
return M.edit_pull_request(owner, repo, number, { state = "closed" })
|
||||
end
|
||||
|
||||
function M.reopen_pull_request(owner, repo, number)
|
||||
return M.edit_pull_request(owner, repo, number, { state = "open" })
|
||||
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)
|
||||
end
|
||||
|
||||
return M
|
||||
160
lua/gitea/auth.lua
Normal file
160
lua/gitea/auth.lua
Normal file
@@ -0,0 +1,160 @@
|
||||
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
|
||||
443
lua/gitea/commands.lua
Normal file
443
lua/gitea/commands.lua
Normal file
@@ -0,0 +1,443 @@
|
||||
local M = {}
|
||||
|
||||
local config = require("gitea.config")
|
||||
local api = require("gitea.api")
|
||||
local auth = require("gitea.auth")
|
||||
|
||||
-- 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
|
||||
end
|
||||
|
||||
-- SSH style: git@my.gitea.com:owner/repo
|
||||
local user_host, path = url:match("^(.-):(.*)$")
|
||||
if user_host and path then
|
||||
-- user_host might be "git@my.gitea.com"
|
||||
local owner, repo = path:match("^(.-)/(.*)$")
|
||||
return owner, repo
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
-------------------------------
|
||||
-- Subcommands
|
||||
-------------------------------
|
||||
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
|
||||
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
|
||||
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)
|
||||
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
|
||||
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
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
22
lua/gitea/config.lua
Normal file
22
lua/gitea/config.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
local Config = {}
|
||||
|
||||
-- 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
|
||||
}
|
||||
|
||||
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
|
||||
18
lua/gitea/init.lua
Normal file
18
lua/gitea/init.lua
Normal file
@@ -0,0 +1,18 @@
|
||||
local M = {}
|
||||
|
||||
local config = require("gitea.config")
|
||||
local commands = require("gitea.commands")
|
||||
local auth = require("gitea.auth")
|
||||
|
||||
-- Main entrypoint
|
||||
function M.setup(user_opts)
|
||||
config.setup(user_opts or {})
|
||||
|
||||
-- Attempt to load existing token; if not found, prompt user
|
||||
auth.ensure_token()
|
||||
|
||||
-- Register :Gitea command and subcommands
|
||||
commands.register()
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user