current commit
261101.1 · fix(git): fix fish preview quoting for special char paths
local core = require "fzf-lua.core"
local path = require "fzf-lua.path"
local libuv = require "fzf-lua.libuv"
local utils = require "fzf-lua.utils"
local config = require "fzf-lua.config"
local shell = require "fzf-lua.shell"

local M = {}

local function set_git_cwd_args(opts)
  -- verify cwd is a git repo, override user supplied
  -- cwd if cwd isn't a git repo, error was already
  -- printed to `:messages` by 'path.git_root'
  local git_root = path.git_root(opts)
  if not opts.cwd or not git_root then
    opts.cwd = git_root
  end
  if opts.git_dir or opts.git_worktree then
    opts.cmd = path.git_cwd(opts.cmd, opts)
  end
  return opts
end

---@param opts fzf-lua.config.GitFiles|{}?
---@return thread?, string?, table?
M.files = function(opts)
  ---@type fzf-lua.config.GitFiles
  opts = config.normalize_opts(opts, "git.files")
  if not opts then return end
  opts = set_git_cwd_args(opts)
  if not opts.cwd then return end
  return core.fzf_exec(opts.cmd, opts)
end

---@param opts fzf-lua.config.GitStatus|{}?
---@return thread?, string?, table?
M.status = function(opts)
  ---@type fzf-lua.config.GitStatus
  opts = config.normalize_opts(opts, "git.status")
  if not opts then return end
  opts = set_git_cwd_args(opts)
  if not opts.cwd then return end
  if opts.preview then
    opts.preview = path.git_cwd(opts.preview --[[@as string]], opts)
  end
  -- we don't need git icons since we get them
  -- as part of our `git status -s`
  opts.git_icons = false

  -- git status does not require preprocessing if not loading devicons
  -- opts.fn_preprocess = opts.file_icons
  --     and [[return require("fzf-lua.devicons").load()]]
  --     or [[return true]]
  --

  opts.header_prefix = opts.header_prefix or "+ -  "
  opts.header_separator = opts.header_separator or "|"

  return core.fzf_exec(opts.cmd, opts)
end

local function git_cmd(opts)
  opts = set_git_cwd_args(opts)
  if not opts.cwd then return end
  return core.fzf_exec(opts.cmd, opts)
end

local function git_preview(opts, file)
  if type(opts.preview) ~= "string" then return end
  if file then
    opts.preview = opts.preview:gsub("[<{]file[}>]", file)
  end
  -- Normalize the cwd passed to git_preview() so that it is the closest
  -- directory up the path that is a Git repo. git_diff() passes file
  -- paths relative to the Git repo root so to ensure the previewer
  -- can interpret the paths correctly, it must use the repo root as cwd.
  opts.cwd = path.git_root(opts)
  opts.preview = path.git_cwd(opts.preview, opts)
  if type(opts.preview_pager) == "function" then
    opts.preview_pager = opts.preview_pager(opts)
  end
  if opts.preview_pager then
    opts.preview = string.format("%s | %s", opts.preview,
      utils._if_win_normalize_vars(opts.preview_pager))
  end
  return opts.preview
end

---@param opts fzf-lua.config.GitBase|{}?
---@param ref string
---@return boolean
local git_validate_ref = function(opts, ref)
  local cmd = path.git_cwd({ "git", "rev-parse", "--verify", ref }, opts --[[@as table]])
  local _, exit_code = utils.io_systemlist(cmd --[[@as string[] ]])
  if exit_code ~= 0 then
    utils.warn("Invalid git ref %s", ref)
    return false
  end
  return true
end

