fix(picker.git_branches): handle detached HEAD (#671)

## Description

Handle [detached
HEAD](https://git-scm.com/docs/git-checkout#_detached_head)

- list: display correct information
- preview: without error notification
- action: checkout the commit that HEAD detached at

<!-- Describe the big picture of your changes to communicate to the
maintainers
  why we should accept this pull request. -->

## Related Issue(s)

<!--
  If this PR fixes any issues, please link to the issue here.
  - Fixes #<issue_number>
-->

- Fixes #672 

## Screenshots

<!-- Add screenshots of the changes if applicable. -->

Before:
<img width="1308" alt="image"
src="https://github.com/user-attachments/assets/fad291a3-a730-4a2b-9eb2-4a0edd83d794"
/>

After:
<img width="1038" alt="image"
src="https://github.com/user-attachments/assets/d312579a-e12d-4286-845c-a706d91a6c95"
/>

---------

Co-authored-by: Folke Lemaitre <folke.lemaitre@gmail.com>
This commit is contained in:
Kyle Whliang 2025-01-21 03:03:38 +08:00 committed by GitHub
parent a1c78a2459
commit 390f687431
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 32 additions and 8 deletions

View file

@ -49,6 +49,7 @@ Snacks.util.set_hl({
UndoSaved = "Special",
GitCommit = "@variable.builtin",
GitBreaking = "Error",
GitDetached = "DiagnosticWarn",
GitBranch = "Title",
GitBranchCurrent = "Number",
GitDate = "Special",

View file

@ -138,9 +138,12 @@ function M.git_branch(item, picker)
local ret = {} ---@type snacks.picker.Highlight[]
if item.current then
ret[#ret + 1] = { a("", 2), "SnacksPickerGitBranchCurrent" }
ret[#ret + 1] = { a(item.branch, 30, { truncate = true }), "SnacksPickerGitBranch" }
else
ret[#ret + 1] = { a("", 2) }
end
if item.detached then
ret[#ret + 1] = { a("(detached HEAD)", 30, { truncate = true }), "SnacksPickerGitDetached" }
else
ret[#ret + 1] = { a(item.branch, 30, { truncate = true }), "SnacksPickerGitBranch" }
end
ret[#ret + 1] = { " " }

View file

@ -192,7 +192,7 @@ function M.cmd(cmd, ctx, opts)
if not killed and code ~= 0 then
Snacks.notify.error(
("Terminal **cmd** `%s` failed with code `%d`:\n- `vim.o.shell = %q`\n\nOutput:\n%s"):format(
cmd,
type(cmd) == "table" and table.concat(cmd, " ") or cmd,
code,
vim.o.shell,
vim.trim(table.concat(output, ""))
@ -254,7 +254,7 @@ function M.git_log(ctx)
"--color=never",
"--no-show-signature",
"--no-patch",
ctx.item.branch,
ctx.item.commit,
}
if not native then
table.insert(cmd, 2, "--no-pager")

View file

@ -163,18 +163,38 @@ function M.branches(opts)
local args = { "--no-pager", "branch", "--no-color", "-vvl" }
local cwd = vim.fs.normalize(opts and opts.cwd or uv.cwd() or ".") or nil
cwd = Snacks.git.get_root(cwd)
local pattern_hash = "[a-zA-Z0-9]+"
local patterns = {
-- stylua: ignore start
--- e.g. "* (HEAD detached at f65a2c8) f65a2c8 chore(build): auto-generate docs"
"^(.)%s(%(HEAD detached at " .. pattern_hash .. "%))%s+(" .. pattern_hash .. ")%s*(.*)$",
--- e.g. " main d2b2b7b [origin/main: behind 276] chore(build): auto-generate docs"
"^(.)%s(%S+)%s+(".. pattern_hash .. ")%s*(.*)$",
-- stylua: ignore end
} ---@type string[]
return require("snacks.picker.source.proc").proc(vim.tbl_deep_extend("force", {
cwd = cwd,
cmd = "git",
args = args,
---@param item snacks.picker.finder.Item
transform = function(item)
local status, branch, commit, msg = item.text:match("^(.)%s(%S+)%s+([a-zA-Z0-9]+)%s*(.*)$")
item.cwd = cwd
item.current = status == "*"
item.branch = branch
item.commit = commit
item.msg = msg
for p, pattern in ipairs(patterns) do
local status, branch, commit, msg = item.text:match(pattern)
if status then
local detached = p == 1
item.current = status == "*"
item.branch = not detached and branch or nil
item.commit = commit
item.msg = msg
item.detached = detached
return
end
end
Snacks.notify.warn("failed to parse branch: " .. item.text)
return false -- skip items we could not parse
end,
}, opts or {}))
end