Compare commits

..

No commits in common. "600aa89ec72af326ba421f9ce8357fa4da76f925" and "e1923ff2e4da460e1d127d280b0820257fa184b3" have entirely different histories.

3 changed files with 17 additions and 284 deletions

View file

@ -97,7 +97,6 @@ local function update_event(access_token, calendar_id, event_id, task)
summary = task.description,
start = { date = task.due },
['end'] = { date = next_day(task.due or '') },
transparency = 'transparent',
}
local _, err = oauth.curl_request(
'PATCH',

View file

@ -231,9 +231,8 @@ end
---@return integer created
---@return integer updated
---@return integer deleted
---@return integer failed
local function push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
local created, updated, deleted, failed = 0, 0, 0, 0
local created, updated, deleted = 0, 0, 0
for _, task in ipairs(s:tasks()) do
local extra = task._extra or {}
local gtid = extra['_gtasks_task_id'] --[[@as string?]]
@ -243,14 +242,12 @@ local function push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
local err = delete_gtask(access_token, list_id, gtid)
if err then
log.warn('gtasks delete failed: ' .. err)
failed = failed + 1
else
if not task._extra then
task._extra = {}
end
task._extra['_gtasks_task_id'] = nil
task._extra['_gtasks_list_id'] = nil
task._extra['_gtasks_synced_at'] = nil
if next(task._extra) == nil then
task._extra = nil
end
@ -259,17 +256,11 @@ local function push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
end
elseif task.status ~= 'deleted' then
if gtid and list_id then
local synced_at = extra['_gtasks_synced_at'] --[[@as string?]]
if not synced_at or task.modified > synced_at then
local err = update_gtask(access_token, list_id, gtid, task_to_gtask(task))
if err then
log.warn('gtasks update failed: ' .. err)
failed = failed + 1
else
task._extra = task._extra or {}
task._extra['_gtasks_synced_at'] = now_ts
updated = updated + 1
end
local err = update_gtask(access_token, list_id, gtid, task_to_gtask(task))
if err then
log.warn('gtasks update failed: ' .. err)
else
updated = updated + 1
end
elseif task.status == 'pending' then
local cat = task.category or config.get().default_category
@ -278,14 +269,12 @@ local function push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
local new_id, create_err = create_gtask(access_token, lid, task_to_gtask(task))
if create_err then
log.warn('gtasks create failed: ' .. create_err)
failed = failed + 1
elseif new_id then
if not task._extra then
task._extra = {}
end
task._extra['_gtasks_task_id'] = new_id
task._extra['_gtasks_list_id'] = lid
task._extra['_gtasks_synced_at'] = now_ts
task.modified = now_ts
by_gtasks_id[new_id] = task
created = created + 1
@ -294,7 +283,7 @@ local function push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
end
end
end
return created, updated, deleted, failed
return created, updated, deleted
end
---@param access_token string
@ -304,24 +293,14 @@ end
---@param by_gtasks_id table<string, pending.Task>
---@return integer created
---@return integer updated
---@return integer failed
---@return table<string, true> seen_remote_ids
---@return table<string, true> fetched_list_ids
local function pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
local created, updated, failed = 0, 0, 0
---@type table<string, true>
local seen_remote_ids = {}
---@type table<string, true>
local fetched_list_ids = {}
local created, updated = 0, 0
for list_name, list_id in pairs(tasklists) do
local items, err = list_gtasks(access_token, list_id)
if err then
log.warn('error fetching list ' .. list_name .. ': ' .. err)
failed = failed + 1
else
fetched_list_ids[list_id] = true
for _, gtask in ipairs(items or {}) do
seen_remote_ids[gtask.id] = true
local local_task = by_gtasks_id[gtask.id]
if local_task then
local gtask_updated = gtask.updated or ''
@ -331,8 +310,6 @@ local function pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
for k, v in pairs(fields) do
local_task[k] = v
end
local_task._extra = local_task._extra or {}
local_task._extra['_gtasks_synced_at'] = now_ts
local_task.modified = now_ts
updated = updated + 1
end
@ -341,7 +318,6 @@ local function pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
fields._extra = {
_gtasks_task_id = gtask.id,
_gtasks_list_id = list_id,
_gtasks_synced_at = now_ts,
}
local new_task = s:add(fields)
by_gtasks_id[gtask.id] = new_task
@ -350,38 +326,7 @@ local function pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
end
end
end
return created, updated, failed, seen_remote_ids, fetched_list_ids
end
---@param s pending.Store
---@param seen_remote_ids table<string, true>
---@param fetched_list_ids table<string, true>
---@param now_ts string
---@return integer unlinked
local function detect_remote_deletions(s, seen_remote_ids, fetched_list_ids, now_ts)
local unlinked = 0
for _, task in ipairs(s:tasks()) do
local extra = task._extra or {}
local gtid = extra['_gtasks_task_id']
local list_id = extra['_gtasks_list_id']
if
task.status ~= 'deleted'
and gtid
and list_id
and fetched_list_ids[list_id]
and not seen_remote_ids[gtid]
then
task._extra['_gtasks_task_id'] = nil
task._extra['_gtasks_list_id'] = nil
task._extra['_gtasks_synced_at'] = nil
if next(task._extra) == nil then
task._extra = nil
end
task.modified = now_ts
unlinked = unlinked + 1
end
end
return unlinked
return created, updated
end
---@param access_token string
@ -429,17 +374,14 @@ function M.push()
---@cast s pending.Store
---@cast now_ts string
local by_gtasks_id = build_id_index(s)
local created, updated, deleted, failed =
push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
local created, updated, deleted = push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
s:save()
require('pending')._recompute_counts()
local buffer = require('pending.buffer')
if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
buffer.render(buffer.bufnr())
end
log.info(
string.format('Google Tasks pushed — +%d ~%d -%d !%d', created, updated, deleted, failed)
)
log.info(string.format('Google Tasks pushed — +%d ~%d -%d', created, updated, deleted))
end)
end
@ -452,24 +394,14 @@ function M.pull()
---@cast s pending.Store
---@cast now_ts string
local by_gtasks_id = build_id_index(s)
local created, updated, failed, seen_remote_ids, fetched_list_ids =
pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
local unlinked = detect_remote_deletions(s, seen_remote_ids, fetched_list_ids, now_ts)
local created, updated = pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
s:save()
require('pending')._recompute_counts()
local buffer = require('pending.buffer')
if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
buffer.render(buffer.bufnr())
end
log.info(
string.format(
'Google Tasks pulled — +%d ~%d !%d, unlinked: %d',
created,
updated,
failed,
unlinked
)
)
log.info(string.format('Google Tasks pulled — +%d ~%d', created, updated))
end)
end
@ -482,11 +414,9 @@ function M.sync()
---@cast s pending.Store
---@cast now_ts string
local by_gtasks_id = build_id_index(s)
local pushed_create, pushed_update, pushed_delete, pushed_failed =
local pushed_create, pushed_update, pushed_delete =
push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
local pulled_create, pulled_update, pulled_failed, seen_remote_ids, fetched_list_ids =
pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
local unlinked = detect_remote_deletions(s, seen_remote_ids, fetched_list_ids, now_ts)
local pulled_create, pulled_update = pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
s:save()
require('pending')._recompute_counts()
local buffer = require('pending.buffer')
@ -495,15 +425,12 @@ function M.sync()
end
log.info(
string.format(
'Google Tasks synced — push: +%d ~%d -%d !%d, pull: +%d ~%d !%d, unlinked: %d',
'Google Tasks synced — push: +%d ~%d -%d, pull: +%d ~%d',
pushed_create,
pushed_update,
pushed_delete,
pushed_failed,
pulled_create,
pulled_update,
pulled_failed,
unlinked
pulled_update
)
)
end)
@ -515,9 +442,6 @@ M._build_notes = build_notes
M._parse_notes = parse_notes
M._task_to_gtask = task_to_gtask
M._gtask_to_fields = gtask_to_fields
M._push_pass = push_pass
M._pull_pass = pull_pass
M._detect_remote_deletions = detect_remote_deletions
---@return nil
function M.health()