---@param opts fzf-lua.config.GitDiff|fzf-lua.config.GitHunks
---@return table?
local normalize_diff_opts = function(opts)
  -- Backward compat `compare_against` -> `ref1`
  ---@diagnostic disable-next-line: undefined-field
  opts.ref1 = opts.compare_against or opts.ref1
  -- Convinience: ref as string array
  if type(opts.ref) == "table" then
    opts.ref, opts.ref1 = opts.ref[1], (opts.ref[2] or opts.ref1)
  end
  -- Ensure supplied refs are valid in this git repository.
  for _, r in ipairs({ "ref", "ref1" }) do
    if type(opts[r]) == "string" and #opts[r] > 0 and not git_validate_ref(opts, opts[r]) then
      return
    end
  end
  -- If no ref was supplied default to last commit, otherwise compare against the index
  if not opts.ref and not opts.ref1 then
    local cmd = path.git_cwd(
      { "git", "-c", "color.status=false", "--no-optional-locks", "status", "--porcelain=v1" },
      opts --[[@as table]])
    local out, exit_code = utils.io_systemlist(cmd --[[@as string[] ]])
    if exit_code == 0 and #out == 0 then
      opts.ref = "HEAD^"
    else
      opts.ref = "HEAD"
    end
  end
  opts.cmd = opts.cmd:gsub("[<{]ref[}>]", opts.ref or "")
  opts.cmd = opts.cmd:gsub("[<{]ref1[}>]", opts.ref1 or "")
  opts.cmd = opts.cmd:gsub("[<{]file[}>]", opts.file and libuv.shellescape(opts.file) or "")
  if type(opts.preview) == "string" then
    opts.preview = opts.preview:gsub("[<{]ref[}>]", opts.ref or "")
    opts.preview = opts.preview:gsub("[<{]ref1[}>]", opts.ref1 or "")
    opts.preview = git_preview(opts, "{-1}")
  end
  if type(opts._headers) == "table" then
    table.insert(opts._headers, "ref")
    table.insert(opts._headers, "ref1")
  end
  return set_git_cwd_args(opts)
end

---@param opts fzf-lua.config.GitDiff|{}?
---@return thread?, string?, table?
M.diff = function(opts)
  ---@type fzf-lua.config.GitDiff
  opts = config.normalize_opts(opts, "git.diff")
  if not opts then return end
  opts = normalize_diff_opts(opts)
  if not opts or not opts.cwd then return end
  return core.fzf_exec(opts.cmd, opts)
end


---@param opts fzf-lua.config.GitHunks|{}?
---@return thread?, string?, table?
M.hunks = function(opts)
  ---@type fzf-lua.config.GitHunks
  opts = config.normalize_opts(opts, "git.hunks")
  if not opts then return end
  opts = normalize_diff_opts(opts)
  if not opts or not opts.cwd then return end

  -- we don't need git icons since we get them
  -- as part of our `git status -s`
  opts.git_icons = false

  return core.fzf_exec(opts.cmd, opts)
end

---@param opts fzf-lua.config.GitCommits|{}?
---@return thread?, string?, table?
M.commits = function(opts)
  ---@type fzf-lua.config.GitCommits
  opts = config.normalize_opts(opts, "git.commits")
  if not opts then return end
  opts.preview = git_preview(opts)
  return git_cmd(opts)
end

---@param opts fzf-lua.config.GitReflog|{}?
---@return thread?, string?, table?
M.reflog = function(opts)
  ---@type fzf-lua.config.GitReflog
  opts = config.normalize_opts(opts, "git.reflog")
  if not opts then return end
  opts.preview = git_preview(opts)
  return git_cmd(opts)
end

