refactor(git/status): rework entries into typed variants on porcelain v2

This commit is contained in:
2026-05-09 22:59:07 +02:00
parent a7932bab5a
commit 067594ef9e
9 changed files with 793 additions and 292 deletions
+9 -10
View File
@@ -651,17 +651,16 @@ end
---@return string[]
local function complete_unstaged_paths(r, lead)
local matches = {}
for path, entry_list in pairs(r.status.entries) do
for path, entry in pairs(r.status.entries) do
if path:sub(1, #lead) == lead then
for _, e in ipairs(entry_list) do
if
e.kind == "unstaged"
or e.kind == "untracked"
or e.kind == "unmerged"
then
table.insert(matches, path)
break
end
local include = entry.kind == "untracked"
or entry.kind == "unmerged"
if not include and entry.kind == "changed" then
---@cast entry ow.Git.Status.ChangedEntry
include = entry.unstaged ~= nil
end
if include then
table.insert(matches, path)
end
end
end
+25 -5
View File
@@ -1,20 +1,40 @@
local HIGHLIGHTS = {
GitDeleted = "Removed",
local DEFAULT_HIGHLIGHTS = {
GitIgnored = "Comment",
GitUnstaged = "Changed",
GitRenamed = "GitStaged",
GitSha = "Identifier",
GitStaged = "Constant",
GitUnmerged = "Todo",
GitUnpulled = "Removed",
GitUnpushed = "Added",
GitUnstaged = "Changed",
GitUntracked = "Added",
GitStagedAdded = "GitStaged",
GitStagedCopied = "GitStaged",
GitStagedDeleted = "GitStaged",
GitStagedModified = "GitStaged",
GitStagedRenamed = "GitStaged",
GitStagedTypeChanged = "GitStaged",
GitUnstagedAdded = "GitUnstaged",
GitUnstagedCopied = "GitUnstaged",
GitUnstagedDeleted = "Removed",
GitUnstagedModified = "GitUnstaged",
GitUnstagedRenamed = "GitStaged",
GitUnstagedTypeChanged = "GitUnstaged",
GitUnmergedAddedByThem = "GitUnmerged",
GitUnmergedAddedByUs = "GitUnmerged",
GitUnmergedBothAdded = "GitUnmerged",
GitUnmergedBothDeleted = "GitUnmerged",
GitUnmergedBothModified = "GitUnmerged",
GitUnmergedDeletedByThem = "GitUnmerged",
GitUnmergedDeletedByUs = "GitUnmerged",
}
local M = {}
function M.init()
for name, link in pairs(HIGHLIGHTS) do
for name, link in pairs(DEFAULT_HIGHLIGHTS) do
vim.api.nvim_set_hl(0, name, { link = link, default = true })
end
+2 -1
View File
@@ -43,10 +43,11 @@ local STATUS_ARGS = {
"-c",
"core.quotePath=false",
"status",
"--porcelain=v1",
"--porcelain=v2",
"--branch",
"--ignored",
"--untracked-files=all",
"-z",
}
---@private
+282 -148
View File
@@ -1,44 +1,207 @@
local M = {}
local UNMERGED = {
DD = true,
AU = true,
UD = true,
UA = true,
DU = true,
AA = true,
UU = true,
}
---@alias ow.Git.Status.EntryKind "untracked"|"unstaged"|"staged"|"unmerged"|"ignored"
---@alias ow.Git.Status.Kind
---| "changed"
---| "unmerged"
---| "untracked"
---| "ignored"
---@class ow.Git.Status.Entry
---@field kind ow.Git.Status.Kind
---@field path string
---@field kind ow.Git.Status.EntryKind
---@field char string
---@field hl string
---@alias ow.Git.Status.Change
---| "modified"
---| "added"
---| "deleted"
---| "renamed"
---| "copied"
---| "type_changed"
---@class ow.Git.Status.ChangedEntry: ow.Git.Status.Entry
---@field kind "changed"
---@field staged ow.Git.Status.Change?
---@field unstaged ow.Git.Status.Change?
---@field orig string?
---@class ow.Git.Status.BranchInfo
---@alias ow.Git.Status.Conflict
---| "both_deleted"
---| "added_by_us"
---| "deleted_by_them"
---| "added_by_them"
---| "deleted_by_us"
---| "both_added"
---| "both_modified"
---@class ow.Git.Status.UnmergedEntry: ow.Git.Status.Entry
---@field kind "unmerged"
---@field conflict ow.Git.Status.Conflict
---@class ow.Git.Status.UntrackedEntry: ow.Git.Status.Entry
---@field kind "untracked"
---@class ow.Git.Status.IgnoredEntry: ow.Git.Status.Entry
---@field kind "ignored"
---@class ow.Git.Status.Mark
---@field char string
---@field hl string
---@alias ow.Git.Status.Section
--- "staged"|"unstaged"|"unmerged"|"untracked"|"ignored"
---@class ow.Git.Status.Row
---@field entry ow.Git.Status.Entry
---@field section ow.Git.Status.Section
---@field side ("staged"|"unstaged")?
---@class ow.Git.Status.Branch
---@field oid string?
---@field head string?
---@field upstream string?
---@field ahead integer
---@field behind integer
---@class ow.Git.Status
---@field branch ow.Git.Status.BranchInfo
---@field entries table<string, ow.Git.Status.Entry[]>
---@field branch ow.Git.Status.Branch
---@field entries table<string, ow.Git.Status.Entry>
local Status = {}
Status.__index = Status
---@param kind ow.Git.Status.EntryKind
---@return ow.Git.Status.Entry[]
function Status:by_kind(kind)
local CHANGE_FROM_CHAR = {
M = "modified",
A = "added",
D = "deleted",
R = "renamed",
C = "copied",
T = "type_changed",
}
local CONFLICT_FROM_XY = {
DD = "both_deleted",
AU = "added_by_us",
UD = "deleted_by_them",
UA = "added_by_them",
DU = "deleted_by_us",
AA = "both_added",
UU = "both_modified",
}
local CHAR_FROM_CHANGE = {
modified = "M",
added = "A",
deleted = "D",
renamed = "R",
copied = "C",
type_changed = "T",
}
---@param s string
---@return string
local function pascal(s)
return (
s:sub(1, 1):upper()
.. s:sub(2):gsub("_(%a)", function(c)
return c:upper()
end)
)
end
---@param path string
---@param staged ow.Git.Status.Change?
---@param unstaged ow.Git.Status.Change?
---@param orig string?
---@return ow.Git.Status.ChangedEntry
local function changed(path, staged, unstaged, orig)
return {
kind = "changed",
path = path,
staged = staged,
unstaged = unstaged,
orig = orig,
}
end
---@param path string
---@param conflict ow.Git.Status.Conflict
---@return ow.Git.Status.UnmergedEntry
local function unmerged(path, conflict)
return { kind = "unmerged", path = path, conflict = conflict }
end
---@param path string
---@return ow.Git.Status.UntrackedEntry
local function untracked(path)
return { kind = "untracked", path = path }
end
---@param path string
---@return ow.Git.Status.IgnoredEntry
local function ignored(path)
return { kind = "ignored", path = path }
end
---@param entry ow.Git.Status.Entry
---@param side ("staged"|"unstaged")?
---@return ow.Git.Status.Mark
function M.mark_for(entry, side)
if entry.kind == "untracked" then
return { char = "?", hl = "GitUntracked" }
end
if entry.kind == "ignored" then
return { char = "i", hl = "GitIgnored" }
end
if entry.kind == "unmerged" then
---@cast entry ow.Git.Status.UnmergedEntry
return { char = "!", hl = "GitUnmerged" .. pascal(entry.conflict) }
end
---@cast entry ow.Git.Status.ChangedEntry
assert(side, "mark_for: side required for changed entry")
local change = side == "staged" and entry.staged or entry.unstaged
assert(change, "mark_for: changed entry has no change on side " .. side)
return {
char = CHAR_FROM_CHANGE[change],
hl = "Git" .. pascal(side) .. pascal(change),
}
end
---@param entry ow.Git.Status.Entry
---@return ow.Git.Status.Mark[]
function M.marks_for(entry)
if entry.kind ~= "changed" then
return { M.mark_for(entry) }
end
---@cast entry ow.Git.Status.ChangedEntry
local out = {}
for _, list in pairs(self.entries) do
for _, e in ipairs(list) do
if e.kind == kind then
table.insert(out, e)
if entry.staged then
table.insert(out, M.mark_for(entry, "staged"))
end
if entry.unstaged then
table.insert(out, M.mark_for(entry, "unstaged"))
end
return out
end
---@param section ow.Git.Status.Section
---@return ow.Git.Status.Row[]
function Status:rows(section)
local out = {}
if section == "staged" or section == "unstaged" then
for _, entry in pairs(self.entries) do
if entry.kind == "changed" then
---@cast entry ow.Git.Status.ChangedEntry
if entry[section] then
table.insert(
out,
{ entry = entry, section = section, side = section }
)
end
end
end
else
for _, entry in pairs(self.entries) do
if entry.kind == section then
table.insert(out, { entry = entry, section = section })
end
end
end
@@ -46,18 +209,18 @@ function Status:by_kind(kind)
end
---@param prefix string
---@return ow.Git.Status.Entry[]
---@return ow.Git.Status.Mark[]
function Status:aggregate_at(prefix)
local match = (prefix == "" or prefix == ".") and "" or prefix .. "/"
local seen = {}
local out = {}
for path, list in pairs(self.entries) do
for path, entry in pairs(self.entries) do
if path == prefix or vim.startswith(path, match) then
for _, e in ipairs(list) do
local key = e.char .. "\0" .. e.hl
for _, mark in ipairs(M.marks_for(entry)) do
local key = mark.char .. "\0" .. mark.hl
if not seen[key] then
seen[key] = true
table.insert(out, e)
table.insert(out, mark)
end
end
end
@@ -68,138 +231,109 @@ function Status:aggregate_at(prefix)
return out
end
---@param line string
---@param branch ow.Git.Status.Branch
local function parse_branch_header(line, branch)
local oid = line:match("^# branch%.oid (.+)$")
if oid then
branch.oid = oid ~= "(initial)" and oid or nil
return
end
local head = line:match("^# branch%.head (.+)$")
if head then
branch.head = head ~= "(detached)" and head or nil
return
end
local up = line:match("^# branch%.upstream (.+)$")
if up then
branch.upstream = up
return
end
local a, b = line:match("^# branch%.ab %+(%d+) %-(%d+)$")
if a and b then
branch.ahead = tonumber(a) --[[@as integer]]
branch.behind = tonumber(b) --[[@as integer]]
end
end
---@param x string
---@return string char, string hl
local function staged_attrs(x)
if x == "R" then
return "R", "GitRenamed"
end
return x, "GitStaged"
end
---@param y string
---@return string char, string hl
local function unstaged_attrs(y)
if y == "R" then
return "R", "GitRenamed"
end
if y == "D" then
return "D", "GitDeleted"
end
return y, "GitUnstaged"
---@return ow.Git.Status.Change?, ow.Git.Status.Change?
local function changes_from_xy(x, y)
local staged = x ~= "." and CHANGE_FROM_CHAR[x] or nil
local unstaged = y ~= "." and CHANGE_FROM_CHAR[y] or nil
return staged, unstaged
end
---@param line string
---@return ow.Git.Status.BranchInfo
local function parse_branch_line(line)
local info = { ahead = 0, behind = 0 }
local content = line:sub(4)
local arrow = content:find("...", 1, true)
if not arrow then
info.head = content
return info
---@param path string
---@return string
local function strip_dir_slash(path)
if path:sub(-1) == "/" then
return path:sub(1, -2)
end
info.head = content:sub(1, arrow - 1)
local rest = content:sub(arrow + 3)
local bracket = rest:find(" %[")
if not bracket then
info.upstream = rest
return info
end
info.upstream = rest:sub(1, bracket - 1)
local inside = rest:match("%[([^%]]+)%]")
if inside then
info.ahead = (tonumber(inside:match("ahead (%d+)")) or 0) --[[@as integer]]
info.behind = (tonumber(inside:match("behind (%d+)")) or 0) --[[@as integer]]
end
return info
end
---@param line string
---@return string x, string y, string path, string? orig
local function parse_status_line(line)
local x = line:sub(1, 1)
local y = line:sub(2, 2)
local rest = line:sub(4)
local orig
if x == "R" or x == "C" or y == "R" or y == "C" then
local arrow = rest:find(" -> ", 1, true)
if arrow then
orig = rest:sub(1, arrow - 1)
rest = rest:sub(arrow + 4)
end
end
return x, y, rest, orig
end
---@param entries table<string, ow.Git.Status.Entry[]>
---@param entry ow.Git.Status.Entry
local function add(entries, entry)
local key = entry.path
if key:sub(-1) == "/" then
key = key:sub(1, -2)
end
local list = entries[key] or {}
table.insert(list, entry)
entries[key] = list
return path
end
---@param stdout string
---@return ow.Git.Status
function M.parse(stdout)
---@type ow.Git.Status.Branch
local branch = { ahead = 0, behind = 0 }
---@type table<string, ow.Git.Status.Entry[]>
---@type table<string, ow.Git.Status.Entry>
local entries = {}
for line in stdout:gmatch("[^\r\n]+") do
if line:sub(1, 2) == "##" then
branch = parse_branch_line(line)
else
local x, y, path, orig = parse_status_line(line)
if x == "?" and y == "?" then
add(entries, {
path = path,
kind = "untracked",
char = "?",
hl = "GitUntracked",
})
elseif x == "!" and y == "!" then
add(entries, {
path = path,
kind = "ignored",
char = "i",
hl = "GitIgnored",
})
elseif UNMERGED[x .. y] then
add(entries, {
path = path,
kind = "unmerged",
char = "!",
hl = "GitUnmerged",
})
else
if x ~= " " then
local char, hl = staged_attrs(x)
add(entries, {
path = path,
kind = "staged",
char = char,
hl = hl,
orig = orig,
})
end
if y ~= " " then
local char, hl = unstaged_attrs(y)
add(entries, {
path = path,
kind = "unstaged",
char = char,
hl = hl,
orig = orig,
})
end
end
end
local tokens = vim.split(stdout, "\0", { plain = true })
while #tokens > 0 and tokens[#tokens] == "" do
tokens[#tokens] = nil
end
local i = 1
while i <= #tokens do
local line = tokens[i] --[[@as string]]
local tag = line:sub(1, 2)
if tag == "# " then
parse_branch_header(line, branch)
elseif tag == "1 " then
local xy, _, _, _, _, _, _, path =
line:match("^1 (%S+) (%S+) (%S+) (%S+) (%S+) (%S+) (%S+) (.+)$")
if xy and path then
local key = strip_dir_slash(path)
local staged, unstaged =
changes_from_xy(xy:sub(1, 1), xy:sub(2, 2))
entries[key] = changed(key, staged, unstaged)
end
elseif tag == "2 " then
local xy, _, _, _, _, _, _, _, path = line:match(
"^2 (%S+) (%S+) (%S+) (%S+) (%S+) (%S+) (%S+) (%S+) (.+)$"
)
local orig = tokens[i + 1]
if xy and path and orig then
local key = strip_dir_slash(path)
local staged, unstaged =
changes_from_xy(xy:sub(1, 1), xy:sub(2, 2))
entries[key] = changed(key, staged, unstaged, orig)
i = i + 1
end
elseif tag == "u " then
local xy, _, _, _, _, _, _, _, _, path = line:match(
"^u (%S+) (%S+) (%S+) (%S+) (%S+) (%S+) (%S+) (%S+) (%S+) (.+)$"
)
local conflict = xy and CONFLICT_FROM_XY[xy] --[[@as ow.Git.Status.Conflict?]]
or nil
if conflict and path then
local key = strip_dir_slash(path)
entries[key] = unmerged(key, conflict)
end
elseif tag == "? " then
local key = strip_dir_slash(line:sub(3))
entries[key] = untracked(key)
elseif tag == "! " then
local key = strip_dir_slash(line:sub(3))
entries[key] = ignored(key)
end
i = i + 1
end
return setmetatable({ branch = branch, entries = entries }, Status)
end
+111 -79
View File
@@ -2,6 +2,7 @@ local Revision = require("git.revision")
local diff = require("git.diff")
local object = require("git.object")
local repo = require("git.repo")
local status = require("git.status")
local util = require("git.util")
local M = {}
@@ -11,8 +12,8 @@ M.URI_PREFIX = "gitstatus://"
---@type ow.Git.StatusView.Placement[]
M.PLACEMENTS = { "sidebar", "split", "current" }
---@type ow.Git.Status.EntryKind[]
local KINDS = { "untracked", "unstaged", "staged", "unmerged" }
---@type ow.Git.Status.Section[]
local SECTIONS = { "untracked", "unstaged", "staged", "unmerged" }
local WINDOW_WIDTH = 50
---@param name string
@@ -29,9 +30,9 @@ end
---@class ow.Git.StatusView.Header
---@field is_header true
---@field kind ow.Git.Status.EntryKind
---@field section ow.Git.Status.Section
---@alias ow.Git.StatusView.Item ow.Git.Status.Entry | ow.Git.StatusView.Header
---@alias ow.Git.StatusView.Item ow.Git.Status.Row | ow.Git.StatusView.Header
---@class ow.Git.StatusView.State
---@field repo ow.Git.Repo
@@ -83,20 +84,26 @@ local function win_for(s)
return win
end
---@param entry ow.Git.Status.Entry
---@param row ow.Git.Status.Row
---@return string line
---@return string hl_group
---@return integer hl_len
local function format_entry(entry)
local label = entry.orig and (entry.orig .. " -> " .. entry.path)
or entry.path
return string.format(" %s %s", entry.char, label), entry.hl, #entry.char
local function format_row(row)
local entry = row.entry
local orig
if entry.kind == "changed" then
---@cast entry ow.Git.Status.ChangedEntry
orig = entry.orig
end
local label = orig and (orig .. " -> " .. entry.path) or entry.path
local mark = status.mark_for(entry, row.side)
return string.format(" %s %s", mark.char, label), mark.hl, #mark.char
end
---@param kind ow.Git.Status.EntryKind
---@param section ow.Git.Status.Section
---@return string
local function display_name(kind)
return (kind:gsub("^%l", string.upper))
local function display_name(section)
return (section:gsub("^%l", string.upper))
end
---@param bufnr integer
@@ -118,18 +125,18 @@ local function render(bufnr, status)
local meta = {}
local marks = {}
for _, kind in ipairs(KINDS) do
local entries = status:by_kind(kind)
if #entries > 0 then
for _, section in ipairs(SECTIONS) do
local rows = status:rows(section)
if #rows > 0 then
table.insert(
lines,
string.format("%s (%d)", display_name(kind), #entries)
string.format("%s (%d)", display_name(section), #rows)
)
meta[#lines] = { is_header = true, kind = kind }
for _, entry in ipairs(entries) do
local line, hl, hl_len = format_entry(entry)
meta[#lines] = { is_header = true, section = section }
for _, row in ipairs(rows) do
local line, hl, hl_len = format_row(row)
table.insert(lines, line)
meta[#lines] = entry
meta[#lines] = row
table.insert(marks, {
row = #lines - 1,
col = 2,
@@ -195,10 +202,10 @@ local function worktree_pane(r, path)
end
---@param s ow.Git.StatusView.State
---@param entry ow.Git.Status.Entry
---@param path string
---@return ow.Git.Diff.Side
local function index_pane(s, entry)
local rev = Revision.new({ stage = 0, path = entry.path })
local function index_pane(s, path)
local rev = Revision.new({ stage = 0, path = path })
return {
buf = object.buf_for(s.repo, rev),
name = object.format_uri(rev),
@@ -206,38 +213,43 @@ local function index_pane(s, entry)
end
---@param s ow.Git.StatusView.State
---@param entry ow.Git.Status.Entry
---@param row ow.Git.Status.Row
---@return ow.Git.Diff.Side?
local function older_pane(s, entry)
if entry.kind == "staged" then
if entry.char == "A" then
local function older_pane(s, row)
local entry = row.entry
if row.section == "staged" then
---@cast entry ow.Git.Status.ChangedEntry
if entry.staged == "added" then
return nil
end
return head_pane(s.repo, entry.orig or entry.path)
end
if entry.kind == "unstaged" then
return index_pane(s, entry)
if row.section == "unstaged" then
return index_pane(s, entry.path)
end
return nil
end
---@param s ow.Git.StatusView.State
---@param entry ow.Git.Status.Entry
---@param row ow.Git.Status.Row
---@return ow.Git.Diff.Side?
local function newer_pane(s, entry)
if entry.kind == "staged" then
if entry.char == "D" then
local function newer_pane(s, row)
local entry = row.entry
if row.section == "staged" then
---@cast entry ow.Git.Status.ChangedEntry
if entry.staged == "deleted" then
return nil
end
return index_pane(s, entry)
return index_pane(s, entry.path)
end
if entry.kind == "unstaged" then
if entry.char == "D" then
if row.section == "unstaged" then
---@cast entry ow.Git.Status.ChangedEntry
if entry.unstaged == "deleted" then
return nil
end
return worktree_pane(s.repo, entry.path)
end
if entry.kind == "untracked" then
if row.section == "untracked" then
return worktree_pane(s.repo, entry.path)
end
return nil
@@ -296,10 +308,16 @@ local function adopt_diff_wins(s, status_win)
return left, right
end
---@param entry ow.Git.Status.Entry
---@param row ow.Git.Status.Row
---@return string
local function entry_key(entry)
return entry.kind .. "|" .. entry.path .. "|" .. (entry.orig or "")
local function row_key(row)
local entry = row.entry
local orig
if entry.kind == "changed" then
---@cast entry ow.Git.Status.ChangedEntry
orig = entry.orig
end
return row.section .. "|" .. entry.path .. "|" .. (orig or "")
end
---@param target_win integer
@@ -335,18 +353,22 @@ local function ensure_right_win(s, status_win, right_win)
end
---@param s ow.Git.StatusView.State
---@param entry ow.Git.Status.Entry
---@param row ow.Git.Status.Row
---@param focus_left boolean
local function view_entry(s, entry, focus_left)
local function view_row(s, row, focus_left)
local status_win = win_for(s)
if not status_win then
return
end
local left = older_pane(s, entry)
local right = newer_pane(s, entry)
local left = older_pane(s, row)
local right = newer_pane(s, row)
if not left and not right then
util.warning("no content for %s entry: %s", entry.kind, entry.path)
util.warning(
"no content for %s row: %s",
row.section,
row.entry.path
)
return
end
@@ -362,7 +384,7 @@ local function view_entry(s, entry, focus_left)
return
end
local key = entry_key(entry)
local key = row_key(row)
local left_win, right_win = adopt_diff_wins(s, status_win)
local want_pair = left and right
@@ -433,8 +455,8 @@ local function preview_or_open(focus_left)
if not s or not item or item.is_header then
return
end
---@cast item ow.Git.Status.Entry
view_entry(s, item, focus_left)
---@cast item ow.Git.Status.Row
view_row(s, item, focus_left)
end
local function action_stage()
@@ -444,18 +466,18 @@ local function action_stage()
end
local paths = {}
if item.is_header then
if item.kind == "staged" or item.kind == "ignored" then
if item.section == "staged" or item.section == "ignored" then
return
end
for _, e in ipairs(s.repo.status:by_kind(item.kind)) do
table.insert(paths, e.path)
for _, row in ipairs(s.repo.status:rows(item.section)) do
table.insert(paths, row.entry.path)
end
else
---@cast item ow.Git.Status.Entry
if item.kind == "staged" then
---@cast item ow.Git.Status.Row
if item.section == "staged" then
return
end
table.insert(paths, item.path)
table.insert(paths, item.entry.path)
end
if #paths == 0 then
return
@@ -477,25 +499,33 @@ local function action_unstage()
if not s or not item then
return
end
if item.kind ~= "staged" then
local rows
if item.is_header then
if item.section ~= "staged" then
return
end
rows = s.repo.status:rows("staged")
else
---@cast item ow.Git.Status.Row
if item.section ~= "staged" then
return
end
rows = { item }
end
---@cast rows ow.Git.Status.Row[]
if #rows == 0 then
return
end
local args = { "restore", "--staged", "--" }
local entries
if item.is_header then
entries = s.repo.status:by_kind("staged")
else
---@cast item ow.Git.Status.Entry
entries = { item }
end
if #entries == 0 then
return
end
for _, e in ipairs(entries) do
if e.orig then
table.insert(args, e.orig)
for _, row in ipairs(rows) do
local entry = row.entry
if entry.kind == "changed" then
---@cast entry ow.Git.Status.ChangedEntry
if entry.orig then
table.insert(args, entry.orig)
end
end
table.insert(args, e.path)
table.insert(args, entry.path)
end
util.git(args, {
cwd = s.repo.worktree,
@@ -515,32 +545,34 @@ local function action_discard()
if not s or not item or item.is_header then
return
end
---@cast item ow.Git.Status.Entry
if item.kind == "staged" then
---@cast item ow.Git.Status.Row
if item.section == "staged" then
util.warning("file has staged changes, unstage first with 'u'")
return
end
local entry = item.entry
local path = entry.path
local prompt, action
if item.kind == "untracked" then
local is_dir = item.path:sub(-1) == "/"
if item.section == "untracked" then
local is_dir = path:sub(-1) == "/"
prompt = string.format(
"Delete untracked %s %s?",
is_dir and "directory" or "file",
item.path
path
)
action = function()
local target = vim.fs.joinpath(s.repo.worktree, item.path)
local target = vim.fs.joinpath(s.repo.worktree, path)
local rc = vim.fn.delete(target, is_dir and "rf" or "")
if rc ~= 0 then
util.error("failed to delete %s", item.path)
util.error("failed to delete %s", path)
end
refresh(vim.api.nvim_get_current_buf())
end
elseif item.kind == "unstaged" then
prompt = string.format("Discard changes to %s?", item.path)
elseif item.section == "unstaged" then
prompt = string.format("Discard changes to %s?", path)
action = function()
util.git({ "checkout", "--", item.path }, {
util.git({ "checkout", "--", path }, {
cwd = s.repo.worktree,
on_exit = function(result)
if result.code ~= 0 then
+17 -35
View File
@@ -1,47 +1,29 @@
local repo = require("git.repo")
local status = require("git.status")
local util = require("git.util")
local M = {}
---@class ow.Git.Statusline.Status
---@field head string?
---@field entries ow.Git.Status.Entry[]
---@field unstaged boolean
---@field staged boolean
---@field conflict boolean
---@field entry ow.Git.Status.Entry?
---@param entries ow.Git.Status.Entry[]
---@param head string?
---@return ow.Git.Statusline.Status
local function build(entries, head)
local out = {
head = head,
entries = entries,
unstaged = false,
staged = false,
conflict = false,
}
for _, e in ipairs(entries) do
if e.kind == "unstaged" or e.kind == "untracked" then
out.unstaged = true
elseif e.kind == "staged" then
out.staged = true
elseif e.kind == "unmerged" then
out.conflict = true
end
end
return out
end
---@param entries ow.Git.Status.Entry[]
---@param entry ow.Git.Status.Entry?
---@return string
local function render(entries)
if #entries == 0 then
local function render(entry)
if not entry then
return ""
end
local marks = status.marks_for(entry)
if #marks == 0 then
return ""
end
local parts = {}
for _, e in ipairs(entries) do
table.insert(parts, string.format("%%#%s#%s%%*", e.hl, e.char))
for _, mark in ipairs(marks) do
table.insert(
parts,
string.format("%%#%s#%s%%*", mark.hl, mark.char)
)
end
return table.concat(parts, " ")
end
@@ -70,9 +52,9 @@ local function update_buf(buf, r)
if not rel then
return clear(buf)
end
local entries = r.status.entries[rel] or {}
vim.b[buf].git_status = build(entries, r:head())
vim.b[buf].git_status_string = render(entries)
local entry = r.status.entries[rel]
vim.b[buf].git_status = { head = r:head(), entry = entry }
vim.b[buf].git_status_string = render(entry)
end
local enabled = false