From c408cd3d241ef4e2ed20b997151ab9212e0f07d7 Mon Sep 17 00:00:00 2001 From: Dominik Polakovics Date: Mon, 23 Dec 2024 01:46:04 +0100 Subject: [PATCH] initial commit --- .chatgpt_config.yaml | 3 + README.md | 72 +++++++ lua/gitea/api.lua | 168 ++++++++++++++++ lua/gitea/auth.lua | 160 +++++++++++++++ lua/gitea/commands.lua | 443 +++++++++++++++++++++++++++++++++++++++++ lua/gitea/config.lua | 22 ++ lua/gitea/init.lua | 18 ++ 7 files changed, 886 insertions(+) create mode 100644 .chatgpt_config.yaml create mode 100644 README.md create mode 100644 lua/gitea/api.lua create mode 100644 lua/gitea/auth.lua create mode 100644 lua/gitea/commands.lua create mode 100644 lua/gitea/config.lua create mode 100644 lua/gitea/init.lua diff --git a/.chatgpt_config.yaml b/.chatgpt_config.yaml new file mode 100644 index 0000000..c16601d --- /dev/null +++ b/.chatgpt_config.yaml @@ -0,0 +1,3 @@ +project_name: "gitea.nvim" +default_prompt_blocks: + - "basic-prompt" diff --git a/README.md b/README.md new file mode 100644 index 0000000..ab33952 --- /dev/null +++ b/README.md @@ -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 ` — Opens an issue buffer for ``. +- `:Gitea pr list` — Lists pull requests for the current repo. +- `:Gitea pr edit ` — Opens a PR buffer for ``. +- `: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 `) +- 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/) diff --git a/lua/gitea/api.lua b/lua/gitea/api.lua new file mode 100644 index 0000000..e1299d2 --- /dev/null +++ b/lua/gitea/api.lua @@ -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 diff --git a/lua/gitea/auth.lua b/lua/gitea/auth.lua new file mode 100644 index 0000000..ce9a313 --- /dev/null +++ b/lua/gitea/auth.lua @@ -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 diff --git a/lua/gitea/commands.lua b/lua/gitea/commands.lua new file mode 100644 index 0000000..9fae399 --- /dev/null +++ b/lua/gitea/commands.lua @@ -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 ", 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 ", 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 ", 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 ", 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 ", 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 [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 ")) + vim.notify("[gitea.nvim] Branch deletion not implemented. Use `git push origin --delete `.") + 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 ", 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 ", 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 ", 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 [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 + 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 diff --git a/lua/gitea/config.lua b/lua/gitea/config.lua new file mode 100644 index 0000000..e02ed5e --- /dev/null +++ b/lua/gitea/config.lua @@ -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 diff --git a/lua/gitea/init.lua b/lua/gitea/init.lua new file mode 100644 index 0000000..af1eaaf --- /dev/null +++ b/lua/gitea/init.lua @@ -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