Skip to content

Commit 6d047b4

Browse files
committed
3-pane ui, result :write buffer
1 parent dcd6113 commit 6d047b4

12 files changed

Lines changed: 2429 additions & 95 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
**/.plans
22
**/.notes/
33
**/.nvimlog
4+
nvim.log
45
**/.DS_Store
56
**/.pyc
67
**/.venv/

lua/glance/diffview.lua

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -611,37 +611,7 @@ end
611611

612612
--- Open a single editable pane for a conflicted working-tree file.
613613
function M.open_conflict(file)
614-
local root = git.repo_root()
615-
if not root then return end
616-
617-
prepare_default_workspace()
618-
open_single_pane()
619-
620-
local full_path = root .. '/' .. file.path
621-
vim.cmd('edit ' .. vim.fn.fnameescape(full_path))
622-
M.new_buf = vim.api.nvim_get_current_buf()
623-
624-
if watch_options().enabled then
625-
M.watch_file(full_path)
626-
end
627-
628-
apply_conflict_highlights(M.new_buf)
629-
set_window_label(M.new_win, 'Conflict: unresolved markers')
630-
631-
M.equalize_panes()
632-
M.setup_autocmds(file)
633-
M.bind_buffer_keymaps()
634-
635-
local opts = { buffer = M.new_buf, silent = true }
636-
vim.keymap.set('n', ']x', jump_to_next_conflict, opts)
637-
vim.keymap.set('n', '[x', jump_to_prev_conflict, opts)
638-
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, {
639-
group = M.autocmd_group,
640-
buffer = M.new_buf,
641-
callback = function()
642-
apply_conflict_highlights(M.new_buf)
643-
end,
644-
})
614+
require('glance.merge').open(M, file)
645615
end
646616

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

828798
local editable_buf = M.editable_buf()
799+
local merge = package.loaded['glance.merge']
800+
local merge_active = merge and merge.is_active and merge.is_active()
829801

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

843-
vim.api.nvim_create_autocmd('BufWritePost', {
844-
group = M.autocmd_group,
845-
buffer = editable_buf,
846-
callback = function()
847-
vim.schedule(function()
848-
M.refresh(file)
849-
filetree.note_repo_activity()
850-
end)
851-
end,
852-
})
815+
if not merge_active then
816+
vim.api.nvim_create_autocmd('BufWritePost', {
817+
group = M.autocmd_group,
818+
buffer = editable_buf,
819+
callback = function()
820+
vim.schedule(function()
821+
M.refresh(file)
822+
filetree.note_repo_activity()
823+
end)
824+
end,
825+
})
826+
end
853827
end
854828
end
855829

@@ -880,6 +854,14 @@ function M.content_wins()
880854
end
881855

882856
function M.hoverable_separator_wins()
857+
local merge = package.loaded['glance.merge']
858+
if merge and merge.is_active and merge.is_active() then
859+
local wins = merge.hoverable_separator_wins(M)
860+
if wins then
861+
return wins
862+
end
863+
end
864+
883865
sync_filetree_pane()
884866

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

10271009
M.reset_workspace()
10281010

1011+
local merge = package.loaded['glance.merge']
1012+
if merge and merge.reset then
1013+
merge.reset()
1014+
end
1015+
10291016
local ui = require('glance.ui')
10301017
ui.close_diff()
10311018
end, debug.traceback)
@@ -1044,6 +1031,12 @@ function M.refresh(file)
10441031
return
10451032
end
10461033

1034+
local merge = package.loaded['glance.merge']
1035+
if merge and merge.is_active and merge.is_active() then
1036+
merge.refresh(M, file)
1037+
return
1038+
end
1039+
10471040
local kind = git.infer_stage_kind(file)
10481041
local old_lines = git.get_file_content(old_content_path(file), old_content_ref(file))
10491042
local old_side_open = M.old_buf and vim.api.nvim_buf_is_valid(M.old_buf)
@@ -1108,6 +1101,11 @@ end
11081101

11091102
--- Explicitly size panes: file tree gets its fixed width, diff panes split the rest.
11101103
function M.equalize_panes()
1104+
local merge = package.loaded['glance.merge']
1105+
if merge and merge.is_active and merge.is_active() and merge.equalize_panes(M) then
1106+
return
1107+
end
1108+
11111109
sync_filetree_pane()
11121110
local tree_visible = filetree.win and vim.api.nvim_win_is_valid(filetree.win)
11131111
local tree_width = 0

