commit dc8235c48c8be65659f75461d8cb185ab1941d0f
parent cfbc03a9549017984a2ca9c139553aac1c016a42
Author: Evgeni Chasnovski <evgeni.chasnovski@gmail.com>
Date: Sat, 4 Oct 2025 16:15:54 +0300
feat(pack): prefer using revision from lockfile during install
Problem: Installing plugin always pulls latest `version` changes
(usually from the default branch or "latest version tag"). It is more
robust to prefer initial installation to use the latest recorded
(i.e. "working") revision.
Solution: Prefer using revision from the lockfile (if present) during
install. The extra `update()` will pull the latest changes.
Diffstat:
3 files changed, 51 insertions(+), 5 deletions(-)
diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt
@@ -224,8 +224,9 @@ tags following semver convention `v<major>.<minor>.<patch>`.
The latest state of all managed plugins is stored inside a *vim.pack-lockfile*
located at `$XDG_CONFIG_HOME/nvim/nvim-pack-lock.json`. It is a JSON file that
is used to persistently track data about plugins. For a more robust config
-treat lockfile like its part: put under version control, etc. Should not be
-edited by hand or deleted.
+treat lockfile like its part: put under version control, etc. In this case
+initial install prefers revision from the lockfile instead of inferring from
+`version`. Should not be edited by hand or deleted.
Example workflows ~
@@ -260,7 +261,8 @@ Basic install and management:
plugin1 = require('plugin1')
<
• Restart Nvim (for example, with |:restart|). Plugins that were not yet
- installed will be available on disk in target state after `add()` call.
+ installed will be available on disk after `add()` call. Their revision is
+ taken from |vim.pack-lockfile| (if present) or inferred from the `version`.
• To update all plugins with new changes:
• Execute |vim.pack.update()|. This will download updates from source and
show confirmation buffer in a separate tabpage.
diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua
@@ -18,7 +18,8 @@
---located at `$XDG_CONFIG_HOME/nvim/nvim-pack-lock.json`. It is a JSON file that
---is used to persistently track data about plugins.
---For a more robust config treat lockfile like its part: put under version control, etc.
----Should not be edited by hand or deleted.
+---In this case initial install prefers revision from the lockfile instead of
+---inferring from `version`. Should not be edited by hand or deleted.
---
---Example workflows ~
---
@@ -56,7 +57,8 @@
---```
---
---- Restart Nvim (for example, with |:restart|). Plugins that were not yet
----installed will be available on disk in target state after `add()` call.
+---installed will be available on disk after `add()` call. Their revision is
+---taken from |vim.pack-lockfile| (if present) or inferred from the `version`.
---
---- To update all plugins with new changes:
--- - Execute |vim.pack.update()|. This will download updates from source and
@@ -625,6 +627,9 @@ local function install_list(plug_list, confirm)
plugin_lock.plugins[p.spec.name].src = p.spec.src
+ -- Prefer revision from the lockfile instead of using `version`
+ p.info.sha_target = (plugin_lock.plugins[p.spec.name] or {}).rev
+
-- Do not skip checkout even if HEAD and target have same commit hash to
-- have new repo in expected detached HEAD state and generated help files.
checkout(p, timestamp, false)
diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua
@@ -341,6 +341,8 @@ describe('vim.pack', function()
after_each(function()
vim.fs.rm(pack_get_dir(), { force = true, recursive = true })
vim.fs.rm(get_lock_path(), { force = true })
+ local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log')
+ pcall(vim.fs.rm, log_path, { force = true })
end)
teardown(function()
@@ -502,6 +504,43 @@ describe('vim.pack', function()
eq(ref_lockfile, get_lock_tbl())
end)
+ it('uses lockfile revision during install', function()
+ exec_lua(function()
+ vim.pack.add({ { src = repos_src.basic, version = 'feat-branch' } })
+ end)
+
+ -- Mock clean initial install, but with lockfile present
+ n.clear()
+ local basic_plug_path = vim.fs.joinpath(pack_get_dir(), 'basic')
+ vim.fs.rm(basic_plug_path, { force = true, recursive = true })
+
+ local basic_rev = git_get_hash('feat-branch', 'basic')
+ local ref_lockfile = {
+ plugins = {
+ basic = { rev = basic_rev, src = repos_src.basic, version = "'feat-branch'" },
+ },
+ }
+ eq(ref_lockfile, get_lock_tbl())
+
+ exec_lua(function()
+ -- Should use revision from lockfile (pointing at latest 'feat-branch'
+ -- commit) and not use latest `main` commit
+ vim.pack.add({ { src = repos_src.basic, version = 'main' } })
+ end)
+ local basic_lua_file = vim.fs.joinpath(pack_get_plug_path('basic'), 'lua', 'basic.lua')
+ eq({ 'return "basic feat-branch"' }, fn.readfile(basic_lua_file))
+
+ -- Running `update()` should still update to use `main`
+ exec_lua(function()
+ vim.pack.update(nil, { force = true })
+ end)
+ eq({ 'return "basic main"' }, fn.readfile(basic_lua_file))
+
+ ref_lockfile.plugins.basic.rev = git_get_hash('main', 'basic')
+ ref_lockfile.plugins.basic.version = "'main'"
+ eq(ref_lockfile, get_lock_tbl())
+ end)
+
it('installs at proper version', function()
local out = exec_lua(function()
vim.pack.add({