Skip to content

Commit 7ab445c

Browse files
authored
feat(review): add ++untracked= to omit the review untracked section (#414)
1 parent a72d99a commit 7ab445c

5 files changed

Lines changed: 207 additions & 42 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ See the documentation for more information.
7777
Yes. Configure Git to open `$MERGED` with Neovim; diffs.nvim will detect
7878
conflict markers automatically. See `:help diffs.nvim-git-mergetool`.
7979

80+
**Q: Can I exclude untracked files from `:Diff review`?**
81+
82+
Yes. Run `:Diff review ++nountracked`. To make it the default, wrap it in your
83+
own command, e.g. `:command! Review Diff review ++nountracked`.
84+
8085
## Known Limitations
8186

8287
- **Incomplete syntax context**: Treesitter parses each diff hunk in isolation.

doc/diffs.nvim.txt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ Paired-window behavior: ~ *diffs.nvim-paired-windows*
540540
the current row maps to worktree content.
541541

542542

543-
:Diff review [++layout=unified|stacked|split] [review-spec] *:Diff-review*
543+
:Diff review [++layout=...] [++nountracked] [review-spec] *:Diff-review*
544544
Open a repository review. `review` must be the first argument; a revision
545545
literally named `review` must be addressed another way (for example
546546
`refs/heads/review`).
@@ -555,6 +555,14 @@ Paired-window behavior: ~ *diffs.nvim-paired-windows*
555555
using merge-base semantics. With `A..B`, reviews explicit target `B`
556556
against base `A` as a direct two-endpoint comparison.
557557

558+
A current-state review (no explicit target) ends with an Untracked
559+
section listing files not yet tracked by git. Pass `++nountracked` to
560+
omit it. To omit it every time, wrap the command, for example: >vim
561+
command! Review Diff review ++nountracked
562+
<
563+
Explicit-target reviews (`A...B`, `A..B`) never include untracked files,
564+
so the option has no effect there.
565+
558566
`++layout=unified` and `++layout=stacked` both create full generated review
559567
maps. Unified review maps use old and new line-number rails; stacked review
560568
maps use one context-aware line-number rail as described at
@@ -604,6 +612,8 @@ Paired-window behavior: ~ *diffs.nvim-paired-windows*
604612
context-aware line-number rail.
605613
++layout=split (optional) Open the first renderable changed review file
606614
as a paired side-by-side endpoint diff.
615+
++nountracked (optional) Omit the Untracked section of a
616+
current-state review (shown by default).
607617
{review-spec} (string, optional) One of:
608618
- empty: review current repo state against the repo
609619
default branch
@@ -613,12 +623,13 @@ Paired-window behavior: ~ *diffs.nvim-paired-windows*
613623
- `{base}..{target}`: direct review of explicit target
614624
`{target}` against base `{base}`
615625

616-
Completion covers `++layout=...` and git refs.
626+
Completion covers `++layout=...`, `++nountracked`, and git refs.
617627

618628
Examples: >vim
619629
:Diff review
620630
:Diff review ++layout=stacked origin/main
621631
:Diff review ++layout=split
632+
:Diff review ++nountracked
622633
:Diff review origin/main
623634
:vertical Diff review origin/main
624635
:Diff review origin/main...refs/forge/pr/42
@@ -645,6 +656,8 @@ Paired-window behavior: ~ *diffs.nvim-paired-windows*
645656
Use this from async/plugin contexts when the current buffer
646657
is not a reliable repo hint.
647658
{vertical} (boolean, optional) Open the review in a vertical split.
659+
{untracked} (boolean, optional) Include the Untracked section of a
660+
current-state review. Defaults to true.
648661

649662
Utilities: ~
650663
`require('diffs.commands').review_split()` opens the currently

lua/diffs/commands.lua

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,10 @@ local layout_options = {
643643
'++layout=split',
644644
}
645645

646+
local untracked_options = {
647+
'++nountracked',
648+
}
649+
646650
---@param layout? "unified"|"stacked"|"split"
647651
---@return diffs.RailStyle
648652
local function rail_style_for_layout(layout)
@@ -739,28 +743,32 @@ end
739743

740744
---@param args string[]
741745
---@param from? integer # first argument index to scan (defaults to 1)
742-
---@return { has_layout: boolean, has_value: boolean }
746+
---@return { has_layout: boolean, has_untracked: boolean, has_value: boolean }
743747
local function args_context(args, from)
744748
local has_layout = false
749+
local has_untracked = false
745750
local has_value = false
746751
for i = from or 1, #args do
747752
local token = args[i]
748753
if token:match('^%+%+layout=') then
749754
has_layout = true
755+
elseif token == '++nountracked' then
756+
has_untracked = true
750757
elseif not token:match('^%+%+') then
751758
has_value = true
752759
end
753760
end
754761
return {
755762
has_layout = has_layout,
763+
has_untracked = has_untracked,
756764
has_value = has_value,
757765
}
758766
end
759767

760768
---@param arglead string
761769
---@param cmdline? string
762770
---@param cursorpos? integer
763-
---@return { has_layout: boolean, has_value: boolean }
771+
---@return { has_layout: boolean, has_untracked: boolean, has_value: boolean }
764772
local function completion_context(arglead, cmdline, cursorpos)
765773
return args_context(command_arg_tokens(arglead, cmdline, cursorpos))
766774
end
@@ -789,7 +797,7 @@ local function complete_diff_args(arglead, cmdline, cursorpos)
789797
end
790798

791799
---@param arglead string
792-
---@param context { has_layout: boolean, has_value: boolean }
800+
---@param context { has_layout: boolean, has_untracked: boolean, has_value: boolean }
793801
---@return string[]
794802
local function complete_review_args(arglead, context)
795803
if context.has_value then
@@ -800,13 +808,19 @@ local function complete_review_args(arglead, context)
800808
if not context.has_layout then
801809
vim.list_extend(matches, prefix_matches(layout_options, arglead))
802810
end
811+
if not context.has_untracked then
812+
vim.list_extend(matches, prefix_matches(untracked_options, arglead))
813+
end
803814
vim.list_extend(matches, review.complete(arglead))
804815
return matches
805816
end
806817
local matches = {}
807818
if arglead == '' and not context.has_layout then
808819
vim.list_extend(matches, layout_options)
809820
end
821+
if arglead == '' and not context.has_untracked then
822+
vim.list_extend(matches, untracked_options)
823+
end
810824
vim.list_extend(matches, review.complete(arglead))
811825
return matches
812826
end
@@ -1106,6 +1120,7 @@ local function stored_review_spec(normalized)
11061120
base = normalized.base,
11071121
target = normalized.target,
11081122
mode = normalized.mode,
1123+
untracked = normalized.untracked,
11091124
}
11101125
end
11111126