---@param opts fzf-lua.config.GitBcommits|{}?
---@return thread?, string?, table?
M.bcommits = function(opts)
  ---@type fzf-lua.config.GitBcommits
  opts = config.normalize_opts(opts, "git.bcommits")
  if not opts then return end
  local bufname = vim.api.nvim_buf_get_name(0)
  if #bufname == 0 then
    utils.info("'bcommits' is not available for unnamed buffers.")
    return
  end
  -- if caller did not specify cwd, we attempt to auto detect the
  -- file's git repo from its parent folder. Could be further
  -- optimized to prevent the duplicate call to `git rev-parse`
  -- but overall it's not a big deal as it's a pretty cheap call
  -- first 'git_root' call won't print a warning to ':messages'
  if not opts.cwd and not opts.git_dir then
    opts.cwd = path.git_root({ cwd = vim.fn.expand("%:p:h") --[[@as string]] }, true)
  end
  local git_root = path.git_root(opts)
  if not git_root then return end
  -- `follow`: list (and preview) the buffer's history across renames. We resolve
  -- the filename the buffer had at each commit via `git log --follow --name-only`
  -- (which prints the per-commit name with no rename-chain bookkeeping), embedding
  -- it as a hidden fzf field so the preview/actions reference the correct path for
  -- commits where the file was named differently.
  if opts.follow and not utils.mode_is_visual() then
    local display_fmt = opts.cmd:match('%-%-pretty=format:"(.-)"') or "%C(yellow)%h%Creset %s"
    local relfile = path.relative_to(vim.fn.expand("%:p") --[[@as string]], git_root)
    -- \1 marks a header line; \31 separates the clean sha from the coloured display
    local logcmd = path.git_cwd({
      "git", "-c", "core.quotepath=false",
      "log", "--follow", "--name-only", "--color=always",
      "--pretty=format:%x01%h%x1f" .. display_fmt, "--", relfile,
    }, opts)
    local entries, sha, disp = {}, nil, nil
    for _, line in ipairs(utils.io_systemlist(logcmd)) do
      local s, d = line:match("^\1([^\31]*)\31(.*)$")
      if s then
        sha, disp = s, d
      elseif sha and #line > 0 then
        local fname = line:gsub("\27%[[%d;]*m", "") -- defensive ANSI strip
        entries[#entries + 1] = string.format("%s\t%s\t%s", sha, fname, disp)
        sha = nil
      end
    end
    opts.fzf_opts = opts.fzf_opts or {}
    if opts.fzf_opts["--delimiter"] == nil then opts.fzf_opts["--delimiter"] = "\t" end
    if opts.fzf_opts["--with-nth"] == nil then opts.fzf_opts["--with-nth"] = "3.." end
    opts.preview = "git show --color {1} -- {2}"
    opts.preview = git_preview(opts)
    ---@class fzf-lua.config.GitBcommitsFollow: fzf-lua.config.GitBcommits
    ---@field fn_match_commit_hash? fun(line: string, opts: table): string?
    ---@field fn_match_file? fun(line: string, opts: table): string?
    ---@cast opts fzf-lua.config.GitBcommitsFollow
    opts.fn_match_commit_hash = opts.fn_match_commit_hash
        or function(l) return l:match("^[^\t]+") end
    opts.fn_match_file = opts.fn_match_file
        or function(l) return (l:match("^[^\t]+\t([^\t]+)")) end
    return core.fzf_exec(entries, opts)
  end
  local file = libuv.shellescape(path.relative_to(vim.fn.expand("%:p") --[[@as string]], git_root))
  local range
  if utils.mode_is_visual() then
    local _, sel = utils.get_visual_selection()
    if not sel then return end
    range = string.format("-L %d,%d:%s --no-patch", sel.start.line, sel["end"].line, file)
    -- when range is specified remove "end of options" marker (#2375)
    opts.cmd = opts.cmd:gsub("%s+%-%-%s-$", " "):gsub("%-%-%s+[<{]file[}>]", " {file}")
  end
  if opts.cmd:match("[<{]file") then
    opts.cmd = opts.cmd:gsub("[<{]file[}>]", range or file)
  else
    opts.cmd = opts.cmd .. " " .. (range or file)
  end
  opts.preview = git_preview(opts, file)
  return git_cmd(opts)
end

---@param opts fzf-lua.config.GitBlame|{}?
---@return thread?, string?, table?
M.blame = function(opts)
  ---@type fzf-lua.config.GitBlame
  opts = config.normalize_opts(opts, "git.blame")
  if not opts then return end
  local bufname = vim.api.nvim_buf_get_name(0)
  if #bufname == 0 then
    utils.info("'blame' is not available for unnamed buffers.")
    return
  end
  -- See "bcommits" for comment
  if not opts.cwd and not opts.git_dir then
    opts.cwd = path.git_root({ cwd = vim.fn.expand("%:p:h") --[[@as string]] }, true)
  end
  local git_root = path.git_root(opts)
  if not git_root then return end
  local file = libuv.shellescape(path.relative_to(vim.fn.expand("%:p") --[[@as string]], git_root))
  local range
  if utils.mode_is_visual() then
    local _, sel = utils.get_visual_selection()
    if not sel then return end
    range = string.format("-L %d,%d %s", sel.start.line, sel["end"].line, file)
  end
  if opts.cmd:match("[<{]file") then
    opts.cmd = opts.cmd:gsub("[<{]file[}>]", range or file)
  else
    opts.cmd = opts.cmd .. " " .. (range or file)
  end
  opts.preview = git_preview(opts, file)
  return git_cmd(opts)
end

---@param line string
---@return string
local function highlight_branch_line(line)
  local u = FzfLua.utils
  local ansi = u.ansi_codes
  line = u.strip_ansi_coloring(line)
  local leader = line:sub(1, 2)
  local rest = line:sub(3)

  local detached_inner = rest:match("^%(([^)]+)%)")
  if detached_inner and
      (detached_inner:match("^HEAD detached") or detached_inner:match("^no branch")) then
    local head = ansi.grey("(") .. ansi.green(detached_inner) .. ansi.grey(")")
    local after = rest:sub(#detached_inner + 3)
    local ws, sha, subject_ws, subject = after:match("^(%s+)(%x%x%x%x%x%x%x+)(%s+)(.*)$")
    if ws and sha then
      return leader .. head .. ws .. ansi.yellow(sha) .. subject_ws .. subject
    end
    return leader .. head .. after
  end

  local branch_name, ws_after_name, after = rest:match("^(%S+)(%s+)(.*)$")
  if not branch_name then return line end
  ---@cast after string

  local colored_branch
  if leader:sub(1, 1) == "*" then
    colored_branch = ansi.green(branch_name)
  elseif leader:sub(1, 1) == "+" then
    colored_branch = ansi.cyan(branch_name)
  elseif branch_name:match("^remotes/") then
    colored_branch = ansi.red(branch_name)
  else
    colored_branch = branch_name
  end

  if after:match("^%->") then
    return leader .. colored_branch .. ws_after_name .. after
  end

  local sha, body_ws, body = after:match("^(%x%x%x%x%x%x%x+)(%s+)(.*)$")
  if not sha then
    return leader .. colored_branch .. ws_after_name .. after
  end
  ---@cast body string

  local function color_tracking(content)
    local ref, suffix = content:match("^([^:]+)(:.*)$")
    if ref and suffix then
      return ansi.grey("[") .. ansi.blue(ref) .. ansi.grey(suffix) .. ansi.grey("]")
    end
    return ansi.grey("[") .. ansi.blue(content) .. ansi.grey("]")
  end

  if leader:sub(1, 1) == "+" then
    local wt_paren, remainder = body:match("^(%b())(.*)$")
    if wt_paren then
      ---@cast remainder string
      local colored_wt = ansi.grey("(") .. ansi.cyan(wt_paren:sub(2, -2)) .. ansi.grey(")")
      remainder = remainder:gsub("^(%s*)(%[)([^%]]+)(%])", function(ws, _, content, _)
        return ws .. color_tracking(content)
      end)
      body = colored_wt .. remainder
    end
  else
    body = body:gsub("^(%[)([^%]]+)(%])", function(_, content, _)
      return color_tracking(content)
    end)
  end

  return leader .. colored_branch .. ws_after_name .. ansi.yellow(sha) .. body_ws .. body
end

---@param line string
---@return string
local function highlight_worktree_line(line)
  local ansi = FzfLua.utils.ansi_codes
  local path_s, padding, rest = line:match("^(%S+)(%s+)(.*)$")
  if not path_s then return line end
  ---@cast rest string

  local sha, sha_ws, body = rest:match("^(%x%x%x%x%x%x%x+)(%s+)(.*)$")
  if not sha then
    body = rest
  end
  ---@cast body string

  body = body:gsub("^(%[)([^%]]+)(%])", function(lb, name, rb)
    return ansi.grey(lb) .. ansi.green(name) .. ansi.grey(rb)
  end)
  body = body:gsub("^(%()(detached HEAD)(%))", function(lp, text, rp)
    return ansi.grey(lp) .. ansi.green(text) .. ansi.grey(rp)
  end)
  body = body:gsub("^(%()(bare)(%))", function(lp, text, rp)
    return ansi.grey(lp) .. ansi.grey(text) .. ansi.grey(rp)
  end)
  body = body:gsub("(%s)(locked)(%s*)$", function(s1, w, s2)
    return s1 .. ansi.magenta(w) .. s2
  end)
  body = body:gsub("(%s)(prunable)(%s*)$", function(s1, w, s2)
    return s1 .. ansi.red(w) .. s2
  end)

  local out = ansi.blue(path_s) .. padding
  if sha then
    out = out .. ansi.yellow(sha) .. sha_ws
  end
  return out .. body
end

---@param opts fzf-lua.config.GitBranches|{}?
---@return thread?, string?, table?
M.branches = function(opts)
  ---@type fzf-lua.config.GitBranches
  opts = config.normalize_opts(opts, "git.branches")
  if not opts then return end
  if opts.fn_transform == nil then opts.fn_transform = highlight_branch_line end
  if opts.preview then
    local preview = path.git_cwd(opts.preview, opts) --[[@as string]]
    opts.preview = shell.stringify_cmd(function(items)
      -- The beginning of the selected line looks like the below,
      -- but we only want the string containing the branch name,
      -- so match the first sequence not including spaces:
      --   branch
      -- * branch
      --   remotes/origin/branch
      --   (HEAD detached at origin/branch)
      if not items[1] then return utils.shell_nop() end
      -- git bisect detached preview (#2441)
      if items[1]:match("%(no branch, bisect") then
        items[1] = items[1]:gsub("%(no.-%)", " ")
      end
      local branch = assert(items[1]:match("^[%*+]*[%s]*[(]?([^%s)]+)"))
      return (preview:gsub("{.*}", branch))
    end, opts, "{}")
  end
  return git_cmd(opts)
end

---@param opts fzf-lua.config.GitWorktrees|{}?
---@return thread?, string?, table?
M.worktrees = function(opts)
  ---@type fzf-lua.config.GitWorktrees
  opts = config.normalize_opts(opts, "git.worktrees")
  if not opts then return end
  if opts.fn_transform == nil then opts.fn_transform = highlight_worktree_line end
  if opts.preview then
    local preview_cmd = opts.preview
    opts.preview = shell.stringify_cmd(function(items)
      if not items[1] then return utils.shell_nop() end
      local cwd = items[1]:match("^[^%s]+")
      ---@cast preview_cmd string
      local cmd = path.git_cwd(preview_cmd, { cwd = cwd })
      return cmd
    end, opts, "{}")
  end
  return git_cmd(opts)
end

---@param opts fzf-lua.config.GitTags|{}?
---@return thread?, string?, table?
M.tags = function(opts)
  ---@type fzf-lua.config.GitTags
  opts = config.normalize_opts(opts, "git.tags")
  if not opts then return end
  return git_cmd(opts)
end

---@param opts fzf-lua.config.GitStash|{}?
---@return thread?, string?, table?
M.stash = function(opts)
  ---@type fzf-lua.config.GitStash
  opts = config.normalize_opts(opts, "git.stash")
  if not opts then return end
  opts = set_git_cwd_args(opts)
  if not opts.cwd then return end

  opts.preview = git_preview(opts)

  if opts.search and opts.search ~= "" then
    -- search by stash content, git stash -G<regex>
    assert(type(opts.search) == "string")
    opts.cmd = opts.cmd .. " -G " .. libuv.shellescape(opts.search)
  end

  opts.fn_transform = function(x)
    local stash, rest = x:match("([^:]+)(.*)")
    if stash then
      stash = FzfLua.utils.ansi_codes.yellow(stash)
      stash = stash:gsub("{%d+}", function(s)
        return ("%s"):format(FzfLua.utils.ansi_codes.green(tostring(s)))
      end)
    end
    return (not stash or not rest) and x or stash .. rest
  end

  return core.fzf_exec(opts.cmd, opts)
end

return M