Compare commits
5 Commits
20be779891
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a78950ac68 | |||
| c4585b7768 | |||
| 79120ed4f7 | |||
| 182e507dc7 | |||
| 22309fe8fd |
@@ -23,7 +23,7 @@ The plugin auto-initializes from its `plugin/git.lua` and exposes `lua/git/`. No
|
||||
With `vim.pack`:
|
||||
|
||||
```lua
|
||||
vim.pack.add({ "https://git.owall.dev/warg/git.nvim" })
|
||||
vim.pack.add({ "https://git.owall.se/warg/git.nvim" })
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
+15
-19
@@ -56,21 +56,6 @@ function M.state(buf)
|
||||
return states[buf]
|
||||
end
|
||||
|
||||
---@param ts integer
|
||||
---@param tz string
|
||||
---@return string
|
||||
local function format_author_time(ts, tz)
|
||||
local sign, hh, mm = tz:match("^([+-])(%d%d)(%d%d)$")
|
||||
---@type number
|
||||
local offset = 0
|
||||
if sign then
|
||||
local h = tonumber(hh) or 0
|
||||
local m = tonumber(mm) or 0
|
||||
offset = (h * 3600 + m * 60) * (sign == "-" and -1 or 1)
|
||||
end
|
||||
return os.date("!%Y-%m-%d %T ", ts + offset) .. tz
|
||||
end
|
||||
|
||||
---@param stdout string
|
||||
---@return ow.Git.Blame.Result
|
||||
local function parse_porcelain(stdout)
|
||||
@@ -207,14 +192,26 @@ local function resolve_source(buf)
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
if util.is_uri(name) then
|
||||
local rev = object.parse_uri(name)
|
||||
if not rev or not rev.base or not rev.path then
|
||||
if not rev then
|
||||
return nil
|
||||
end
|
||||
local r = repo.find(buf)
|
||||
if not r then
|
||||
return nil
|
||||
end
|
||||
return { repo = r, rel = rev.path, revision = rev.base }
|
||||
local rel = rev.path
|
||||
if not rel then
|
||||
local state = r:state(buf)
|
||||
rel = state and state.blob_path or nil
|
||||
if not rel then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
local revision = nil
|
||||
if rev.path and rev.base and rev.stage == nil then
|
||||
revision = rev.base
|
||||
end
|
||||
return { repo = r, rel = rel, revision = revision }
|
||||
end
|
||||
if not repo.is_worktree_buf(buf) then
|
||||
return nil
|
||||
@@ -339,7 +336,6 @@ local function extract_commit_hunk(diff_text, orig_line)
|
||||
end
|
||||
local out = {} ---@type string[]
|
||||
table.insert(out, ("Hunk %d of %d"):format(target, #hunk_starts))
|
||||
table.insert(out, "")
|
||||
local start_idx = hunk_starts[target]
|
||||
local end_idx = hunk_starts[target + 1] and (hunk_starts[target + 1] - 1)
|
||||
or #lines
|
||||
@@ -535,7 +531,7 @@ local function open_popup(state, lnum, watch_buf)
|
||||
head = { "Not Committed Yet" }
|
||||
else
|
||||
local short = sha:sub(1, 8)
|
||||
local date = format_author_time(commit.author_time, commit.author_tz)
|
||||
local date = util.format_git_time(commit.author_time, commit.author_tz)
|
||||
sha_len = #short
|
||||
date_col = sha_len + 2 + #commit.author + 2
|
||||
head = {
|
||||
|
||||
@@ -19,6 +19,7 @@ end
|
||||
---@field immutable boolean?
|
||||
---@field index_writer boolean?
|
||||
---@field index_mode string?
|
||||
---@field blob_path string?
|
||||
|
||||
---@alias ow.Git.Repo.Event
|
||||
---| "change"
|
||||
|
||||
@@ -120,6 +120,23 @@ function M.split_lines(content)
|
||||
return lines
|
||||
end
|
||||
|
||||
---@param ts integer
|
||||
---@param tz string
|
||||
---@return string
|
||||
function M.format_git_time(ts, tz)
|
||||
local sign, hh, mm = tz:match("^([+-])(%d%d)(%d%d)$")
|
||||
local offset = 0
|
||||
if sign then
|
||||
local h = tonumber(hh) or 0
|
||||
local m = tonumber(mm) or 0
|
||||
offset = math.floor(h * 3600 + m * 60)
|
||||
if sign == "-" then
|
||||
offset = -offset
|
||||
end
|
||||
end
|
||||
return os.date("!%a %b %e %T %Y ", ts + offset) .. tz
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param ns integer
|
||||
---@param lines string[]
|
||||
|
||||
+94
-12
@@ -159,6 +159,29 @@ function M.buf_for(r, rev)
|
||||
return buf
|
||||
end
|
||||
|
||||
---@param lines string[]
|
||||
local function format_header_dates(lines)
|
||||
for i, line in ipairs(lines) do
|
||||
if line == "" then
|
||||
return
|
||||
end
|
||||
local prefix, ts, tz = line:match("^(author .-) (%d+) ([+-]%d%d%d%d)$")
|
||||
if not prefix then
|
||||
prefix, ts, tz = line:match("^(committer .-) (%d+) ([+-]%d%d%d%d)$")
|
||||
end
|
||||
if not prefix then
|
||||
prefix, ts, tz = line:match("^(tagger .-) (%d+) ([+-]%d%d%d%d)$")
|
||||
end
|
||||
if prefix then
|
||||
local n = math.floor(assert(tonumber(ts)))
|
||||
lines[i] = ("%s %s"):format(
|
||||
prefix,
|
||||
util.format_git_time(n, tz --[[@as string]])
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param r ow.Git.Repo
|
||||
---@param rev ow.Git.Revision
|
||||
@@ -189,7 +212,9 @@ local function populate(buf, r, rev, state, rev_sha)
|
||||
end
|
||||
end
|
||||
|
||||
util.set_buf_lines(buf, 0, -1, util.split_lines(stdout))
|
||||
local lines = util.split_lines(stdout)
|
||||
format_header_dates(lines)
|
||||
util.set_buf_lines(buf, 0, -1, lines)
|
||||
state.sha = rev_sha
|
||||
return true
|
||||
end
|
||||
@@ -296,6 +321,10 @@ local function side_buf(r, blob, path)
|
||||
if full then
|
||||
local buf = M.buf_for(r, Revision.new({ base = full }))
|
||||
set_ft_from_path(buf, path)
|
||||
local state = r:state(buf)
|
||||
if state then
|
||||
state.blob_path = path
|
||||
end
|
||||
return buf
|
||||
end
|
||||
local p = vim.fs.joinpath(r.worktree, path)
|
||||
@@ -308,10 +337,45 @@ local function side_buf(r, blob, path)
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param commit string
|
||||
---@param parent boolean
|
||||
---@param path string
|
||||
---@return integer?
|
||||
local function commit_side_buf(r, commit, parent, path)
|
||||
local rev = parent and (commit .. "^") or commit
|
||||
local sha = r:rev_parse(rev, false)
|
||||
if not sha then
|
||||
return nil
|
||||
end
|
||||
if not r:rev_parse(sha .. ":" .. path, false) then
|
||||
return nil
|
||||
end
|
||||
return M.buf_for(r, Revision.new({ base = sha, path = path }))
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param commit string?
|
||||
---@param parent boolean
|
||||
---@param blob string?
|
||||
---@param path string
|
||||
local function load_side(r, blob, path)
|
||||
local buf = side_buf(r, blob, path)
|
||||
---@return integer?
|
||||
local function side(r, commit, parent, blob, path)
|
||||
if commit then
|
||||
local buf = commit_side_buf(r, commit, parent, path)
|
||||
if buf then
|
||||
return buf
|
||||
end
|
||||
end
|
||||
return side_buf(r, blob, path)
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param commit string?
|
||||
---@param parent boolean
|
||||
---@param blob string?
|
||||
---@param path string
|
||||
local function load_side(r, commit, parent, blob, path)
|
||||
local buf = side(r, commit, parent, blob, path)
|
||||
if not buf then
|
||||
util.error("no content for %s", path)
|
||||
return
|
||||
@@ -321,14 +385,15 @@ local function load_side(r, blob, path)
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param commit string?
|
||||
---@param section ow.Git.DiffSection
|
||||
local function open_section(r, section)
|
||||
local function open_section(r, commit, section)
|
||||
if not section.blob_a or not section.blob_b then
|
||||
util.error("no index line, cannot determine blob SHAs")
|
||||
return
|
||||
end
|
||||
local left = side_buf(r, section.blob_a, section.path_a)
|
||||
local right = side_buf(r, section.blob_b, section.path_b)
|
||||
local left = side(r, commit, true, section.blob_a, section.path_a)
|
||||
local right = side(r, commit, false, section.blob_b, section.path_b)
|
||||
if left and right then
|
||||
vim.cmd.normal({ "m'", bang = true })
|
||||
vim.api.nvim_set_current_buf(right)
|
||||
@@ -348,6 +413,21 @@ local function open_section(r, section)
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param buf integer
|
||||
---@return string?
|
||||
local function commit_in_buf(r, buf)
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
if not vim.startswith(name, M.URI_PREFIX) then
|
||||
return nil
|
||||
end
|
||||
local rev = M.parse_uri(name)
|
||||
if not rev or not rev.base or rev.path then
|
||||
return nil
|
||||
end
|
||||
return r:rev_parse(rev.base .. "^{commit}", false)
|
||||
end
|
||||
|
||||
---@class ow.Git.Object.OpenOpts
|
||||
---@field split (false|"above"|"below"|"left"|"right")?
|
||||
|
||||
@@ -394,7 +474,7 @@ function M.open_under_cursor()
|
||||
line:match("^%d+ (%w+) (%x+)\t(.+)$")
|
||||
if entry_sha then
|
||||
if entry_type == "blob" then
|
||||
load_side(r, entry_sha, entry_name --[[@as string]])
|
||||
load_side(r, nil, false, entry_sha, entry_name --[[@as string]])
|
||||
else
|
||||
M.open(r, entry_sha, { split = false })
|
||||
end
|
||||
@@ -406,24 +486,26 @@ function M.open_under_cursor()
|
||||
return false
|
||||
end
|
||||
|
||||
local commit = commit_in_buf(r, vim.api.nvim_get_current_buf())
|
||||
|
||||
if line:match("^diff %-%-git ") then
|
||||
open_section(r, section)
|
||||
open_section(r, commit, section)
|
||||
return true
|
||||
end
|
||||
if line:match("^%-%-%- ") then
|
||||
load_side(r, section.blob_a, section.path_a)
|
||||
load_side(r, commit, true, section.blob_a, section.path_a)
|
||||
return true
|
||||
end
|
||||
if line:match("^%+%+%+ ") then
|
||||
load_side(r, section.blob_b, section.path_b)
|
||||
load_side(r, commit, false, section.blob_b, section.path_b)
|
||||
return true
|
||||
end
|
||||
local prefix = line:sub(1, 1)
|
||||
if prefix == "+" then
|
||||
load_side(r, section.blob_b, section.path_b)
|
||||
load_side(r, commit, false, section.blob_b, section.path_b)
|
||||
return true
|
||||
elseif prefix == "-" then
|
||||
load_side(r, section.blob_a, section.path_a)
|
||||
load_side(r, commit, true, section.blob_a, section.path_a)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
|
||||
+86
-2
@@ -86,9 +86,9 @@ t.test("line popup formats the datetime in the author timezone", function()
|
||||
)
|
||||
t.truthy(
|
||||
(lines[1] or ""):match(
|
||||
"%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d [+%-]%d%d%d%d$"
|
||||
"%u%l%l %u%l%l +%d+ %d%d:%d%d:%d%d %d%d%d%d [+%-]%d%d%d%d$"
|
||||
),
|
||||
"the head line ends with an ISO datetime and numeric timezone"
|
||||
"the head line ends with a human datetime and numeric timezone"
|
||||
)
|
||||
end)
|
||||
|
||||
@@ -467,6 +467,90 @@ t.test("line popup works in a git:// object buffer", function()
|
||||
)
|
||||
end)
|
||||
|
||||
t.test(
|
||||
"line popup works in a git://<blob-sha> buffer with a recovered path",
|
||||
function()
|
||||
local object = require("git.object")
|
||||
local dir = h.make_repo({ ["a.txt"] = "alpha\nbeta\ngamma\n" })
|
||||
local sha = h.git(dir, "rev-parse", "HEAD").stdout
|
||||
local tree = h.git(dir, "rev-parse", "HEAD^{tree}").stdout
|
||||
local blob = h.git(dir, "rev-parse", "HEAD:a.txt").stdout
|
||||
local r = assert(require("git.core.repo").resolve(dir))
|
||||
|
||||
object.open(r, tree, { split = false })
|
||||
local tree_buf = vim.api.nvim_get_current_buf()
|
||||
local found ---@type integer?
|
||||
for i, l in ipairs(vim.api.nvim_buf_get_lines(tree_buf, 0, -1, false)) do
|
||||
if l:match("^%d+ blob %x+\ta%.txt$") then
|
||||
found = i
|
||||
break
|
||||
end
|
||||
end
|
||||
local lnum = assert(found, "expected an a.txt tree entry")
|
||||
vim.api.nvim_win_set_cursor(0, { lnum, 0 })
|
||||
t.truthy(object.open_under_cursor())
|
||||
local gbuf = vim.api.nvim_get_current_buf()
|
||||
t.eq(vim.api.nvim_buf_get_name(gbuf), "git://" .. blob)
|
||||
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
blame.line_popup(gbuf)
|
||||
local float = wait_float()
|
||||
t.defer(function()
|
||||
pcall(vim.api.nvim_win_close, float, true)
|
||||
end)
|
||||
local lines = vim.api.nvim_buf_get_lines(
|
||||
vim.api.nvim_win_get_buf(float),
|
||||
0,
|
||||
-1,
|
||||
false
|
||||
)
|
||||
t.truthy(
|
||||
vim.startswith(lines[1] or "", sha:sub(1, 8)),
|
||||
"the popup blames the commit even in a bare-blob git:// buffer"
|
||||
)
|
||||
end
|
||||
)
|
||||
|
||||
t.test("line popup works in a git://:0: index buffer", function()
|
||||
local object = require("git.object")
|
||||
local Revision = require("git.core.revision")
|
||||
local dir = h.make_repo({ ["a.txt"] = "alpha\nbeta\ngamma\n" })
|
||||
local sha = h.git(dir, "rev-parse", "HEAD").stdout
|
||||
local r = assert(require("git.core.repo").resolve(dir))
|
||||
|
||||
local ibuf = object.buf_for(r, Revision.new({ stage = 0, path = "a.txt" }))
|
||||
vim.api.nvim_set_current_buf(ibuf)
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
blame.line_popup(ibuf)
|
||||
local float = wait_float()
|
||||
t.defer(function()
|
||||
pcall(vim.api.nvim_win_close, float, true)
|
||||
end)
|
||||
local lines = vim.api.nvim_buf_get_lines(
|
||||
vim.api.nvim_win_get_buf(float),
|
||||
0,
|
||||
-1,
|
||||
false
|
||||
)
|
||||
t.truthy(
|
||||
vim.startswith(lines[1] or "", sha:sub(1, 8)),
|
||||
"the popup blames the commit for an index entry"
|
||||
)
|
||||
end)
|
||||
|
||||
t.test("line popup skips a bare git://<commit-sha> object dump", function()
|
||||
local object = require("git.object")
|
||||
local dir = h.make_repo({ ["a.txt"] = "alpha\n" })
|
||||
local r = assert(require("git.core.repo").resolve(dir))
|
||||
|
||||
object.open(r, "HEAD", { split = false })
|
||||
local cbuf = vim.api.nvim_get_current_buf()
|
||||
t.quietly(function()
|
||||
blame.line_popup(cbuf)
|
||||
end)
|
||||
t.eq(blame.state(cbuf), nil, "no blame state on a commit dump")
|
||||
end)
|
||||
|
||||
t.test("open_commit opens the commit that last touched the line", function()
|
||||
local _, buf = setup("alpha\nbeta\ngamma\n")
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
|
||||
@@ -134,6 +134,45 @@ t.test("read_uri opens stage-0 entry as a writable index buffer", function()
|
||||
t.eq(vim.api.nvim_buf_get_lines(buf, 0, -1, false), { "first" })
|
||||
end)
|
||||
|
||||
t.test("M.open renders author/committer dates, not unix timestamps", function()
|
||||
local dir = h.make_repo({ a = "first\n" })
|
||||
local r = assert(require("git.core.repo").resolve(dir))
|
||||
|
||||
object.open(r, "HEAD", { split = false })
|
||||
local author = assert(find_line(0, "author "), "expected an author line")
|
||||
local committer =
|
||||
assert(find_line(0, "committer "), "expected a committer line")
|
||||
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||
for _, lnum in ipairs({ author, committer }) do
|
||||
local line = assert(lines[lnum])
|
||||
t.truthy(
|
||||
line:match(
|
||||
" %u%l%l %u%l%l +%d+ %d%d:%d%d:%d%d %d%d%d%d [+-]%d%d%d%d$"
|
||||
),
|
||||
"expected formatted date on: " .. line
|
||||
)
|
||||
t.falsy(
|
||||
line:match(" %d%d%d%d%d%d%d%d%d%d? [+-]%d%d%d%d$"),
|
||||
"expected no unix timestamp on: " .. line
|
||||
)
|
||||
end
|
||||
end)
|
||||
|
||||
t.test("M.open renders tagger dates on annotated tags", function()
|
||||
local dir = h.make_repo({ a = "first\n" })
|
||||
h.git(dir, "tag", "-m", "rel", "v1")
|
||||
local r = assert(require("git.core.repo").resolve(dir))
|
||||
|
||||
object.open(r, "v1", { split = false })
|
||||
local tagger = assert(find_line(0, "tagger "), "expected a tagger line")
|
||||
local line =
|
||||
assert(vim.api.nvim_buf_get_lines(0, tagger - 1, tagger, false)[1])
|
||||
t.truthy(
|
||||
line:match(" %u%l%l %u%l%l +%d+ %d%d:%d%d:%d%d %d%d%d%d [+-]%d%d%d%d$"),
|
||||
"expected formatted date on: " .. line
|
||||
)
|
||||
end)
|
||||
|
||||
t.test("open_under_cursor on a 'tree <sha>' line opens the tree", function()
|
||||
local dir = h.make_repo({ a = "first\n" })
|
||||
local r = assert(require("git.core.repo").resolve(dir))
|
||||
@@ -163,18 +202,58 @@ t.test("open_under_cursor on a 'parent <sha>' line opens the parent", function()
|
||||
t.eq(vim.api.nvim_buf_get_name(0), "git://" .. parent_sha)
|
||||
end)
|
||||
|
||||
t.test("open_under_cursor on a '+++ b/<path>' line loads the blob", function()
|
||||
t.test(
|
||||
"open_under_cursor on a '+++ b/<path>' line opens the file at the commit",
|
||||
function()
|
||||
local dir = h.make_repo({ ["a.txt"] = "first\n" })
|
||||
local r = assert(require("git.core.repo").resolve(dir))
|
||||
local blob_sha = h.git(dir, "rev-parse", "HEAD:a.txt").stdout
|
||||
local commit_sha = h.git(dir, "rev-parse", "HEAD").stdout
|
||||
|
||||
object.open(r, "HEAD", { split = false })
|
||||
local lnum = assert(find_line(0, "+++ b/a.txt"), "expected a +++ line")
|
||||
vim.api.nvim_win_set_cursor(0, { lnum, 0 })
|
||||
|
||||
t.truthy(object.open_under_cursor())
|
||||
t.eq(vim.api.nvim_buf_get_name(0), "git://" .. commit_sha .. ":a.txt")
|
||||
end
|
||||
)
|
||||
|
||||
t.test(
|
||||
"open_under_cursor on a '--- a/<path>' line opens the parent at that path",
|
||||
function()
|
||||
local dir = h.make_repo({ ["a.txt"] = "first\n" })
|
||||
t.write(dir, "a.txt", "second\n")
|
||||
h.git(dir, "add", "a.txt")
|
||||
h.git(dir, "commit", "-q", "-m", "second")
|
||||
local r = assert(require("git.core.repo").resolve(dir))
|
||||
local parent_sha = h.git(dir, "rev-parse", "HEAD~").stdout
|
||||
|
||||
object.open(r, "HEAD", { split = false })
|
||||
local lnum = assert(find_line(0, "--- a/a.txt"), "expected a --- line")
|
||||
vim.api.nvim_win_set_cursor(0, { lnum, 0 })
|
||||
|
||||
t.truthy(object.open_under_cursor())
|
||||
t.eq(vim.api.nvim_buf_get_name(0), "git://" .. parent_sha .. ":a.txt")
|
||||
end
|
||||
)
|
||||
|
||||
t.test(
|
||||
"open_under_cursor on a tree-entry blob still uses the bare blob URI",
|
||||
function()
|
||||
local dir = h.make_repo({ ["a.txt"] = "first\n" })
|
||||
local r = assert(require("git.core.repo").resolve(dir))
|
||||
local tree_sha = h.git(dir, "rev-parse", "HEAD^{tree}").stdout
|
||||
local blob_sha = h.git(dir, "rev-parse", "HEAD:a.txt").stdout
|
||||
|
||||
object.open(r, tree_sha, { split = false })
|
||||
local lnum =
|
||||
assert(find_line(0, "100644 blob "), "expected a tree entry line")
|
||||
vim.api.nvim_win_set_cursor(0, { lnum, 0 })
|
||||
|
||||
t.truthy(object.open_under_cursor())
|
||||
t.eq(vim.api.nvim_buf_get_name(0), "git://" .. blob_sha)
|
||||
end)
|
||||
end
|
||||
)
|
||||
|
||||
t.test("open_under_cursor returns false on a non-dispatchable line", function()
|
||||
local dir = h.make_repo({ a = "first\n" })
|
||||
|
||||
Reference in New Issue
Block a user