lua/glance/git.lua

Lines changed: 193 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,31 @@ local function git_dir_at_root(root)
9292
return M._git_dir
9393
end
9494

95+
local function read_trimmed_file(path)
96+
if type(path) ~= 'string' or path == '' then
97+
return nil
98+
end
99+
100+
local stat = vim.uv.fs_stat(path)
101+
if not stat or stat.type ~= 'file' then
102+
return nil
103+
end
104+
105+
local file = io.open(path, 'r')
106+
if not file then
107+
return nil
108+
end
109+
110+
local content = file:read('*a')
111+
file:close()
112+
content = vim.trim(content or '')
113+
if content == '' then
114+
return nil
115+
end
116+
117+
return content
118+
end
119+
95120
local function format_timespec(spec)
96121
if type(spec) == 'table' then
97122
return tostring(spec.sec or 0) .. ':' .. tostring(spec.nsec or 0)
@@ -619,6 +644,151 @@ function M.git_dir()
619644
return git_dir_at_root(M.repo_root())
620645
end
621646

647+
function M.get_unmerged_stage_entries(filepath)
648+
if type(filepath) ~= 'string' or filepath == '' then
649+
return {}
650+
end
651+
652+
local ok, output = M.run_git_capture({ 'ls-files', '-u', '--', filepath })
653+
if not ok then
654+
return {}
655+
end
656+
657+
local entries = {}
658+
for line in output:gmatch('[^\n]+') do
659+
local mode, oid, stage, path = line:match('^(%d+)%s+([0-9a-f]+)%s+(%d)%s+(.+)$')
660+
if mode and oid and stage and path then
661+
entries[tonumber(stage)] = {
662+
mode = mode,
663+
oid = oid,
664+
stage = tonumber(stage),
665+
path = path,
666+
}
667+
end
668+
end
669+
670+
return entries
671+
end
672+
673+
local function ref_name_label(ref)
674+
local ok, output = M.run_git_capture({ 'name-rev', '--name-only', '--always', ref }, {
675+
allowed_codes = { 0, 128 },
676+
})
677+
if not ok then
678+
return nil
679+
end
680+
681+
local label = vim.trim(output)
682+
if label == '' or label == 'undefined' or label == ref then
683+
return nil
684+
end
685+
686+
if label:match('^refs/heads/') then
687+
return label:gsub('^refs/heads/', '')
688+
end
689+
690+
return label
691+
end
692+
693+
local function short_ref_oid(ref)
694+
local ok, output = M.run_git_capture({ 'rev-parse', '--short', ref }, {
695+
allowed_codes = { 0, 128 },
696+
})
697+
if not ok then
698+
return nil
699+
end
700+
701+
local oid = vim.trim(output)
702+
if oid == '' then
703+
return nil
704+
end
705+
706+
return oid
707+
end
708+
709+
local function ref_display(ref)
710+
if type(ref) ~= 'string' or ref == '' then
711+
return nil
712+
end
713+
714+
local label = ref_name_label(ref)
715+
if label and label ~= ref then
716+
return ref .. ' (' .. label .. ')'
717+
end
718+
719+
local oid = short_ref_oid(ref)
720+
if oid then
721+
return ref .. ' (' .. oid .. ')'
722+
end
723+
724+
return ref
725+
end
726+
727+
function M.get_operation_context()
728+
local git_dir = M.git_dir()
729+
if not git_dir then
730+
return {
731+
kind = nil,
732+
prefix = nil,
733+
ours_ref = 'HEAD',
734+
ours_display = ref_display('HEAD') or 'HEAD',
735+
theirs_ref = nil,
736+
theirs_display = nil,
737+
}
738+
end
739+
740+
local context = {
741+
kind = nil,
742+
prefix = nil,
743+
ours_ref = 'HEAD',
744+
ours_display = ref_display('HEAD') or 'HEAD',
745+
theirs_ref = nil,
746+
theirs_display = nil,
747+
}
748+
749+
if vim.uv.fs_stat(git_dir .. '/rebase-merge') or vim.uv.fs_stat(git_dir .. '/rebase-apply') then
750+
context.kind = 'rebase'
751+
context.prefix = 'Rebasing'
752+
if vim.uv.fs_stat(git_dir .. '/REBASE_HEAD') then
753+
context.theirs_ref = 'REBASE_HEAD'
754+
context.theirs_display = ref_display('REBASE_HEAD') or 'REBASE_HEAD'
755+
else
756+
local onto = read_trimmed_file(git_dir .. '/rebase-merge/onto')
757+
or read_trimmed_file(git_dir .. '/rebase-apply/onto')
758+
if onto then
759+
context.theirs_ref = onto
760+
context.theirs_display = short_ref_oid(onto) or onto
761+
end
762+
end
763+
return context
764+
end
765+
766+
if vim.uv.fs_stat(git_dir .. '/MERGE_HEAD') then
767+
context.kind = 'merge'
768+
context.theirs_ref = 'MERGE_HEAD'
769+
context.theirs_display = ref_display('MERGE_HEAD') or 'MERGE_HEAD'
770+
return context
771+
end
772+
773+
if vim.uv.fs_stat(git_dir .. '/CHERRY_PICK_HEAD') then
774+
context.kind = 'cherry_pick'
775+
context.prefix = 'Cherry-picking'
776+
context.theirs_ref = 'CHERRY_PICK_HEAD'
777+
context.theirs_display = ref_display('CHERRY_PICK_HEAD') or 'CHERRY_PICK_HEAD'
778+
return context
779+
end
780+
781+
if vim.uv.fs_stat(git_dir .. '/REVERT_HEAD') then
782+
context.kind = 'revert'
783+
context.prefix = 'Reverting'
784+
context.theirs_ref = 'REVERT_HEAD'
785+
context.theirs_display = ref_display('REVERT_HEAD') or 'REVERT_HEAD'
786+
return context
787+
end
788+
789+
return context
790+
end
791+
622792
function M.repo_watch_paths()
623793
local git_dir = M.git_dir()
624794
if not git_dir then
@@ -657,30 +827,22 @@ end
657827
--- Retrieve file content at a specific git ref.
658828
--- @param filepath string Path relative to repo root
659829
--- @param ref string|nil "HEAD", ":" (index), or nil (working tree / disk)
660-
--- @return string[] Lines of file content
661-
function M.get_file_content(filepath, ref)
830+
--- @return string Raw file text
831+
function M.get_file_text(filepath, ref)
662832
if ref == nil then
663833
-- Read from working tree (disk)
664834
local root = M.repo_root()
665835
if not root then
666-
return {}
836+
return ''
667837
end
668838
local full_path = root .. '/' .. filepath
669-
local f = io.open(full_path, 'r')
839+
local f = io.open(full_path, 'rb')
670840
if not f then
671-
return {}
841+
return ''
672842
end
673843
local content = f:read('*a')
674844
f:close()
675-
local lines = {}
676-
for line in (content .. '\n'):gmatch('(.-)\n') do
677-
table.insert(lines, line)
678-
end
679-
-- Remove trailing empty line from the split
680-
if #lines > 0 and lines[#lines] == '' then
681-
table.remove(lines)
682-
end
683-
return lines
845+
return content or ''
684846
end
685847

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

694856
local result = vim.fn.system('git show ' .. vim.fn.shellescape(git_ref) .. ' 2>/dev/null')
695857
if vim.v.shell_error ~= 0 then
858+
return ''
859+
end
860+
861+
return result or ''
862+
end
863+
864+
local function split_content_lines(text)
865+
if type(text) ~= 'string' or text == '' then
696866
return {}
697867
end
698868

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

879+
--- Retrieve file content at a specific git ref.
880+
--- @param filepath string Path relative to repo root
881+
--- @param ref string|nil "HEAD", ":" (index), or nil (working tree / disk)
882+
--- @return string[] Lines of file content
883+
function M.get_file_content(filepath, ref)
884+
return split_content_lines(M.get_file_text(filepath, ref))
885+
end
886+
709887
function M.entry_paths(file)
710888
local paths = {}
711889
local seen = {}

0 commit comments

Comments
 (0)