refactor(git/status): rework entries into typed variants on porcelain v2
This commit is contained in:
+8
-9
@@ -651,17 +651,16 @@ end
|
|||||||
---@return string[]
|
---@return string[]
|
||||||
local function complete_unstaged_paths(r, lead)
|
local function complete_unstaged_paths(r, lead)
|
||||||
local matches = {}
|
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
|
if path:sub(1, #lead) == lead then
|
||||||
for _, e in ipairs(entry_list) do
|
local include = entry.kind == "untracked"
|
||||||
if
|
or entry.kind == "unmerged"
|
||||||
e.kind == "unstaged"
|
if not include and entry.kind == "changed" then
|
||||||
or e.kind == "untracked"
|
---@cast entry ow.Git.Status.ChangedEntry
|
||||||
or e.kind == "unmerged"
|
include = entry.unstaged ~= nil
|
||||||
then
|
|
||||||
table.insert(matches, path)
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
|
if include then
|
||||||
|
table.insert(matches, path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+25
-5
@@ -1,20 +1,40 @@
|
|||||||
local HIGHLIGHTS = {
|
local DEFAULT_HIGHLIGHTS = {
|
||||||
GitDeleted = "Removed",
|
|
||||||
GitIgnored = "Comment",
|
GitIgnored = "Comment",
|
||||||
GitUnstaged = "Changed",
|
|
||||||
GitRenamed = "GitStaged",
|
|
||||||
GitSha = "Identifier",
|
GitSha = "Identifier",
|
||||||
GitStaged = "Constant",
|
GitStaged = "Constant",
|
||||||
GitUnmerged = "Todo",
|
GitUnmerged = "Todo",
|
||||||
GitUnpulled = "Removed",
|
GitUnpulled = "Removed",
|
||||||
GitUnpushed = "Added",
|
GitUnpushed = "Added",
|
||||||
|
GitUnstaged = "Changed",
|
||||||
GitUntracked = "Added",
|
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 = {}
|
local M = {}
|
||||||
|
|
||||||
function M.init()
|
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 })
|
vim.api.nvim_set_hl(0, name, { link = link, default = true })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -43,10 +43,11 @@ local STATUS_ARGS = {
|
|||||||
"-c",
|
"-c",
|
||||||
"core.quotePath=false",
|
"core.quotePath=false",
|
||||||
"status",
|
"status",
|
||||||
"--porcelain=v1",
|
"--porcelain=v2",
|
||||||
"--branch",
|
"--branch",
|
||||||
"--ignored",
|
"--ignored",
|
||||||
"--untracked-files=all",
|
"--untracked-files=all",
|
||||||
|
"-z",
|
||||||
}
|
}
|
||||||
|
|
||||||
---@private
|
---@private
|
||||||
|
|||||||
+278
-144
@@ -1,44 +1,207 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local UNMERGED = {
|
---@alias ow.Git.Status.Kind
|
||||||
DD = true,
|
---| "changed"
|
||||||
AU = true,
|
---| "unmerged"
|
||||||
UD = true,
|
---| "untracked"
|
||||||
UA = true,
|
---| "ignored"
|
||||||
DU = true,
|
|
||||||
AA = true,
|
|
||||||
UU = true,
|
|
||||||
}
|
|
||||||
|
|
||||||
---@alias ow.Git.Status.EntryKind "untracked"|"unstaged"|"staged"|"unmerged"|"ignored"
|
|
||||||
|
|
||||||
---@class ow.Git.Status.Entry
|
---@class ow.Git.Status.Entry
|
||||||
|
---@field kind ow.Git.Status.Kind
|
||||||
---@field path string
|
---@field path string
|
||||||
---@field kind ow.Git.Status.EntryKind
|
|
||||||
---@field char string
|
---@alias ow.Git.Status.Change
|
||||||
---@field hl string
|
---| "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?
|
---@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 head string?
|
||||||
---@field upstream string?
|
---@field upstream string?
|
||||||
---@field ahead integer
|
---@field ahead integer
|
||||||
---@field behind integer
|
---@field behind integer
|
||||||
|
|
||||||
---@class ow.Git.Status
|
---@class ow.Git.Status
|
||||||
---@field branch ow.Git.Status.BranchInfo
|
---@field branch ow.Git.Status.Branch
|
||||||
---@field entries table<string, ow.Git.Status.Entry[]>
|
---@field entries table<string, ow.Git.Status.Entry>
|
||||||
local Status = {}
|
local Status = {}
|
||||||
Status.__index = Status
|
Status.__index = Status
|
||||||
|
|
||||||
---@param kind ow.Git.Status.EntryKind
|
local CHANGE_FROM_CHAR = {
|
||||||
---@return ow.Git.Status.Entry[]
|
M = "modified",
|
||||||
function Status:by_kind(kind)
|
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 = {}
|
local out = {}
|
||||||
for _, list in pairs(self.entries) do
|
if entry.staged then
|
||||||
for _, e in ipairs(list) do
|
table.insert(out, M.mark_for(entry, "staged"))
|
||||||
if e.kind == kind then
|
end
|
||||||
table.insert(out, e)
|
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
|
end
|
||||||
end
|
end
|
||||||
@@ -46,18 +209,18 @@ function Status:by_kind(kind)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@param prefix string
|
---@param prefix string
|
||||||
---@return ow.Git.Status.Entry[]
|
---@return ow.Git.Status.Mark[]
|
||||||
function Status:aggregate_at(prefix)
|
function Status:aggregate_at(prefix)
|
||||||
local match = (prefix == "" or prefix == ".") and "" or prefix .. "/"
|
local match = (prefix == "" or prefix == ".") and "" or prefix .. "/"
|
||||||
local seen = {}
|
local seen = {}
|
||||||
local out = {}
|
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
|
if path == prefix or vim.startswith(path, match) then
|
||||||
for _, e in ipairs(list) do
|
for _, mark in ipairs(M.marks_for(entry)) do
|
||||||
local key = e.char .. "\0" .. e.hl
|
local key = mark.char .. "\0" .. mark.hl
|
||||||
if not seen[key] then
|
if not seen[key] then
|
||||||
seen[key] = true
|
seen[key] = true
|
||||||
table.insert(out, e)
|
table.insert(out, mark)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -68,138 +231,109 @@ function Status:aggregate_at(prefix)
|
|||||||
return out
|
return out
|
||||||
end
|
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
|
---@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
|
---@param y string
|
||||||
---@return string char, string hl
|
---@return ow.Git.Status.Change?, ow.Git.Status.Change?
|
||||||
local function unstaged_attrs(y)
|
local function changes_from_xy(x, y)
|
||||||
if y == "R" then
|
local staged = x ~= "." and CHANGE_FROM_CHAR[x] or nil
|
||||||
return "R", "GitRenamed"
|
local unstaged = y ~= "." and CHANGE_FROM_CHAR[y] or nil
|
||||||
end
|
return staged, unstaged
|
||||||
if y == "D" then
|
|
||||||
return "D", "GitDeleted"
|
|
||||||
end
|
|
||||||
return y, "GitUnstaged"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param line string
|
---@param path string
|
||||||
---@return ow.Git.Status.BranchInfo
|
---@return string
|
||||||
local function parse_branch_line(line)
|
local function strip_dir_slash(path)
|
||||||
local info = { ahead = 0, behind = 0 }
|
if path:sub(-1) == "/" then
|
||||||
local content = line:sub(4)
|
return path:sub(1, -2)
|
||||||
local arrow = content:find("...", 1, true)
|
|
||||||
if not arrow then
|
|
||||||
info.head = content
|
|
||||||
return info
|
|
||||||
end
|
end
|
||||||
info.head = content:sub(1, arrow - 1)
|
return path
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param stdout string
|
---@param stdout string
|
||||||
---@return ow.Git.Status
|
---@return ow.Git.Status
|
||||||
function M.parse(stdout)
|
function M.parse(stdout)
|
||||||
|
---@type ow.Git.Status.Branch
|
||||||
local branch = { ahead = 0, behind = 0 }
|
local branch = { ahead = 0, behind = 0 }
|
||||||
---@type table<string, ow.Git.Status.Entry[]>
|
---@type table<string, ow.Git.Status.Entry>
|
||||||
local entries = {}
|
local entries = {}
|
||||||
for line in stdout:gmatch("[^\r\n]+") do
|
|
||||||
if line:sub(1, 2) == "##" then
|
local tokens = vim.split(stdout, "\0", { plain = true })
|
||||||
branch = parse_branch_line(line)
|
while #tokens > 0 and tokens[#tokens] == "" do
|
||||||
else
|
tokens[#tokens] = nil
|
||||||
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
|
end
|
||||||
if y ~= " " then
|
|
||||||
local char, hl = unstaged_attrs(y)
|
local i = 1
|
||||||
add(entries, {
|
while i <= #tokens do
|
||||||
path = path,
|
local line = tokens[i] --[[@as string]]
|
||||||
kind = "unstaged",
|
local tag = line:sub(1, 2)
|
||||||
char = char,
|
if tag == "# " then
|
||||||
hl = hl,
|
parse_branch_header(line, branch)
|
||||||
orig = orig,
|
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
|
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
|
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
|
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
|
end
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
|
||||||
return setmetatable({ branch = branch, entries = entries }, Status)
|
return setmetatable({ branch = branch, entries = entries }, Status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+110
-78
@@ -2,6 +2,7 @@ local Revision = require("git.revision")
|
|||||||
local diff = require("git.diff")
|
local diff = require("git.diff")
|
||||||
local object = require("git.object")
|
local object = require("git.object")
|
||||||
local repo = require("git.repo")
|
local repo = require("git.repo")
|
||||||
|
local status = require("git.status")
|
||||||
local util = require("git.util")
|
local util = require("git.util")
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
@@ -11,8 +12,8 @@ M.URI_PREFIX = "gitstatus://"
|
|||||||
---@type ow.Git.StatusView.Placement[]
|
---@type ow.Git.StatusView.Placement[]
|
||||||
M.PLACEMENTS = { "sidebar", "split", "current" }
|
M.PLACEMENTS = { "sidebar", "split", "current" }
|
||||||
|
|
||||||
---@type ow.Git.Status.EntryKind[]
|
---@type ow.Git.Status.Section[]
|
||||||
local KINDS = { "untracked", "unstaged", "staged", "unmerged" }
|
local SECTIONS = { "untracked", "unstaged", "staged", "unmerged" }
|
||||||
local WINDOW_WIDTH = 50
|
local WINDOW_WIDTH = 50
|
||||||
|
|
||||||
---@param name string
|
---@param name string
|
||||||
@@ -29,9 +30,9 @@ end
|
|||||||
|
|
||||||
---@class ow.Git.StatusView.Header
|
---@class ow.Git.StatusView.Header
|
||||||
---@field is_header true
|
---@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
|
---@class ow.Git.StatusView.State
|
||||||
---@field repo ow.Git.Repo
|
---@field repo ow.Git.Repo
|
||||||
@@ -83,20 +84,26 @@ local function win_for(s)
|
|||||||
return win
|
return win
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param entry ow.Git.Status.Entry
|
---@param row ow.Git.Status.Row
|
||||||
---@return string line
|
---@return string line
|
||||||
---@return string hl_group
|
---@return string hl_group
|
||||||
---@return integer hl_len
|
---@return integer hl_len
|
||||||
local function format_entry(entry)
|
local function format_row(row)
|
||||||
local label = entry.orig and (entry.orig .. " -> " .. entry.path)
|
local entry = row.entry
|
||||||
or entry.path
|
local orig
|
||||||
return string.format(" %s %s", entry.char, label), entry.hl, #entry.char
|
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
|
end
|
||||||
|
|
||||||
---@param kind ow.Git.Status.EntryKind
|
---@param section ow.Git.Status.Section
|
||||||
---@return string
|
---@return string
|
||||||
local function display_name(kind)
|
local function display_name(section)
|
||||||
return (kind:gsub("^%l", string.upper))
|
return (section:gsub("^%l", string.upper))
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
@@ -118,18 +125,18 @@ local function render(bufnr, status)
|
|||||||
|
|
||||||
local meta = {}
|
local meta = {}
|
||||||
local marks = {}
|
local marks = {}
|
||||||
for _, kind in ipairs(KINDS) do
|
for _, section in ipairs(SECTIONS) do
|
||||||
local entries = status:by_kind(kind)
|
local rows = status:rows(section)
|
||||||
if #entries > 0 then
|
if #rows > 0 then
|
||||||
table.insert(
|
table.insert(
|
||||||
lines,
|
lines,
|
||||||
string.format("%s (%d)", display_name(kind), #entries)
|
string.format("%s (%d)", display_name(section), #rows)
|
||||||
)
|
)
|
||||||
meta[#lines] = { is_header = true, kind = kind }
|
meta[#lines] = { is_header = true, section = section }
|
||||||
for _, entry in ipairs(entries) do
|
for _, row in ipairs(rows) do
|
||||||
local line, hl, hl_len = format_entry(entry)
|
local line, hl, hl_len = format_row(row)
|
||||||
table.insert(lines, line)
|
table.insert(lines, line)
|
||||||
meta[#lines] = entry
|
meta[#lines] = row
|
||||||
table.insert(marks, {
|
table.insert(marks, {
|
||||||
row = #lines - 1,
|
row = #lines - 1,
|
||||||
col = 2,
|
col = 2,
|
||||||
@@ -195,10 +202,10 @@ local function worktree_pane(r, path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@param s ow.Git.StatusView.State
|
---@param s ow.Git.StatusView.State
|
||||||
---@param entry ow.Git.Status.Entry
|
---@param path string
|
||||||
---@return ow.Git.Diff.Side
|
---@return ow.Git.Diff.Side
|
||||||
local function index_pane(s, entry)
|
local function index_pane(s, path)
|
||||||
local rev = Revision.new({ stage = 0, path = entry.path })
|
local rev = Revision.new({ stage = 0, path = path })
|
||||||
return {
|
return {
|
||||||
buf = object.buf_for(s.repo, rev),
|
buf = object.buf_for(s.repo, rev),
|
||||||
name = object.format_uri(rev),
|
name = object.format_uri(rev),
|
||||||
@@ -206,38 +213,43 @@ local function index_pane(s, entry)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@param s ow.Git.StatusView.State
|
---@param s ow.Git.StatusView.State
|
||||||
---@param entry ow.Git.Status.Entry
|
---@param row ow.Git.Status.Row
|
||||||
---@return ow.Git.Diff.Side?
|
---@return ow.Git.Diff.Side?
|
||||||
local function older_pane(s, entry)
|
local function older_pane(s, row)
|
||||||
if entry.kind == "staged" then
|
local entry = row.entry
|
||||||
if entry.char == "A" then
|
if row.section == "staged" then
|
||||||
|
---@cast entry ow.Git.Status.ChangedEntry
|
||||||
|
if entry.staged == "added" then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
return head_pane(s.repo, entry.orig or entry.path)
|
return head_pane(s.repo, entry.orig or entry.path)
|
||||||
end
|
end
|
||||||
if entry.kind == "unstaged" then
|
if row.section == "unstaged" then
|
||||||
return index_pane(s, entry)
|
return index_pane(s, entry.path)
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param s ow.Git.StatusView.State
|
---@param s ow.Git.StatusView.State
|
||||||
---@param entry ow.Git.Status.Entry
|
---@param row ow.Git.Status.Row
|
||||||
---@return ow.Git.Diff.Side?
|
---@return ow.Git.Diff.Side?
|
||||||
local function newer_pane(s, entry)
|
local function newer_pane(s, row)
|
||||||
if entry.kind == "staged" then
|
local entry = row.entry
|
||||||
if entry.char == "D" then
|
if row.section == "staged" then
|
||||||
|
---@cast entry ow.Git.Status.ChangedEntry
|
||||||
|
if entry.staged == "deleted" then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
return index_pane(s, entry)
|
return index_pane(s, entry.path)
|
||||||
end
|
end
|
||||||
if entry.kind == "unstaged" then
|
if row.section == "unstaged" then
|
||||||
if entry.char == "D" then
|
---@cast entry ow.Git.Status.ChangedEntry
|
||||||
|
if entry.unstaged == "deleted" then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
return worktree_pane(s.repo, entry.path)
|
return worktree_pane(s.repo, entry.path)
|
||||||
end
|
end
|
||||||
if entry.kind == "untracked" then
|
if row.section == "untracked" then
|
||||||
return worktree_pane(s.repo, entry.path)
|
return worktree_pane(s.repo, entry.path)
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
@@ -296,10 +308,16 @@ local function adopt_diff_wins(s, status_win)
|
|||||||
return left, right
|
return left, right
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param entry ow.Git.Status.Entry
|
---@param row ow.Git.Status.Row
|
||||||
---@return string
|
---@return string
|
||||||
local function entry_key(entry)
|
local function row_key(row)
|
||||||
return entry.kind .. "|" .. entry.path .. "|" .. (entry.orig or "")
|
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
|
end
|
||||||
|
|
||||||
---@param target_win integer
|
---@param target_win integer
|
||||||
@@ -335,18 +353,22 @@ local function ensure_right_win(s, status_win, right_win)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@param s ow.Git.StatusView.State
|
---@param s ow.Git.StatusView.State
|
||||||
---@param entry ow.Git.Status.Entry
|
---@param row ow.Git.Status.Row
|
||||||
---@param focus_left boolean
|
---@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)
|
local status_win = win_for(s)
|
||||||
if not status_win then
|
if not status_win then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local left = older_pane(s, entry)
|
local left = older_pane(s, row)
|
||||||
local right = newer_pane(s, entry)
|
local right = newer_pane(s, row)
|
||||||
if not left and not right then
|
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
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -362,7 +384,7 @@ local function view_entry(s, entry, focus_left)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local key = entry_key(entry)
|
local key = row_key(row)
|
||||||
local left_win, right_win = adopt_diff_wins(s, status_win)
|
local left_win, right_win = adopt_diff_wins(s, status_win)
|
||||||
local want_pair = left and right
|
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
|
if not s or not item or item.is_header then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
---@cast item ow.Git.Status.Entry
|
---@cast item ow.Git.Status.Row
|
||||||
view_entry(s, item, focus_left)
|
view_row(s, item, focus_left)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function action_stage()
|
local function action_stage()
|
||||||
@@ -444,18 +466,18 @@ local function action_stage()
|
|||||||
end
|
end
|
||||||
local paths = {}
|
local paths = {}
|
||||||
if item.is_header then
|
if item.is_header then
|
||||||
if item.kind == "staged" or item.kind == "ignored" then
|
if item.section == "staged" or item.section == "ignored" then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
for _, e in ipairs(s.repo.status:by_kind(item.kind)) do
|
for _, row in ipairs(s.repo.status:rows(item.section)) do
|
||||||
table.insert(paths, e.path)
|
table.insert(paths, row.entry.path)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
---@cast item ow.Git.Status.Entry
|
---@cast item ow.Git.Status.Row
|
||||||
if item.kind == "staged" then
|
if item.section == "staged" then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
table.insert(paths, item.path)
|
table.insert(paths, item.entry.path)
|
||||||
end
|
end
|
||||||
if #paths == 0 then
|
if #paths == 0 then
|
||||||
return
|
return
|
||||||
@@ -477,25 +499,33 @@ local function action_unstage()
|
|||||||
if not s or not item then
|
if not s or not item then
|
||||||
return
|
return
|
||||||
end
|
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
|
return
|
||||||
end
|
end
|
||||||
local args = { "restore", "--staged", "--" }
|
local args = { "restore", "--staged", "--" }
|
||||||
local entries
|
for _, row in ipairs(rows) do
|
||||||
if item.is_header then
|
local entry = row.entry
|
||||||
entries = s.repo.status:by_kind("staged")
|
if entry.kind == "changed" then
|
||||||
else
|
---@cast entry ow.Git.Status.ChangedEntry
|
||||||
---@cast item ow.Git.Status.Entry
|
if entry.orig then
|
||||||
entries = { item }
|
table.insert(args, entry.orig)
|
||||||
end
|
end
|
||||||
if #entries == 0 then
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
for _, e in ipairs(entries) do
|
table.insert(args, entry.path)
|
||||||
if e.orig then
|
|
||||||
table.insert(args, e.orig)
|
|
||||||
end
|
|
||||||
table.insert(args, e.path)
|
|
||||||
end
|
end
|
||||||
util.git(args, {
|
util.git(args, {
|
||||||
cwd = s.repo.worktree,
|
cwd = s.repo.worktree,
|
||||||
@@ -515,32 +545,34 @@ local function action_discard()
|
|||||||
if not s or not item or item.is_header then
|
if not s or not item or item.is_header then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
---@cast item ow.Git.Status.Entry
|
---@cast item ow.Git.Status.Row
|
||||||
if item.kind == "staged" then
|
if item.section == "staged" then
|
||||||
util.warning("file has staged changes, unstage first with 'u'")
|
util.warning("file has staged changes, unstage first with 'u'")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local entry = item.entry
|
||||||
|
local path = entry.path
|
||||||
|
|
||||||
local prompt, action
|
local prompt, action
|
||||||
if item.kind == "untracked" then
|
if item.section == "untracked" then
|
||||||
local is_dir = item.path:sub(-1) == "/"
|
local is_dir = path:sub(-1) == "/"
|
||||||
prompt = string.format(
|
prompt = string.format(
|
||||||
"Delete untracked %s %s?",
|
"Delete untracked %s %s?",
|
||||||
is_dir and "directory" or "file",
|
is_dir and "directory" or "file",
|
||||||
item.path
|
path
|
||||||
)
|
)
|
||||||
action = function()
|
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 "")
|
local rc = vim.fn.delete(target, is_dir and "rf" or "")
|
||||||
if rc ~= 0 then
|
if rc ~= 0 then
|
||||||
util.error("failed to delete %s", item.path)
|
util.error("failed to delete %s", path)
|
||||||
end
|
end
|
||||||
refresh(vim.api.nvim_get_current_buf())
|
refresh(vim.api.nvim_get_current_buf())
|
||||||
end
|
end
|
||||||
elseif item.kind == "unstaged" then
|
elseif item.section == "unstaged" then
|
||||||
prompt = string.format("Discard changes to %s?", item.path)
|
prompt = string.format("Discard changes to %s?", path)
|
||||||
action = function()
|
action = function()
|
||||||
util.git({ "checkout", "--", item.path }, {
|
util.git({ "checkout", "--", path }, {
|
||||||
cwd = s.repo.worktree,
|
cwd = s.repo.worktree,
|
||||||
on_exit = function(result)
|
on_exit = function(result)
|
||||||
if result.code ~= 0 then
|
if result.code ~= 0 then
|
||||||
|
|||||||
+17
-35
@@ -1,47 +1,29 @@
|
|||||||
local repo = require("git.repo")
|
local repo = require("git.repo")
|
||||||
|
local status = require("git.status")
|
||||||
local util = require("git.util")
|
local util = require("git.util")
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
---@class ow.Git.Statusline.Status
|
---@class ow.Git.Statusline.Status
|
||||||
---@field head string?
|
---@field head string?
|
||||||
---@field entries ow.Git.Status.Entry[]
|
---@field entry ow.Git.Status.Entry?
|
||||||
---@field unstaged boolean
|
|
||||||
---@field staged boolean
|
|
||||||
---@field conflict boolean
|
|
||||||
|
|
||||||
---@param entries ow.Git.Status.Entry[]
|
---@param entry 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[]
|
|
||||||
---@return string
|
---@return string
|
||||||
local function render(entries)
|
local function render(entry)
|
||||||
if #entries == 0 then
|
if not entry then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
local marks = status.marks_for(entry)
|
||||||
|
if #marks == 0 then
|
||||||
return ""
|
return ""
|
||||||
end
|
end
|
||||||
local parts = {}
|
local parts = {}
|
||||||
for _, e in ipairs(entries) do
|
for _, mark in ipairs(marks) do
|
||||||
table.insert(parts, string.format("%%#%s#%s%%*", e.hl, e.char))
|
table.insert(
|
||||||
|
parts,
|
||||||
|
string.format("%%#%s#%s%%*", mark.hl, mark.char)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
return table.concat(parts, " ")
|
return table.concat(parts, " ")
|
||||||
end
|
end
|
||||||
@@ -70,9 +52,9 @@ local function update_buf(buf, r)
|
|||||||
if not rel then
|
if not rel then
|
||||||
return clear(buf)
|
return clear(buf)
|
||||||
end
|
end
|
||||||
local entries = r.status.entries[rel] or {}
|
local entry = r.status.entries[rel]
|
||||||
vim.b[buf].git_status = build(entries, r:head())
|
vim.b[buf].git_status = { head = r:head(), entry = entry }
|
||||||
vim.b[buf].git_status_string = render(entries)
|
vim.b[buf].git_status_string = render(entry)
|
||||||
end
|
end
|
||||||
|
|
||||||
local enabled = false
|
local enabled = false
|
||||||
|
|||||||
+17
-11
@@ -49,9 +49,11 @@ function GitDecorator:new()
|
|||||||
self.highlight_range = "name"
|
self.highlight_range = "name"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local status = require("git.status")
|
||||||
|
|
||||||
---@param node Node
|
---@param node Node
|
||||||
---@return ow.Git.Status.Entry[]?
|
---@return ow.Git.Status.Mark[]?
|
||||||
local function entries_for(node)
|
local function marks_for(node)
|
||||||
local r = repo.find(node.absolute_path)
|
local r = repo.find(node.absolute_path)
|
||||||
if not r then
|
if not r then
|
||||||
return
|
return
|
||||||
@@ -64,20 +66,24 @@ local function entries_for(node)
|
|||||||
local list = r.status:aggregate_at(rel)
|
local list = r.status:aggregate_at(rel)
|
||||||
return #list > 0 and list or nil
|
return #list > 0 and list or nil
|
||||||
end
|
end
|
||||||
return r.status.entries[rel]
|
local entry = r.status.entries[rel]
|
||||||
|
if not entry then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
return status.marks_for(entry)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param node Node
|
---@param node Node
|
||||||
---@return { str: string, hl: string[] }[]?
|
---@return { str: string, hl: string[] }[]?
|
||||||
function GitDecorator.icons(_, node)
|
function GitDecorator.icons(_, node)
|
||||||
local list = entries_for(node)
|
local list = marks_for(node)
|
||||||
if not list then
|
if not list then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local out = {}
|
local out = {}
|
||||||
for _, entry in ipairs(list) do
|
for _, mark in ipairs(list) do
|
||||||
if entry.kind ~= "ignored" then
|
if mark.hl ~= "GitIgnored" then
|
||||||
table.insert(out, { str = entry.char, hl = { entry.hl } })
|
table.insert(out, { str = mark.char, hl = { mark.hl } })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return out
|
return out
|
||||||
@@ -86,16 +92,16 @@ end
|
|||||||
---@param node Node
|
---@param node Node
|
||||||
---@return string?
|
---@return string?
|
||||||
function GitDecorator.highlight_group(_, node)
|
function GitDecorator.highlight_group(_, node)
|
||||||
local list = entries_for(node)
|
local list = marks_for(node)
|
||||||
if not list then
|
if not list then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local hl
|
local hl
|
||||||
for _, entry in ipairs(list) do
|
for _, mark in ipairs(list) do
|
||||||
if entry.kind ~= "ignored" then
|
if mark.hl ~= "GitIgnored" then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
hl = hl or entry.hl
|
hl = hl or mark.hl
|
||||||
end
|
end
|
||||||
return hl
|
return hl
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
local t = require("test")
|
||||||
|
local status = require("git.status")
|
||||||
|
|
||||||
|
local NUL = "\0"
|
||||||
|
|
||||||
|
---@param parts string[]
|
||||||
|
---@return string
|
||||||
|
local function nul(parts)
|
||||||
|
return table.concat(parts, NUL) .. NUL
|
||||||
|
end
|
||||||
|
|
||||||
|
t.test("branch headers: initial repo, no commits", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"# branch.oid (initial)",
|
||||||
|
"# branch.head main",
|
||||||
|
}))
|
||||||
|
t.eq(s.branch.oid, nil)
|
||||||
|
t.eq(s.branch.head, "main")
|
||||||
|
t.eq(s.branch.upstream, nil)
|
||||||
|
t.eq(s.branch.ahead, 0)
|
||||||
|
t.eq(s.branch.behind, 0)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("branch headers: detached HEAD", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"# branch.oid 1234567890abcdef1234567890abcdef12345678",
|
||||||
|
"# branch.head (detached)",
|
||||||
|
}))
|
||||||
|
t.eq(s.branch.oid, "1234567890abcdef1234567890abcdef12345678")
|
||||||
|
t.eq(s.branch.head, nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("branch headers: with upstream and ahead/behind", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"# branch.oid abc123",
|
||||||
|
"# branch.head main",
|
||||||
|
"# branch.upstream origin/main",
|
||||||
|
"# branch.ab +3 -2",
|
||||||
|
}))
|
||||||
|
t.eq(s.branch.head, "main")
|
||||||
|
t.eq(s.branch.upstream, "origin/main")
|
||||||
|
t.eq(s.branch.ahead, 3)
|
||||||
|
t.eq(s.branch.behind, 2)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("type 1: staged-only modification", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"1 M. N... 100644 100644 100644 abc abc foo.lua",
|
||||||
|
}))
|
||||||
|
local e = s.entries["foo.lua"]
|
||||||
|
---@cast e ow.Git.Status.ChangedEntry
|
||||||
|
t.eq(e.kind, "changed")
|
||||||
|
t.eq(e.path, "foo.lua")
|
||||||
|
t.eq(e.staged, "modified")
|
||||||
|
t.eq(e.unstaged, nil)
|
||||||
|
t.eq(e.orig, nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("type 1: unstaged-only modification", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"1 .M N... 100644 100644 100644 abc abc foo.lua",
|
||||||
|
}))
|
||||||
|
local e = s.entries["foo.lua"]
|
||||||
|
---@cast e ow.Git.Status.ChangedEntry
|
||||||
|
t.eq(e.staged, nil)
|
||||||
|
t.eq(e.unstaged, "modified")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("type 1: both sides modified", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"1 MM N... 100644 100644 100644 abc abc foo.lua",
|
||||||
|
}))
|
||||||
|
local e = s.entries["foo.lua"]
|
||||||
|
---@cast e ow.Git.Status.ChangedEntry
|
||||||
|
t.eq(e.staged, "modified")
|
||||||
|
t.eq(e.unstaged, "modified")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("type 1: deleted (unstaged)", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"1 .D N... 100644 100644 000000 abc abc foo.lua",
|
||||||
|
}))
|
||||||
|
local e = s.entries["foo.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
||||||
|
t.eq(e.unstaged, "deleted")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("type 1: added (staged)", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"1 A. N... 000000 100644 100644 abc abc new.lua",
|
||||||
|
}))
|
||||||
|
local e = s.entries["new.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
||||||
|
t.eq(e.staged, "added")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("type 1: type-changed (unstaged)", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"1 .T N... 100644 100644 120000 abc abc foo.lua",
|
||||||
|
}))
|
||||||
|
local e = s.entries["foo.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
||||||
|
t.eq(e.unstaged, "type_changed")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("type 2: renamed with orig", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"2 R. N... 100644 100644 100644 abc abc R100 new.lua",
|
||||||
|
"old.lua",
|
||||||
|
}))
|
||||||
|
local e = s.entries["new.lua"]
|
||||||
|
---@cast e ow.Git.Status.ChangedEntry
|
||||||
|
t.eq(e.kind, "changed")
|
||||||
|
t.eq(e.path, "new.lua")
|
||||||
|
t.eq(e.staged, "renamed")
|
||||||
|
t.eq(e.orig, "old.lua")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("type 2: copied with orig", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"2 C. N... 100644 100644 100644 abc abc C90 copy.lua",
|
||||||
|
"src.lua",
|
||||||
|
}))
|
||||||
|
local e = s.entries["copy.lua"]
|
||||||
|
---@cast e ow.Git.Status.ChangedEntry
|
||||||
|
t.eq(e.staged, "copied")
|
||||||
|
t.eq(e.orig, "src.lua")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("type u: all seven conflict types", function()
|
||||||
|
local cases = {
|
||||||
|
{ xy = "DD", expected = "both_deleted" },
|
||||||
|
{ xy = "AU", expected = "added_by_us" },
|
||||||
|
{ xy = "UD", expected = "deleted_by_them" },
|
||||||
|
{ xy = "UA", expected = "added_by_them" },
|
||||||
|
{ xy = "DU", expected = "deleted_by_us" },
|
||||||
|
{ xy = "AA", expected = "both_added" },
|
||||||
|
{ xy = "UU", expected = "both_modified" },
|
||||||
|
}
|
||||||
|
for _, c in ipairs(cases) do
|
||||||
|
local s = status.parse(nul({
|
||||||
|
string.format(
|
||||||
|
"u %s N... 100644 100644 100644 100644 abc abc abc conflict.lua",
|
||||||
|
c.xy
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
local e = s.entries["conflict.lua"]
|
||||||
|
t.eq(e.kind, "unmerged", "kind for " .. c.xy)
|
||||||
|
t.eq(
|
||||||
|
(e --[[@as ow.Git.Status.UnmergedEntry]]).conflict,
|
||||||
|
c.expected,
|
||||||
|
"conflict for " .. c.xy
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("type ?: untracked", function()
|
||||||
|
local s = status.parse(nul({ "? new.txt" }))
|
||||||
|
local e = s.entries["new.txt"]
|
||||||
|
t.eq(e.kind, "untracked")
|
||||||
|
t.eq(e.path, "new.txt")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("type !: ignored", function()
|
||||||
|
local s = status.parse(nul({ "! .secret" }))
|
||||||
|
local e = s.entries[".secret"]
|
||||||
|
t.eq(e.kind, "ignored")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("mixed: branch + multiple variants", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"# branch.oid abc",
|
||||||
|
"# branch.head main",
|
||||||
|
"# branch.upstream origin/main",
|
||||||
|
"# branch.ab +0 -0",
|
||||||
|
"1 M. N... 100644 100644 100644 a a staged.lua",
|
||||||
|
"1 .M N... 100644 100644 100644 a a unstaged.lua",
|
||||||
|
"1 MM N... 100644 100644 100644 a a both.lua",
|
||||||
|
"u UU N... 100644 100644 100644 100644 a a a conflict.lua",
|
||||||
|
"? untracked.txt",
|
||||||
|
"! ignored.txt",
|
||||||
|
}))
|
||||||
|
t.eq(s.branch.head, "main")
|
||||||
|
local staged = s.entries["staged.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
||||||
|
local unstaged = s.entries["unstaged.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
||||||
|
local both = s.entries["both.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
||||||
|
t.eq(staged.staged, "modified")
|
||||||
|
t.eq(unstaged.unstaged, "modified")
|
||||||
|
t.eq(both.staged, "modified")
|
||||||
|
t.eq(both.unstaged, "modified")
|
||||||
|
t.eq(s.entries["conflict.lua"].kind, "unmerged")
|
||||||
|
t.eq(s.entries["untracked.txt"].kind, "untracked")
|
||||||
|
t.eq(s.entries["ignored.txt"].kind, "ignored")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("paths with spaces survive splitting", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"1 .M N... 100644 100644 100644 a a path with spaces.lua",
|
||||||
|
}))
|
||||||
|
local e = s.entries["path with spaces.lua"] --[[@as ow.Git.Status.ChangedEntry]]
|
||||||
|
t.eq(e.unstaged, "modified")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("mark_for: changed staged modified", function()
|
||||||
|
local entry = {
|
||||||
|
kind = "changed",
|
||||||
|
path = "x",
|
||||||
|
staged = "modified",
|
||||||
|
}
|
||||||
|
t.eq(status.mark_for(entry, "staged"), { char = "M", hl = "GitStagedModified" })
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("mark_for: changed unstaged deleted uses GitUnstagedDeleted", function()
|
||||||
|
local entry = {
|
||||||
|
kind = "changed",
|
||||||
|
path = "x",
|
||||||
|
unstaged = "deleted",
|
||||||
|
}
|
||||||
|
t.eq(status.mark_for(entry, "unstaged"), { char = "D", hl = "GitUnstagedDeleted" })
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("mark_for: changed renamed uses per-side renamed hl", function()
|
||||||
|
local entry = {
|
||||||
|
kind = "changed",
|
||||||
|
path = "x",
|
||||||
|
staged = "renamed",
|
||||||
|
orig = "y",
|
||||||
|
}
|
||||||
|
t.eq(status.mark_for(entry, "staged"), { char = "R", hl = "GitStagedRenamed" })
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("mark_for: untracked / ignored / unmerged ignore side", function()
|
||||||
|
t.eq(
|
||||||
|
status.mark_for({ kind = "untracked", path = "x" } --[[@as ow.Git.Status.Entry]]),
|
||||||
|
{ char = "?", hl = "GitUntracked" }
|
||||||
|
)
|
||||||
|
t.eq(
|
||||||
|
status.mark_for({ kind = "ignored", path = "x" } --[[@as ow.Git.Status.Entry]]),
|
||||||
|
{ char = "i", hl = "GitIgnored" }
|
||||||
|
)
|
||||||
|
t.eq(
|
||||||
|
status.mark_for({
|
||||||
|
kind = "unmerged",
|
||||||
|
path = "x",
|
||||||
|
conflict = "both_modified",
|
||||||
|
} --[[@as ow.Git.Status.Entry]]),
|
||||||
|
{ char = "!", hl = "GitUnmergedBothModified" }
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("marks_for: changed with both sides yields two marks", function()
|
||||||
|
local entry = {
|
||||||
|
kind = "changed",
|
||||||
|
path = "x",
|
||||||
|
staged = "modified",
|
||||||
|
unstaged = "modified",
|
||||||
|
}
|
||||||
|
local marks = status.marks_for(entry)
|
||||||
|
t.eq(#marks, 2)
|
||||||
|
t.eq(marks[1], { char = "M", hl = "GitStagedModified" })
|
||||||
|
t.eq(marks[2], { char = "M", hl = "GitUnstagedModified" })
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("marks_for: changed one-sided yields one mark", function()
|
||||||
|
local entry = { kind = "changed", path = "x", staged = "added" }
|
||||||
|
local marks = status.marks_for(entry)
|
||||||
|
t.eq(#marks, 1)
|
||||||
|
t.eq(marks[1], { char = "A", hl = "GitStagedAdded" })
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("marks_for: untracked yields one mark", function()
|
||||||
|
local entry = { kind = "untracked", path = "x" } --[[@as ow.Git.Status.Entry]]
|
||||||
|
local marks = status.marks_for(entry)
|
||||||
|
t.eq(#marks, 1)
|
||||||
|
t.eq(marks[1], { char = "?", hl = "GitUntracked" })
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("Status:rows buckets by section", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"1 M. N... 100644 100644 100644 a a staged.lua",
|
||||||
|
"1 .M N... 100644 100644 100644 a a unstaged.lua",
|
||||||
|
"1 MM N... 100644 100644 100644 a a both.lua",
|
||||||
|
"? untracked.txt",
|
||||||
|
}))
|
||||||
|
t.eq(#s:rows("staged"), 2, "staged section: staged.lua + both.lua")
|
||||||
|
t.eq(#s:rows("unstaged"), 2, "unstaged section: unstaged.lua + both.lua")
|
||||||
|
t.eq(#s:rows("untracked"), 1)
|
||||||
|
t.eq(#s:rows("unmerged"), 0)
|
||||||
|
t.eq(#s:rows("ignored"), 0)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("Status:rows for staged carries side='staged'", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"1 M. N... 100644 100644 100644 a a x.lua",
|
||||||
|
}))
|
||||||
|
local row = assert(s:rows("staged")[1])
|
||||||
|
t.eq(row.section, "staged")
|
||||||
|
t.eq(row.side, "staged")
|
||||||
|
t.eq(row.entry.kind, "changed")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("Status:rows for untracked has nil side", function()
|
||||||
|
local s = status.parse(nul({ "? x.txt" }))
|
||||||
|
local row = assert(s:rows("untracked")[1])
|
||||||
|
t.eq(row.section, "untracked")
|
||||||
|
t.eq(row.side, nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("Status:aggregate_at dedups marks under prefix", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"1 .M N... 100644 100644 100644 a a sub/a.lua",
|
||||||
|
"1 .M N... 100644 100644 100644 a a sub/b.lua",
|
||||||
|
"? sub/c.txt",
|
||||||
|
}))
|
||||||
|
local marks = s:aggregate_at("sub")
|
||||||
|
t.eq(#marks, 2, "modified ('M') and untracked ('?') deduped")
|
||||||
|
local m1 = assert(marks[1])
|
||||||
|
local m2 = assert(marks[2])
|
||||||
|
local hls = { m1.hl, m2.hl }
|
||||||
|
table.sort(hls)
|
||||||
|
t.eq(hls, { "GitUnstagedModified", "GitUntracked" })
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("Status:aggregate_at with prefix '.' includes everything", function()
|
||||||
|
local s = status.parse(nul({
|
||||||
|
"1 .M N... 100644 100644 100644 a a a.lua",
|
||||||
|
"? b.txt",
|
||||||
|
}))
|
||||||
|
t.eq(#s:aggregate_at("."), 2)
|
||||||
|
end)
|
||||||
@@ -78,7 +78,7 @@ local function setup_sidebar_with_unstaged_file(
|
|||||||
)
|
)
|
||||||
r:refresh()
|
r:refresh()
|
||||||
t.wait_for(function()
|
t.wait_for(function()
|
||||||
return r.status and #r.status:by_kind("unstaged") > 0
|
return r.status and #r.status:rows("unstaged") > 0
|
||||||
end, "git status to report unstaged changes")
|
end, "git status to report unstaged changes")
|
||||||
|
|
||||||
local entry_line = assert(
|
local entry_line = assert(
|
||||||
@@ -108,7 +108,7 @@ t.test("stage with diff open: sidebar cursor stays put", function()
|
|||||||
vim.api.nvim_set_current_win(sidebar_win)
|
vim.api.nvim_set_current_win(sidebar_win)
|
||||||
t.press("s")
|
t.press("s")
|
||||||
t.wait_for(function()
|
t.wait_for(function()
|
||||||
return #r.status:by_kind("staged") > 0
|
return #r.status:rows("staged") > 0
|
||||||
end, "stage to propagate to repo state")
|
end, "stage to propagate to repo state")
|
||||||
|
|
||||||
t.eq(
|
t.eq(
|
||||||
@@ -145,7 +145,7 @@ t.test(
|
|||||||
vim.api.nvim_set_current_win(sidebar_win)
|
vim.api.nvim_set_current_win(sidebar_win)
|
||||||
t.press("s")
|
t.press("s")
|
||||||
t.wait_for(function()
|
t.wait_for(function()
|
||||||
return #r.status:by_kind("staged") > 0
|
return #r.status:rows("staged") > 0
|
||||||
end, "stage to propagate to repo state")
|
end, "stage to propagate to repo state")
|
||||||
|
|
||||||
t.eq(
|
t.eq(
|
||||||
|
|||||||
Reference in New Issue
Block a user