From 11ce1fed9d46dc7729ad5f4b7733dc7ef2c5d996 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 8 Sep 2026 23:32:04 +1000 Subject: [PATCH] Save the lockfile through a temp file and a rename An interrupted save could leave a truncated lock.json. --- lua/arti-pack/lock.lua | 10 +++++++++- tests/lock_spec.lua | 13 +++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lua/arti-pack/lock.lua b/lua/arti-pack/lock.lua index fdea743..ba01e7a 100644 --- a/lua/arti-pack/lock.lua +++ b/lua/arti-pack/lock.lua @@ -33,15 +33,23 @@ function M:load() return self end +-- Write then rename, so an interrupted save cannot truncate the live lockfile. function M:save() vim.fn.mkdir(dirname(self.path), 'p') local payload = { version = 1, plugins = self.entries } - local fd, err = io.open(self.path, 'w') + local tmp = string.format('%s.%d.tmp', self.path, vim.uv and vim.uv.os_getpid() or vim.loop.os_getpid()) + local fd, err = io.open(tmp, 'w') if not fd then return false, err end fd:write(vim.json.encode(payload)) fd:close() + + local ok, rename_err = os.rename(tmp, self.path) + if not ok then + os.remove(tmp) + return false, rename_err + end return true end diff --git a/tests/lock_spec.lua b/tests/lock_spec.lua index 58d97e8..4b6030b 100644 --- a/tests/lock_spec.lua +++ b/tests/lock_spec.lua @@ -42,6 +42,19 @@ t.describe('lock', function() t.eq({}, Lock.new(path):load():names()) end) + t.it('leaves no temporary file beside the state file', function() + local dir = tmpdir() + local lock = Lock.new(dir .. '/lock.json') + lock:set('plenary.nvim', { tag = 'v0.1.4' }) + t.truthy(lock:save()) + + local names = {} + for _, entry in ipairs(vim.fn.readdir(dir)) do + table.insert(names, entry) + end + t.eq({ 'lock.json' }, names) + end) + t.it('lists names in sorted order', function() local lock = Lock.new(tmpdir() .. '/lock.json') lock:set('zebra', { tag = 'v1' })