Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
**/.plans
**/.notes/
**/.nvimlog
nvim.log
**/.DS_Store
**/.pyc
**/.venv/
Expand Down
80 changes: 39 additions & 41 deletions lua/glance/diffview.lua
Original file line number Diff line number Diff line change
Expand Up @@ -611,37 +611,7 @@ end

--- Open a single editable pane for a conflicted working-tree file.
function M.open_conflict(file)
local root = git.repo_root()
if not root then return end

prepare_default_workspace()
open_single_pane()

local full_path = root .. '/' .. file.path
vim.cmd('edit ' .. vim.fn.fnameescape(full_path))
M.new_buf = vim.api.nvim_get_current_buf()

if watch_options().enabled then
M.watch_file(full_path)
end

apply_conflict_highlights(M.new_buf)
set_window_label(M.new_win, 'Conflict: unresolved markers')

M.equalize_panes()
M.setup_autocmds(file)
M.bind_buffer_keymaps()

local opts = { buffer = M.new_buf, silent = true }
vim.keymap.set('n', ']x', jump_to_next_conflict, opts)
vim.keymap.set('n', '[x', jump_to_prev_conflict, opts)
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, {
group = M.autocmd_group,
buffer = M.new_buf,
callback = function()
apply_conflict_highlights(M.new_buf)
end,
})
require('glance.merge').open(M, file)
end

--- Open a single read-only placeholder pane for visible-but-unsupported states.
Expand Down Expand Up @@ -826,6 +796,8 @@ function M.setup_autocmds(file)
})

local editable_buf = M.editable_buf()
local merge = package.loaded['glance.merge']
local merge_active = merge and merge.is_active and merge.is_active()

-- When the workspace's editable buffer is saved, refresh the diff.
if editable_buf and vim.api.nvim_buf_get_option(editable_buf, 'buftype') == '' then
Expand All @@ -840,16 +812,18 @@ function M.setup_autocmds(file)
end,
})

vim.api.nvim_create_autocmd('BufWritePost', {
group = M.autocmd_group,
buffer = editable_buf,
callback = function()
vim.schedule(function()
M.refresh(file)
filetree.note_repo_activity()
end)
end,
})
if not merge_active then
vim.api.nvim_create_autocmd('BufWritePost', {
group = M.autocmd_group,
buffer = editable_buf,
callback = function()
vim.schedule(function()
M.refresh(file)
filetree.note_repo_activity()
end)
end,
})
end
end
end

Expand Down Expand Up @@ -880,6 +854,14 @@ function M.content_wins()
end

function M.hoverable_separator_wins()
local merge = package.loaded['glance.merge']
if merge and merge.is_active and merge.is_active() then
local wins = merge.hoverable_separator_wins(M)
if wins then
return wins
end
end

sync_filetree_pane()

local wins = {}
Expand Down Expand Up @@ -1026,6 +1008,11 @@ function M.close(force)

M.reset_workspace()

local merge = package.loaded['glance.merge']
if merge and merge.reset then
merge.reset()
end

local ui = require('glance.ui')
ui.close_diff()
end, debug.traceback)
Expand All @@ -1044,6 +1031,12 @@ function M.refresh(file)
return
end

local merge = package.loaded['glance.merge']
if merge and merge.is_active and merge.is_active() then
merge.refresh(M, file)
return
end

local kind = git.infer_stage_kind(file)
local old_lines = git.get_file_content(old_content_path(file), old_content_ref(file))
local old_side_open = M.old_buf and vim.api.nvim_buf_is_valid(M.old_buf)
Expand Down Expand Up @@ -1108,6 +1101,11 @@ end

--- Explicitly size panes: file tree gets its fixed width, diff panes split the rest.
function M.equalize_panes()
local merge = package.loaded['glance.merge']
if merge and merge.is_active and merge.is_active() and merge.equalize_panes(M) then
return
end

sync_filetree_pane()
local tree_visible = filetree.win and vim.api.nvim_win_is_valid(filetree.win)
local tree_width = 0
Expand Down
208 changes: 193 additions & 15 deletions lua/glance/git.lua
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,31 @@ local function git_dir_at_root(root)
return M._git_dir
end

local function read_trimmed_file(path)
if type(path) ~= 'string' or path == '' then
return nil
end

local stat = vim.uv.fs_stat(path)
if not stat or stat.type ~= 'file' then
return nil
end

local file = io.open(path, 'r')
if not file then
return nil
end

local content = file:read('*a')
file:close()
content = vim.trim(content or '')
if content == '' then
return nil
end

return content
end

local function format_timespec(spec)
if type(spec) == 'table' then
return tostring(spec.sec or 0) .. ':' .. tostring(spec.nsec or 0)
Expand Down Expand Up @@ -619,6 +644,151 @@ function M.git_dir()
return git_dir_at_root(M.repo_root())
end

function M.get_unmerged_stage_entries(filepath)
if type(filepath) ~= 'string' or filepath == '' then
return {}
end

local ok, output = M.run_git_capture({ 'ls-files', '-u', '--', filepath })
if not ok then
return {}
end

local entries = {}
for line in output:gmatch('[^\n]+') do
local mode, oid, stage, path = line:match('^(%d+)%s+([0-9a-f]+)%s+(%d)%s+(.+)$')
if mode and oid and stage and path then
entries[tonumber(stage)] = {
mode = mode,
oid = oid,
stage = tonumber(stage),
path = path,
}
end
end

return entries
end

