diff --git a/doc/pending.txt b/doc/pending.txt index 9b30047..90db8ee 100644 --- a/doc/pending.txt +++ b/doc/pending.txt @@ -1501,6 +1501,7 @@ Configuration: ~ vim.g.pending = { forge = { close = false, + validate = false, warn_missing_cli = true, github = { icon = '', @@ -1527,6 +1528,10 @@ Top-level fields: ~ done on buffer open. Only forges with an explicit per-forge key (e.g. `github = {}`) are checked; unconfigured forges are skipped entirely. + {validate} (boolean, default: false) When true, new or changed + forge refs are validated on `:w` by fetching metadata. + Logs a warning if the ref is not found, auth fails, or + the CLI is missing. {warn_missing_cli} (boolean, default: true) When true, warns once per forge per session if the CLI is missing or fails. diff --git a/lua/pending/config.lua b/lua/pending/config.lua index 60775fe..2ec13cc 100644 --- a/lua/pending/config.lua +++ b/lua/pending/config.lua @@ -41,6 +41,7 @@ ---@class pending.ForgeConfig ---@field close? boolean +---@field validate? boolean ---@field warn_missing_cli? boolean ---@field [string] pending.ForgeInstanceConfig @@ -155,6 +156,7 @@ local defaults = { sync = {}, forge = { close = false, + validate = false, warn_missing_cli = true, github = { icon = '', diff --git a/lua/pending/diff.lua b/lua/pending/diff.lua index 29c292b..103ba6a 100644 --- a/lua/pending/diff.lua +++ b/lua/pending/diff.lua @@ -82,14 +82,25 @@ function M.parse_buffer(lines) return result end +---@param a? pending.ForgeRef +---@param b? pending.ForgeRef +---@return boolean +local function refs_equal(a, b) + if not a or not b then + return false + end + return a.forge == b.forge and a.owner == b.owner and a.repo == b.repo and a.number == b.number +end + ---@param lines string[] ---@param s pending.Store ---@param hidden_ids? table ----@return nil +---@return pending.ForgeRef[] function M.apply(lines, s, hidden_ids) local parsed = M.parse_buffer(lines) local now = timestamp() local data = s:data() + local new_refs = {} ---@type pending.ForgeRef[] local old_by_id = {} for _, task in ipairs(data.tasks) do @@ -120,6 +131,9 @@ function M.apply(lines, s, hidden_ids) order = order_counter, _extra = entry.forge_ref and { _forge_ref = entry.forge_ref } or nil, }) + if entry.forge_ref then + table.insert(new_refs, entry.forge_ref) + end else seen_ids[entry.id] = true local task = old_by_id[entry.id] @@ -154,6 +168,10 @@ function M.apply(lines, s, hidden_ids) end end if entry.forge_ref ~= nil then + local old_ref = task._extra and task._extra._forge_ref or nil + if not refs_equal(old_ref, entry.forge_ref) then + table.insert(new_refs, entry.forge_ref) + end if not task._extra then task._extra = {} end @@ -188,6 +206,9 @@ function M.apply(lines, s, hidden_ids) order = order_counter, _extra = entry.forge_ref and { _forge_ref = entry.forge_ref } or nil, }) + if entry.forge_ref then + table.insert(new_refs, entry.forge_ref) + end end ::continue:: @@ -202,6 +223,7 @@ function M.apply(lines, s, hidden_ids) end s:save() + return new_refs end return M diff --git a/lua/pending/init.lua b/lua/pending/init.lua index 6c5eaee..9b642ed 100644 --- a/lua/pending/init.lua +++ b/lua/pending/init.lua @@ -489,9 +489,13 @@ function M._on_write(bufnr) if #stack > UNDO_MAX then table.remove(stack, 1) end - diff.apply(lines, s, hidden) + local new_refs = diff.apply(lines, s, hidden) M._recompute_counts() buffer.render(bufnr) + if new_refs and #new_refs > 0 then + local forge = require('pending.forge') + forge.validate_refs(new_refs) + end end ---@return nil diff --git a/spec/diff_spec.lua b/spec/diff_spec.lua index 01d8aac..791d7f6 100644 --- a/spec/diff_spec.lua +++ b/spec/diff_spec.lua @@ -275,6 +275,98 @@ describe('diff', function() assert.are.equal('completion', tasks[1].recur_mode) end) + it('returns forge refs for new tasks', function() + local lines = { + '# Inbox', + '- [ ] Fix bug gh:user/repo#42', + } + local refs = diff.apply(lines, s) + assert.are.equal(1, #refs) + assert.are.equal('github', refs[1].forge) + assert.are.equal(42, refs[1].number) + end) + + it('returns forge refs for changed refs on existing tasks', function() + s:add({ + description = 'Fix bug gh:user/repo#1', + _extra = { + _forge_ref = { + forge = 'github', + owner = 'user', + repo = 'repo', + type = 'issue', + number = 1, + url = '', + }, + }, + }) + s:save() + local lines = { + '# Todo', + '/1/- [ ] Fix bug gh:user/repo#99', + } + local refs = diff.apply(lines, s) + assert.are.equal(1, #refs) + assert.are.equal(99, refs[1].number) + end) + + it('returns empty when forge ref is unchanged', function() + s:add({ + description = 'Fix bug gh:user/repo#42', + _extra = { + _forge_ref = { + forge = 'github', + owner = 'user', + repo = 'repo', + type = 'issue', + number = 42, + url = '', + }, + }, + }) + s:save() + local lines = { + '# Todo', + '/1/- [ ] Fix bug gh:user/repo#42', + } + local refs = diff.apply(lines, s) + assert.are.equal(0, #refs) + end) + + it('returns empty for tasks without forge refs', function() + local lines = { + '# Inbox', + '- [ ] Plain task', + } + local refs = diff.apply(lines, s) + assert.are.equal(0, #refs) + end) + + it('returns forge refs for duplicated tasks', function() + s:add({ + description = 'Fix bug gh:user/repo#42', + _extra = { + _forge_ref = { + forge = 'github', + owner = 'user', + repo = 'repo', + type = 'issue', + number = 42, + url = '', + }, + }, + }) + s:save() + local lines = { + '# Todo', + '/1/- [ ] Fix bug gh:user/repo#42', + '/1/- [ ] Fix bug gh:user/repo#42', + } + local refs = diff.apply(lines, s) + assert.are.equal(1, #refs) + assert.are.equal(42, refs[1].number) + end) + it('clears priority when [N] is removed from buffer line', function() s:add({ description = 'Task name', priority = 1 }) s:save()