mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-15 23:53:18 +00:00
feat(browse): add :Jbrowse to open current file on remote (#89)
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
--- @class jj.cmd.browse
|
||||
local M = {}
|
||||
|
||||
local utils = require("jj.utils")
|
||||
|
||||
--- Build a line anchor for a hosted-file URL.
|
||||
--- GitHub uses `#L<start>-L<end>` for ranges; GitLab uses `#L<start>-<end>`.
|
||||
--- @param host string|nil Remote host name (e.g. github.com)
|
||||
--- @param line1 integer|nil 1-indexed start line
|
||||
--- @param line2 integer|nil 1-indexed end line
|
||||
--- @return string anchor URL fragment (including leading `#`), or empty string
|
||||
local function build_anchor(host, line1, line2)
|
||||
if not line1 then
|
||||
return ""
|
||||
end
|
||||
|
||||
-- GitHub: #L1-L2, GitLab: #L1-2
|
||||
local is_gitlab = host and host:match("gitlab")
|
||||
if line2 and line2 ~= line1 then
|
||||
if is_gitlab then
|
||||
return string.format("#L%d-%d", line1, line2)
|
||||
end
|
||||
return string.format("#L%d-L%d", line1, line2)
|
||||
end
|
||||
|
||||
return string.format("#L%d", line1)
|
||||
end
|
||||
|
||||
--- Build a browser URL for a file in a repo.
|
||||
--- Falls back to GitHub-style routing unless host is recognized.
|
||||
--- @param base_repo_url string HTTPS base repo URL (e.g. https://host/owner/repo)
|
||||
--- @param host string Remote hostname
|
||||
--- @param ref string Commit SHA (preferred) or branch-like reference
|
||||
--- @param path string Repo-relative file path
|
||||
--- @param line1 integer|nil 1-indexed start line
|
||||
--- @param line2 integer|nil 1-indexed end line
|
||||
--- @return string url
|
||||
local function build_browse_url(base_repo_url, host, ref, path, line1, line2)
|
||||
local encoded_path = utils.url_encode_path(path)
|
||||
local anchor = build_anchor(host, line1, line2)
|
||||
|
||||
if host and host:match("gitlab") then
|
||||
return string.format("%s/-/blob/%s/%s%s", base_repo_url, ref, encoded_path, anchor)
|
||||
end
|
||||
|
||||
if host and (host:match("gitea") or host:match("forgejo")) then
|
||||
-- Use commit URLs only when ref looks like a SHA; otherwise use branch URLs.
|
||||
local is_sha = type(ref) == "string" and ref:match("^[0-9a-fA-F]+$") and #ref >= 7
|
||||
local kind = is_sha and "commit" or "branch"
|
||||
return string.format("%s/src/%s/%s/%s%s", base_repo_url, kind, ref, encoded_path, anchor)
|
||||
end
|
||||
|
||||
-- GitHub-style default
|
||||
return string.format("%s/blob/%s/%s%s", base_repo_url, ref, encoded_path, anchor)
|
||||
end
|
||||
|
||||
--- Open current file on remote at current line / selected range
|
||||
--- @param opts? {line1?: number, line2?: number, range?: number, args?: string, fargs?: string[]}
|
||||
function M.browse(opts)
|
||||
if not utils.ensure_jj() then
|
||||
return
|
||||
end
|
||||
|
||||
local abs_path = vim.api.nvim_buf_get_name(0)
|
||||
if not utils.is_file(abs_path) then
|
||||
utils.notify("Current buffer is not a file", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local root = utils.get_jj_root()
|
||||
if not root then
|
||||
utils.notify("Not in a jj repository", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local repo_rel = utils.relpath(root, abs_path)
|
||||
if not repo_rel then
|
||||
utils.notify("File is not within jj repository root", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
repo_rel = repo_rel:gsub("\\", "/")
|
||||
|
||||
local line1, line2
|
||||
if opts and opts.range and opts.range > 0 and opts.line1 and opts.line2 then
|
||||
line1, line2 = opts.line1, opts.line2
|
||||
else
|
||||
line1 = vim.api.nvim_win_get_cursor(0)[1]
|
||||
line2 = line1
|
||||
end
|
||||
|
||||
local revset = "@"
|
||||
if opts then
|
||||
if type(opts.args) == "string" and opts.args ~= "" then
|
||||
revset = vim.trim(opts.args)
|
||||
elseif type(opts.fargs) == "table" and #opts.fargs > 0 then
|
||||
revset = vim.trim(opts.fargs[1] or "")
|
||||
end
|
||||
end
|
||||
|
||||
local remotes = utils.get_remotes()
|
||||
if remotes == nil then
|
||||
utils.notify("Failed to get git remotes", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
if #remotes == 0 then
|
||||
utils.notify("No git remotes found", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local function browse_with_remote(remote)
|
||||
local base_repo_url, host = utils.normalize_remote_url(remote.url)
|
||||
if not base_repo_url or not host then
|
||||
utils.notify("Unsupported remote URL: " .. (remote.url or ""), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- If a revset has been given the walkback is none since the user doesn't expect any walkback
|
||||
local max_walkback = 20
|
||||
if revset ~= "@" then
|
||||
max_walkback = 0
|
||||
end
|
||||
|
||||
local commit_id = utils.get_pushed_commit_id(revset, remote.name, max_walkback)
|
||||
if not commit_id then
|
||||
utils.notify(
|
||||
string.format("Could not determine a remote reachable commit for %s", revset),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
-- Prefer a single remote bookmark name (more readable). If there are multiple,
|
||||
-- stick to commit SHA to avoid ambiguity.
|
||||
local ref = utils.get_unique_remote_bookmark_name(commit_id, remote.name) or commit_id
|
||||
|
||||
local url = build_browse_url(base_repo_url, host, ref, repo_rel, line1, line2)
|
||||
utils.open_url(url)
|
||||
utils.notify("Opening in browser", vim.log.levels.INFO, 1000)
|
||||
end
|
||||
|
||||
if #remotes == 1 then
|
||||
browse_with_remote(remotes[1])
|
||||
return
|
||||
end
|
||||
|
||||
vim.ui.select(remotes, {
|
||||
prompt = "Select remote to browse: ",
|
||||
format_item = function(item)
|
||||
return string.format("%s (%s)", item.name, item.url)
|
||||
end,
|
||||
}, function(choice)
|
||||
if choice then
|
||||
browse_with_remote(choice)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function M.register_command()
|
||||
vim.api.nvim_create_user_command("Jbrowse", function(cmdopts)
|
||||
M.browse(cmdopts)
|
||||
end, {
|
||||
nargs = "?",
|
||||
range = true,
|
||||
desc = "Open current file on remote (optionally pass revset; supports visual line range)",
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user