local function ref_name_label(ref)
local ok, output = M.run_git_capture({ 'name-rev', '--name-only', '--always', ref }, {
allowed_codes = { 0, 128 },
})
if not ok then
return nil
end

local label = vim.trim(output)
if label == '' or label == 'undefined' or label == ref then
return nil
end

if label:match('^refs/heads/') then
return label:gsub('^refs/heads/', '')
end

return label
end

local function short_ref_oid(ref)
local ok, output = M.run_git_capture({ 'rev-parse', '--short', ref }, {
allowed_codes = { 0, 128 },
})
if not ok then
return nil
end

local oid = vim.trim(output)
if oid == '' then
return nil
end

return oid
end

local function ref_display(ref)
if type(ref) ~= 'string' or ref == '' then
return nil
end

local label = ref_name_label(ref)
if label and label ~= ref then
return ref .. ' (' .. label .. ')'
end

local oid = short_ref_oid(ref)
if oid then
return ref .. ' (' .. oid .. ')'
end

return ref
end

function M.get_operation_context()
local git_dir = M.git_dir()
if not git_dir then
return {
kind = nil,
prefix = nil,
ours_ref = 'HEAD',
ours_display = ref_display('HEAD') or 'HEAD',
theirs_ref = nil,
theirs_display = nil,
}
end

local context = {
kind = nil,
prefix = nil,
ours_ref = 'HEAD',
ours_display = ref_display('HEAD') or 'HEAD',
theirs_ref = nil,
theirs_display = nil,
}

if vim.uv.fs_stat(git_dir .. '/rebase-merge') or vim.uv.fs_stat(git_dir .. '/rebase-apply') then
context.kind = 'rebase'
context.prefix = 'Rebasing'
if vim.uv.fs_stat(git_dir .. '/REBASE_HEAD') then
context.theirs_ref = 'REBASE_HEAD'
context.theirs_display = ref_display('REBASE_HEAD') or 'REBASE_HEAD'
else
local onto = read_trimmed_file(git_dir .. '/rebase-merge/onto')
or read_trimmed_file(git_dir .. '/rebase-apply/onto')
if onto then
context.theirs_ref = onto
context.theirs_display = short_ref_oid(onto) or onto
end
end
return context
end

if vim.uv.fs_stat(git_dir .. '/MERGE_HEAD') then
context.kind = 'merge'
context.theirs_ref = 'MERGE_HEAD'
context.theirs_display = ref_display('MERGE_HEAD') or 'MERGE_HEAD'
return context
end

if vim.uv.fs_stat(git_dir .. '/CHERRY_PICK_HEAD') then
context.kind = 'cherry_pick'
context.prefix = 'Cherry-picking'
context.theirs_ref = 'CHERRY_PICK_HEAD'
context.theirs_display = ref_display('CHERRY_PICK_HEAD') or 'CHERRY_PICK_HEAD'
return context
end

if vim.uv.fs_stat(git_dir .. '/REVERT_HEAD') then
context.kind = 'revert'
context.prefix = 'Reverting'
context.theirs_ref = 'REVERT_HEAD'
context.theirs_display = ref_display('REVERT_HEAD') or 'REVERT_HEAD'
return context
end

return context
end

function M.repo_watch_paths()
local git_dir = M.git_dir()
if not git_dir then
Expand Down Expand Up @@ -657,30 +827,22 @@ end
--- Retrieve file content at a specific git ref.
--- @param filepath string Path relative to repo root
--- @param ref string|nil "HEAD", ":" (index), or nil (working tree / disk)
--- @return string[] Lines of file content
function M.get_file_content(filepath, ref)
--- @return string Raw file text
function M.get_file_text(filepath, ref)
if ref == nil then
-- Read from working tree (disk)
local root = M.repo_root()
if not root then
return {}
return ''
end
local full_path = root .. '/' .. filepath
local f = io.open(full_path, 'r')
local f = io.open(full_path, 'rb')
if not f then
return {}
return ''
end
local content = f:read('*a')
f:close()
local lines = {}
for line in (content .. '\n'):gmatch('(.-)\n') do
table.insert(lines, line)
end
-- Remove trailing empty line from the split
if #lines > 0 and lines[#lines] == '' then
table.remove(lines)
end
return lines
return content or ''
end

-- ref is "HEAD" or ":" (index)
Expand All @@ -693,11 +855,19 @@ function M.get_file_content(filepath, ref)

local result = vim.fn.system('git show ' .. vim.fn.shellescape(git_ref) .. ' 2>/dev/null')
if vim.v.shell_error ~= 0 then
return ''
end

return result or ''
end

local function split_content_lines(text)
if type(text) ~= 'string' or text == '' then
return {}
end

local lines = {}
for line in (result .. '\n'):gmatch('(.-)\n') do
for line in (text .. '\n'):gmatch('(.-)\n') do
table.insert(lines, line)
end
if #lines > 0 and lines[#lines] == '' then
Expand All @@ -706,6 +876,14 @@ function M.get_file_content(filepath, ref)
return lines
end

--- Retrieve file content at a specific git ref.
--- @param filepath string Path relative to repo root
--- @param ref string|nil "HEAD", ":" (index), or nil (working tree / disk)
--- @return string[] Lines of file content
function M.get_file_content(filepath, ref)
return split_content_lines(M.get_file_text(filepath, ref))
end

function M.entry_paths(file)
local paths = {}
local seen = {}
Expand Down
Loading
Loading