lua/diffs/review.lua

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ local notify = log.notify
1717
---@field repo? string
1818
---@field mode? string
1919
---@field vertical? boolean
20+
---@field untracked? boolean
2021

2122
---@class diffs.ReviewCommandParseResult
2223
---@field spec diffs.ReviewSpec
@@ -28,6 +29,7 @@ local notify = log.notify
2829
---@field repo_root string
2930
---@field mode string?
3031
---@field vertical boolean
32+
---@field untracked boolean
3133
---@field display string
3234
---@field exec_args string[]
3335

@@ -179,17 +181,28 @@ function M.parse_command_args(args)
179181
local tokens = split_args(args)
180182
local layout = 'unified'
181183
local has_layout = false
182-
183-
while tokens[1] and tokens[1]:match('^%+%+layout=') do
184-
if has_layout then
185-
return nil, 'repeated ++layout option'
186-
end
187-
local value = tokens[1]:match('^%+%+layout=(.+)$')
188-
if value ~= 'unified' and value ~= 'stacked' and value ~= 'split' then
189-
return nil, 'unsupported layout ' .. tostring(value)
184+
local untracked = nil
185+
local has_untracked = false
186+
187+
while tokens[1] and (tokens[1]:match('^%+%+layout=') or tokens[1] == '++nountracked') do
188+
local token = tokens[1]
189+
if token:match('^%+%+layout=') then
190+
if has_layout then
191+
return nil, 'repeated ++layout option'
192+
end
193+
local value = token:match('^%+%+layout=(.+)$')
194+
if value ~= 'unified' and value ~= 'stacked' and value ~= 'split' then
195+
return nil, 'unsupported layout ' .. tostring(value)
196+
end
197+
has_layout = true
198+
layout = value
199+
else
200+
if has_untracked then
201+
return nil, 'repeated ++nountracked option'
202+
end
203+
has_untracked = true
204+
untracked = false
190205
end
191-
has_layout = true
192-
layout = value
193206
table.remove(tokens, 1)
194207
end
195208

@@ -201,6 +214,9 @@ function M.parse_command_args(args)
201214
if not spec then
202215
return nil, err
203216
end
217+
if has_untracked then
218+
spec.untracked = untracked
219+
end
204220