View file

@ -176,193 +176,3 @@ describe('gtasks field conversion', function()
end)
end)
end)
describe('gtasks push_pass _gtasks_synced_at', function()
local helpers = require('spec.helpers')
local store_mod = require('pending.store')
local oauth = require('pending.sync.oauth')
local s
local orig_curl
before_each(function()
local dir = helpers.tmpdir()
s = store_mod.new(dir .. '/pending.json')
s:load()
orig_curl = oauth.curl_request
end)
after_each(function()
oauth.curl_request = orig_curl
end)
it('sets _gtasks_synced_at after push create', function()
local task =
s:add({ description = 'New task', status = 'pending', category = 'Work', priority = 0 })
oauth.curl_request = function(method, url, _headers, _body)
if method == 'POST' and url:find('/tasks$') then
return { id = 'gtask-new-1' }, nil
end
return {}, nil
end
local now_ts = '2026-03-05T10:00:00Z'
local tasklists = { Work = 'list-1' }
local by_id = {}
gtasks._push_pass('fake-token', tasklists, s, now_ts, by_id)
assert.is_not_nil(task._extra)
assert.equals('2026-03-05T10:00:00Z', task._extra['_gtasks_synced_at'])
end)
it('skips update when modified <= _gtasks_synced_at', function()
local task =
s:add({ description = 'Existing task', status = 'pending', category = 'Work', priority = 0 })
task._extra = {
_gtasks_task_id = 'remote-1',
_gtasks_list_id = 'list-1',
_gtasks_synced_at = '2026-03-05T10:00:00Z',
}
task.modified = '2026-03-05T09:00:00Z'
local patch_called = false
oauth.curl_request = function(method, _url, _headers, _body)
if method == 'PATCH' then
patch_called = true
end
return {}, nil
end
local now_ts = '2026-03-05T11:00:00Z'
local tasklists = { Work = 'list-1' }
local by_id = { ['remote-1'] = task }
gtasks._push_pass('fake-token', tasklists, s, now_ts, by_id)
assert.is_false(patch_called)
end)
it('pushes update when modified > _gtasks_synced_at', function()
local task =
s:add({ description = 'Changed task', status = 'pending', category = 'Work', priority = 0 })
task._extra = {
_gtasks_task_id = 'remote-2',
_gtasks_list_id = 'list-1',
_gtasks_synced_at = '2026-03-05T08:00:00Z',
}
task.modified = '2026-03-05T09:00:00Z'
local patch_called = false
oauth.curl_request = function(method, _url, _headers, _body)
if method == 'PATCH' then
patch_called = true
end
return {}, nil
end
local now_ts = '2026-03-05T11:00:00Z'
local tasklists = { Work = 'list-1' }
local by_id = { ['remote-2'] = task }
gtasks._push_pass('fake-token', tasklists, s, now_ts, by_id)
assert.is_true(patch_called)
end)
it('pushes update when no _gtasks_synced_at (backwards compat)', function()
local task =
s:add({ description = 'Old task', status = 'pending', category = 'Work', priority = 0 })
task._extra = {
_gtasks_task_id = 'remote-3',
_gtasks_list_id = 'list-1',
}
task.modified = '2026-01-01T00:00:00Z'
local patch_called = false
oauth.curl_request = function(method, _url, _headers, _body)
if method == 'PATCH' then
patch_called = true
end
return {}, nil
end
local now_ts = '2026-03-05T11:00:00Z'
local tasklists = { Work = 'list-1' }
local by_id = { ['remote-3'] = task }
gtasks._push_pass('fake-token', tasklists, s, now_ts, by_id)
assert.is_true(patch_called)
end)
end)
describe('gtasks detect_remote_deletions', function()
local helpers = require('spec.helpers')
local store_mod = require('pending.store')
local s
before_each(function()
local dir = helpers.tmpdir()
s = store_mod.new(dir .. '/pending.json')
s:load()
end)
it('clears remote IDs when list was fetched but task ID is absent', function()
local task =
s:add({ description = 'Gone remote', status = 'pending', category = 'Work', priority = 0 })
task._extra = {
_gtasks_task_id = 'old-remote-id',
_gtasks_list_id = 'list-1',
_gtasks_synced_at = '2026-01-01T00:00:00Z',
}
local seen = {}
local fetched = { ['list-1'] = true }
local now_ts = '2026-03-05T10:00:00Z'
local unlinked = gtasks._detect_remote_deletions(s, seen, fetched, now_ts)
assert.equals(1, unlinked)
assert.is_nil(task._extra)
assert.equals('2026-03-05T10:00:00Z', task.modified)
end)
it('leaves task untouched when its list fetch failed', function()
local task = s:add({
description = 'Unknown list task',
status = 'pending',
category = 'Work',
priority = 0,
})
task._extra = {
_gtasks_task_id = 'remote-id',
_gtasks_list_id = 'list-unfetched',
}
local seen = {}
local fetched = {}
local now_ts = '2026-03-05T10:00:00Z'
local unlinked = gtasks._detect_remote_deletions(s, seen, fetched, now_ts)
assert.equals(0, unlinked)
assert.is_not_nil(task._extra)
assert.equals('remote-id', task._extra['_gtasks_task_id'])
end)
it('skips tasks with status == deleted', function()
local task =
s:add({ description = 'Deleted task', status = 'deleted', category = 'Work', priority = 0 })
task._extra = {
_gtasks_task_id = 'remote-del',
_gtasks_list_id = 'list-1',
}
local seen = {}
local fetched = { ['list-1'] = true }
local now_ts = '2026-03-05T10:00:00Z'
local unlinked = gtasks._detect_remote_deletions(s, seen, fetched, now_ts)
assert.equals(0, unlinked)
assert.is_not_nil(task._extra)
assert.equals('remote-del', task._extra['_gtasks_task_id'])
end)
end)