mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-05 10:46:08 +00:00
feat(browse): add :Jbrowse to open current file on remote (#89)
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
- [Split changes from the log buffer](#split-changes-from-the-log-buffer)
|
||||
- [Rebase changes from the log buffer](#rebase-changes-from-the-log-buffer)
|
||||
- [Open a PR/MR from the log buffer](#open-a-prmr-from-the-log-buffer)
|
||||
- [Browse current file on remote](#browse-current-file-on-remote)
|
||||
- [Open a changed file](#open-a-changed-file)
|
||||
- [Restore a changed file](#restore-a-changed-file)
|
||||
- [Installation](#installation)
|
||||
@@ -69,6 +70,7 @@
|
||||
- `undo` - Undo the last operation
|
||||
- `redo` - Redo the last undone operation
|
||||
- `open_pr` - Open a PR/MR on your remote (GitHub, GitLab, Gitea, Forgejo, etc.)
|
||||
- `browse` - Open the current file on your remote at the current line (or selected range)
|
||||
- `annotate` / `annotate_line` - View file blame and line history with change ID, author, and timestamp
|
||||
- `commit` - Describe the current change and create a new one after
|
||||
- Diff commands
|
||||
@@ -265,6 +267,32 @@ The plugin automatically:
|
||||
|
||||
**This is a jj.nvim exclusive feature** - the ability to seamlessly bridge from your Neovim jj workflow directly to your remote platform's PR/MR interface.
|
||||
|
||||
### Browse current file on remote
|
||||
|
||||
Open the current buffer's file in your browser on the hosted remote (GitHub/GitLab/Gitea/Forgejo, etc.) at the current cursor line or a visually selected range.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```sh
|
||||
:Jbrowse " Use @ (best-effort chooses a remote-reachable ref)
|
||||
:Jbrowse main " Use an explicit revset (no walkback)
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
|
||||
- Takes the current buffer path and makes it repo-relative (must be inside a jj repo)
|
||||
- Collects git remotes; if there's more than one, prompts you to pick one
|
||||
- Normalizes the remote URL to an HTTPS base repo URL
|
||||
- Picks a ref that is expected to exist on the remote:
|
||||
- With default `@`: walks back first-parent up to 20 parents to find a commit reachable from that remote's bookmarks; falls back to `trunk()` if needed
|
||||
- With an explicit revset (e.g. `main`, `@-2`): uses that revset directly (no walkback)
|
||||
- If there's a single unambiguous remote bookmark pointing at the chosen commit, uses that bookmark name; otherwise uses the commit SHA
|
||||
- Builds a provider-specific URL and adds a line anchor:
|
||||
- GitHub-style: `#L<start>` or `#L<start>-L<end>`
|
||||
- GitLab-style: `#L<start>` or `#L<start>-<end>`
|
||||
|
||||
In Visual mode, select lines and run `:Jbrowse` to open a range.
|
||||
|
||||
### Open a changed file
|
||||
|
||||
Just press enter to open a file from the `status` output in your current window.
|
||||
@@ -308,6 +336,8 @@ The plugin provides a `:J` command that accepts jj subcommands:
|
||||
:J fetch " Fetch from remote
|
||||
:J open_pr " Open PR for current change's bookmark
|
||||
:J open_pr --list " Select bookmark from all and open PR
|
||||
:Jbrowse " Open current file on remote at cursor line
|
||||
:Jbrowse main " Open current file on remote at the given revset
|
||||
:J split " Split a change interactively
|
||||
:J bookmark create/move/delete
|
||||
:J tag set " Set a tag (prompts for revision and tag name)
|
||||
|
||||
@@ -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
|
||||
@@ -44,6 +44,7 @@ local function describe_editor_keymaps()
|
||||
close = {
|
||||
desc = "Close describe editor without saving",
|
||||
handler = "<cmd>close!<CR>",
|
||||
modes = { "n" },
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
+1
-16
@@ -7,7 +7,6 @@ local terminal = require("jj.ui.terminal")
|
||||
local editor = require("jj.ui.editor")
|
||||
local parser = require("jj.core.parser")
|
||||
|
||||
local diff = require("jj.diff")
|
||||
local log_module = require("jj.cmd.log")
|
||||
local describe_module = require("jj.cmd.describe")
|
||||
local status_module = require("jj.cmd.status")
|
||||
@@ -805,6 +804,7 @@ function M.commit(description)
|
||||
close = {
|
||||
desc = "Close commit editor without saving",
|
||||
handler = "<cmd>close!<CR>",
|
||||
modes = { "n" },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1223,21 +1223,6 @@ function M.register_command()
|
||||
end,
|
||||
desc = "Execute jj commands with subcommand support",
|
||||
})
|
||||
|
||||
local function create_diff_command(name, fn, desc)
|
||||
vim.api.nvim_create_user_command(name, function(opts)
|
||||
local rev = opts.fargs[1]
|
||||
if rev then
|
||||
fn({ rev = rev })
|
||||
else
|
||||
fn()
|
||||
end
|
||||
end, { nargs = "?", desc = desc .. " (optionally pass jj revision)" })
|
||||
end
|
||||
|
||||
create_diff_command("Jdiff", diff.open_vdiff, "Vertical diff against jj revision")
|
||||
create_diff_command("Jhdiff", diff.open_hdiff, "Horizontal diff against jj revision")
|
||||
create_diff_command("Jvdiff", diff.open_vdiff, "Vertical diff against jj revision")
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -160,5 +160,23 @@ function M.open_hdiff(opts)
|
||||
M.diff_current(vim.tbl_extend("force", { layout = "horizontal" }, { rev = opts and opts.rev }))
|
||||
end
|
||||
|
||||
-- Register the different diff commands
|
||||
function M.register_command()
|
||||
local function create_diff_command(name, fn, desc)
|
||||
vim.api.nvim_create_user_command(name, function(opts)
|
||||
local rev = opts.fargs[1]
|
||||
if rev then
|
||||
fn({ rev = rev })
|
||||
else
|
||||
fn()
|
||||
end
|
||||
end, { nargs = "?", desc = desc .. " (optionally pass jj revision)" })
|
||||
end
|
||||
|
||||
create_diff_command("Jdiff", M.open_vdiff, "Vertical diff against jj revision")
|
||||
create_diff_command("Jhdiff", M.open_hdiff, "Horizontal diff against jj revision")
|
||||
create_diff_command("Jvdiff", M.open_vdiff, "Vertical diff against jj revision")
|
||||
end
|
||||
|
||||
---@return jj.diff
|
||||
return M
|
||||
|
||||
@@ -4,6 +4,7 @@ local picker = require("jj.picker")
|
||||
local editor = require("jj.ui.editor")
|
||||
local terminal = require("jj.ui.terminal")
|
||||
local diff = require("jj.diff")
|
||||
local browse = require("jj.browse")
|
||||
|
||||
--- Jujutsu plugin configuration
|
||||
--- @class jj.Config
|
||||
@@ -49,7 +50,10 @@ function M.setup(opts)
|
||||
terminal.setup(opts and opts.terminal or {})
|
||||
diff.setup(M.config.diff)
|
||||
|
||||
-- Register the commands form the different modules
|
||||
cmd.register_command()
|
||||
browse.register_command()
|
||||
diff.register_command()
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
+245
-26
@@ -124,6 +124,153 @@ function M.url_encode(str)
|
||||
end))
|
||||
end
|
||||
|
||||
--- URL-encode a file path for use in URLs.
|
||||
--- Encodes each path segment and preserves `/` separators.
|
||||
--- Also normalizes Windows path separators (`\` -> `/`).
|
||||
--- @param path string
|
||||
--- @return string
|
||||
function M.url_encode_path(path)
|
||||
path = (path or ""):gsub("\\", "/")
|
||||
local parts = vim.split(path, "/", { plain = true })
|
||||
for i, seg in ipairs(parts) do
|
||||
parts[i] = M.url_encode(seg)
|
||||
end
|
||||
return table.concat(parts, "/")
|
||||
end
|
||||
|
||||
--- Check whether a path points to an existing file.
|
||||
--- @param path string|nil
|
||||
--- @return boolean
|
||||
function M.is_file(path)
|
||||
if not path or path == "" then
|
||||
return false
|
||||
end
|
||||
local st = vim.uv.fs_stat(path)
|
||||
return st ~= nil and st.type == "file"
|
||||
end
|
||||
|
||||
--- Compute a repository-relative path.
|
||||
--- Returns nil if `path` is outside of `root`.
|
||||
--- @param root string
|
||||
--- @param path string
|
||||
--- @return string|nil
|
||||
function M.relpath(root, path)
|
||||
if not root or root == "" or not path or path == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local ok_norm_root, norm_root = pcall(vim.fs.normalize, root)
|
||||
local ok_norm_path, norm_path = pcall(vim.fs.normalize, path)
|
||||
if not ok_norm_root or not ok_norm_path then
|
||||
norm_root, norm_path = root, path
|
||||
end
|
||||
|
||||
local ok_rel, rel = pcall(vim.fs.relpath, norm_root, norm_path)
|
||||
if ok_rel and rel and rel ~= "" and not rel:match("^%.%.") then
|
||||
return rel
|
||||
end
|
||||
|
||||
local prefix = norm_root
|
||||
if not prefix:match("/$") then
|
||||
prefix = prefix .. "/"
|
||||
end
|
||||
if vim.startswith(norm_path, prefix) then
|
||||
return norm_path:sub(#prefix + 1)
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Normalize a git remote URL to an HTTPS repo URL.
|
||||
--- Supports:
|
||||
--- - `git@host:owner/repo(.git)`
|
||||
--- - `ssh://git@host/owner/repo(.git)`
|
||||
--- - `https://host/owner/repo(.git)`
|
||||
--- @param raw_url string
|
||||
--- @return string|nil repo_url HTTPS base repo URL (no trailing .git)
|
||||
--- @return string|nil host Hostname extracted from the remote
|
||||
function M.normalize_remote_url(raw_url)
|
||||
if not raw_url or raw_url == "" then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
raw_url = raw_url:gsub("%.git$", "")
|
||||
|
||||
-- scp-like SSH: git@host:owner/repo
|
||||
if raw_url:match("^git@") then
|
||||
local host = raw_url:match("^git@([^:]+):")
|
||||
local repo_path = raw_url:match("^git@[^:]+:(.+)$")
|
||||
if host and repo_path then
|
||||
return "https://" .. host .. "/" .. repo_path, host
|
||||
end
|
||||
end
|
||||
|
||||
-- ssh://git@host/owner/repo
|
||||
if raw_url:match("^ssh://") then
|
||||
local host = raw_url:match("^ssh://[^@]+@([^/]+)/") or raw_url:match("^ssh://([^/]+)/")
|
||||
local rest = raw_url:gsub("^ssh://[^@]+@", ""):gsub("^ssh://", "")
|
||||
local repo_path = rest:match("^[^/]+/(.+)$")
|
||||
if host and repo_path then
|
||||
repo_path = repo_path:gsub("%.git$", "")
|
||||
return "https://" .. host .. "/" .. repo_path, host
|
||||
end
|
||||
end
|
||||
|
||||
-- HTTPS
|
||||
local host = raw_url:match("^https?://([^/]+)")
|
||||
if host then
|
||||
return raw_url, host
|
||||
end
|
||||
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
--- Open a URL using the system default handler.
|
||||
--- Prefers `vim.ui.open()` when available.
|
||||
--- @param url string
|
||||
function M.open_url(url)
|
||||
if not url or url == "" then
|
||||
return
|
||||
end
|
||||
|
||||
if vim.ui and type(vim.ui.open) == "function" then
|
||||
vim.ui.open(url)
|
||||
return
|
||||
end
|
||||
|
||||
if vim.fn.has("win32") == 1 then
|
||||
-- `start` is a cmd.exe builtin
|
||||
vim.fn.jobstart({ "cmd.exe", "/c", "start", "", url }, { detach = true })
|
||||
return
|
||||
end
|
||||
|
||||
local open_cmd = vim.fn.has("mac") == 1 and "open" or "xdg-open"
|
||||
vim.fn.jobstart({ open_cmd, url }, { detach = true })
|
||||
end
|
||||
|
||||
--- Best-effort unquoting of a jj `RefSymbol` rendering.
|
||||
---
|
||||
--- `RefSymbol` values are displayed as revset symbols, which may be quoted and
|
||||
--- escaped if necessary. For browser URLs, we generally want the raw name.
|
||||
---
|
||||
--- This handles the common cases of surrounding single/double quotes.
|
||||
--- @param s string|nil
|
||||
--- @return string
|
||||
function M.unquote_refsymbol(s)
|
||||
s = vim.trim(s or "")
|
||||
if s == "" then
|
||||
return ""
|
||||
end
|
||||
local first = s:sub(1, 1)
|
||||
local last = s:sub(-1)
|
||||
if (first == '"' and last == '"') or (first == "'" and last == "'") then
|
||||
s = s:sub(2, -2)
|
||||
-- Minimal unescaping (covers typical `\"` and `\\` sequences)
|
||||
s = s:gsub("\\\\", "\\"):gsub('\\"', '"'):gsub("\\'", "'")
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
--- Get all bookmarks in the repository, filters out deleted bookmarks
|
||||
--- @return string[] List of bookmarks, or empty list if none found
|
||||
function M.get_all_bookmarks()
|
||||
@@ -264,27 +411,17 @@ function M.open_pr_for_bookmark(bookmark)
|
||||
-- Get all git remotes
|
||||
local remotes = M.get_remotes()
|
||||
|
||||
if #remotes == 0 then
|
||||
if not remotes or #remotes == 0 then
|
||||
M.notify("No git remotes found", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Helper function to open PR for a given remote URL
|
||||
local function open_pr_with_url(raw_url)
|
||||
-- Remove .git suffix if present
|
||||
raw_url = raw_url:gsub("%.git$", "")
|
||||
|
||||
-- Convert SSH URL to HTTPS and detect platform
|
||||
local repo_url, host
|
||||
if raw_url:match("^git@") then
|
||||
-- Extract host and path from git@host:path
|
||||
host = raw_url:match("^git@([^:]+):")
|
||||
local repo_path = raw_url:match("^git@[^:]+:(.+)$")
|
||||
repo_url = "https://" .. host .. "/" .. repo_path
|
||||
else
|
||||
-- Extract host from https://host/path
|
||||
host = raw_url:match("https?://([^/]+)")
|
||||
repo_url = raw_url
|
||||
local repo_url, host = M.normalize_remote_url(raw_url)
|
||||
if not repo_url or not host then
|
||||
M.notify("Unsupported remote URL: " .. (raw_url or ""), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Construct the appropriate PR/MR URL based on the platform
|
||||
@@ -302,17 +439,7 @@ function M.open_pr_for_bookmark(bookmark)
|
||||
pr_url = repo_url .. "/compare/" .. encoded_bookmark .. "?expand=1"
|
||||
end
|
||||
|
||||
-- Open the URL using xdg-open or the system's default browser
|
||||
local open_cmd
|
||||
if vim.fn.has("mac") == 1 then
|
||||
open_cmd = "open"
|
||||
elseif vim.fn.has("win32") == 1 then
|
||||
open_cmd = "start"
|
||||
else
|
||||
open_cmd = "xdg-open"
|
||||
end
|
||||
|
||||
vim.fn.jobstart({ open_cmd, pr_url }, { detach = true })
|
||||
M.open_url(pr_url)
|
||||
M.notify(string.format("Opening PR for bookmark `%s`", bookmark), vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
@@ -452,6 +579,98 @@ function M.get_current_commit_id()
|
||||
return vim.trim(output)
|
||||
end
|
||||
|
||||
--- Return a commit id that is expected to exist on a given remote.
|
||||
---
|
||||
--- This is a best-effort heuristic used for browser URLs: if a commit isn't
|
||||
--- reachable from the remote's bookmarks, most web UIs will 404.
|
||||
---
|
||||
--- Strategy:
|
||||
--- - Starting from `start_revset`, walk back first-parent (`revset-`) up to
|
||||
--- `max_walkback` times until the commit is contained in
|
||||
--- `::remote_bookmarks(remote="<remote_name>")`.
|
||||
--- - If still not found, return the commit id of `trunk()`.
|
||||
---
|
||||
--- @param start_revset string Starting point revset (e.g. "@")
|
||||
--- @param remote_name string Remote name (e.g. "origin")
|
||||
--- @param max_walkback? integer Maximum number of parents to traverse (default: 20)
|
||||
--- @return string|nil commit_id
|
||||
function M.get_pushed_commit_id(start_revset, remote_name, max_walkback)
|
||||
max_walkback = max_walkback or 20
|
||||
start_revset = (start_revset and start_revset ~= "") and start_revset or "@"
|
||||
remote_name = (remote_name and remote_name ~= "") and remote_name or "origin"
|
||||
|
||||
-- Build a revset expression string we can embed in a template StringLiteral.
|
||||
local remote_literal = remote_name:gsub("\\", "\\\\"):gsub('"', '\\"')
|
||||
local templ = [[if(self.contained_in("::remote_bookmarks(remote='%s')"), commit_id)]]
|
||||
templ = string.format(templ, remote_literal)
|
||||
|
||||
-- If `start_revset` is already a commit id (or change id), we still let jj
|
||||
-- resolve it; otherwise, treat it as a revset expression.
|
||||
local current = start_revset
|
||||
for _ = 0, max_walkback do
|
||||
-- Use a single-quoted StringLiteral for the revset, so we can keep the
|
||||
-- remote name quoted inside the revset expression.
|
||||
local cmd = string.format(
|
||||
"jj log -r %s --no-graph --quiet -T %s",
|
||||
vim.fn.shellescape(current),
|
||||
vim.fn.shellescape(templ)
|
||||
)
|
||||
|
||||
local out, ok = runner.execute_command(cmd, "Error determining remote-reachable commit", nil, true)
|
||||
if ok and out and not out:match("^%s*$") then
|
||||
return vim.trim(out)
|
||||
end
|
||||
current = current .. "-"
|
||||
end
|
||||
|
||||
return M.get_commit_id("trunk()")
|
||||
end
|
||||
|
||||
--- Return a remote bookmark name pointing at `revset` on `remote_name`.
|
||||
---
|
||||
--- If there are 0 or multiple matching bookmarks and none of those are neither `main` nor `master`, returns nil.
|
||||
--- This avoids choosing an arbitrary name when multiple bookmarks point to the
|
||||
--- same commit.
|
||||
---
|
||||
--- @param revset string
|
||||
--- @param remote_name string
|
||||
--- @return string|nil bookmark_name
|
||||
function M.get_unique_remote_bookmark_name(revset, remote_name)
|
||||
revset = (revset and revset ~= "") and revset or "@"
|
||||
remote_name = (remote_name and remote_name ~= "") and remote_name or "origin"
|
||||
|
||||
-- Render remote bookmarks as tab-separated pairs: <remote>\t<name>\n
|
||||
local tmpl = [[self.remote_bookmarks().map(|b| b.remote() ++ "\t" ++ b.name() ++ "\n").join("")]]
|
||||
local cmd =
|
||||
string.format("jj log -r %s --no-graph --quiet -T %s", vim.fn.shellescape(revset), vim.fn.shellescape(tmpl))
|
||||
local out, ok = runner.execute_command(cmd, "Error getting remote bookmarks", nil, true)
|
||||
if not ok or not out or out:match("^%s*$") then
|
||||
return nil
|
||||
end
|
||||
|
||||
local matches = {}
|
||||
for line in out:gmatch("[^\r\n]+") do
|
||||
local remote_sym, name_sym = line:match("^(.-)\t(.*)$")
|
||||
local r = M.unquote_refsymbol(remote_sym)
|
||||
local n = M.unquote_refsymbol(name_sym)
|
||||
if r == remote_name and n ~= "" then
|
||||
table.insert(matches, n)
|
||||
end
|
||||
end
|
||||
|
||||
if #matches == 1 then
|
||||
return matches[1]
|
||||
elseif #matches > 1 then
|
||||
for _, name in ipairs(matches) do
|
||||
-- Unless it's main or master which takes precedence
|
||||
if name == "main" or name == "master" then
|
||||
return name
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Extract the description from the describe text
|
||||
--- @param lines string[] The lines of a described change
|
||||
--- @return string|nil
|
||||
|
||||
Reference in New Issue
Block a user