205221
return {
206222
spec = spec,
@@ -231,6 +247,9 @@ function M.normalize(spec, repo_root_override)
231247
if spec.vertical ~= nil and type(spec.vertical) ~= 'boolean' then
232248
error('diffs: review.vertical must be a boolean')
233249
end
250+
if spec.untracked ~= nil and type(spec.untracked) ~= 'boolean' then
251+
error('diffs: review.untracked must be a boolean')
252+
end
234253

235254
local repo_root = repo_root_override or resolve_repo_root(spec.repo)
236255
if not repo_root then
@@ -278,12 +297,18 @@ function M.normalize(spec, repo_root_override)
278297
return nil, ref_err
279298
end
280299

300+
local untracked = true
301+
if spec.untracked ~= nil then
302+
untracked = spec.untracked
303+
end
304+
281305
return {
282306
base = base,
283307
target = target,
284308
repo_root = repo_root,
285309
mode = mode,
286310
vertical = spec.vertical or false,
311+
untracked = untracked,
287312
display = display,
288313
exec_args = exec_args,
289314
},
@@ -575,36 +600,38 @@ local function run_current_state(review, deps)
575600
lines = unstaged_rendered,
576601
}, unstaged_specs, state)
577602

578-
local untracked, untracked_err = untracked_paths(review)
579-
if not untracked then
580-
return nil,
581-
untracked_err ~= '' and untracked_err or 'git ls-files failed for review Untracked section',
582-
nil
583-
end
584-
local untracked_lines = {}
585-
local untracked_specs = {}
586-
for _, path in ipairs(untracked) do
587-
if is_untracked_binary(review.repo_root, path) then
588-
dbg('skipping binary untracked %s', path)
589-
else
590-
local spec = diffspec.index_to_worktree(path)
591-
local rendered, render_err = render.file(spec, review.repo_root)
592-
if rendered then
593-
for _, line in ipairs(rendered) do
594-
untracked_lines[#untracked_lines + 1] = line
603+
if review.untracked then
604+
local untracked, untracked_err = untracked_paths(review)
605+
if not untracked then
606+
return nil,
607+
untracked_err ~= '' and untracked_err or 'git ls-files failed for review Untracked section',
608+
nil
609+
end
610+
local untracked_lines = {}
611+
local untracked_specs = {}
612+
for _, path in ipairs(untracked) do
613+
if is_untracked_binary(review.repo_root, path) then
614+
dbg('skipping binary untracked %s', path)
615+
else
616+
local spec = diffspec.index_to_worktree(path)
617+
local rendered, render_err = render.file(spec, review.repo_root)
618+
if rendered then
619+
for _, line in ipairs(rendered) do
620+
untracked_lines[#untracked_lines + 1] = line
621+
end
622+
untracked_specs[path] = spec
623+
elseif render_err then
624+
dbg('skipping untracked %s: %s', path, render_err)
595625
end
596-
untracked_specs[path] = spec
597-
elseif render_err then
598-
dbg('skipping untracked %s: %s', path, render_err)
599626
end
600627
end
628+
append_section(lines, {
629+
id = 'untracked',
630+
label = 'Untracked',
631+
description = 'empty -> worktree',
632+
lines = untracked_lines,
633+
}, untracked_specs, state)
601634
end
602-
append_section(lines, {
603-
id = 'untracked',
604-
label = 'Untracked',
605-
description = 'empty -> worktree',
606-
lines = untracked_lines,
607-
}, untracked_specs, state)
608635

609636
return lines,
610637
nil,
@@ -779,12 +806,14 @@ function M.open(spec, deps)
779806
base = review.base,
780807
target = review.target,
781808
mode = review.mode,
809+
untracked = review.untracked,
782810
}),
783811
rail_style = deps.rail_style,
784812
vars = {
785813
diffs_review_base = review.base,
786814
diffs_review_target = review.target,
787815
diffs_review_mode = review.mode,
816+
diffs_review_untracked = review.untracked,
788817
},
789818
})
790819

@@ -847,13 +876,15 @@ function M.reload(bufnr, repo_root, path, deps)
847876
local stored_base = get_buf_var(bufnr, 'diffs_review_base')
848877
local stored_target = get_buf_var(bufnr, 'diffs_review_target')
849878
local stored_mode = get_buf_var(bufnr, 'diffs_review_mode')
879+
local stored_untracked = get_buf_var(bufnr, 'diffs_review_untracked')
850880

851881
local review_spec
852882
if stored_base then
853883
review_spec = {
854884
base = stored_base,
855885
target = stored_target,
856886
mode = stored_mode,
887+
untracked = stored_untracked,
857888
}
858889
else
859890
local parse_err

0 commit comments

Comments
 (0)