fix(pack)!: ensure plugin is fully absent if not fully installed

Problem: Currently it is possible to have plugin in a "partial install"
  state when `git clone` was successfull but `git checkout` was not.
  This was done to not checkout default branch by default in these
  situations (for security reasons).

  The problem is that it adds complexity when both dealing with lockfile
  (plugin's `rev` might be `nil`) and in how `src` and `version` are
  treated (wrong `src` - no plugin on disk; wrong `version` - "partial"
  plugin on disk).

Solution: Treat plugin as "installed" if both `git clone` and
  `git checkout` are successful, while ensuring that not installed
  plugins are not on disk and in lockfile.

  This also means that if in 'init.lua' there is a `vim.pack.add()` with
  bad `version`, for first install there will be an informative error
  about it BUT next session will also try to install it. The solution is
  the same - adjust `version` beforehand.
This commit is contained in:
Evgeni Chasnovski
2025-11-13 15:45:52 +02:00
parent 9e2599df05
commit c3ac329c7a
3 changed files with 11 additions and 11 deletions

View File

@@ -375,7 +375,8 @@ add({specs}, {opts}) *vim.pack.add()*
nothing. nothing.
• If doesn't exist, install it by downloading from `src` into `name` • If doesn't exist, install it by downloading from `src` into `name`
subdirectory (via partial blobless `git clone`) and update revision to subdirectory (via partial blobless `git clone`) and update revision to
match `version` (via `git checkout`). match `version` (via `git checkout`). Plugin will not be on disk if
any step resulted in an error.
• For each plugin execute |:packadd| (or customizable `load` function) • For each plugin execute |:packadd| (or customizable `load` function)
making it reachable by Nvim. making it reachable by Nvim.

View File

@@ -677,7 +677,6 @@ local function install_list(plug_list, confirm)
trigger_event(p, 'PackChangedPre', 'install') trigger_event(p, 'PackChangedPre', 'install')
git_clone(p.spec.src, p.path) git_clone(p.spec.src, p.path)
p.info.installed = true
plugin_lock.plugins[p.spec.name].src = p.spec.src plugin_lock.plugins[p.spec.name].src = p.spec.src
@@ -685,6 +684,7 @@ local function install_list(plug_list, confirm)
p.info.sha_target = (plugin_lock.plugins[p.spec.name] or {}).rev p.info.sha_target = (plugin_lock.plugins[p.spec.name] or {}).rev
checkout(p, timestamp) checkout(p, timestamp)
p.info.installed = true
trigger_event(p, 'PackChanged', 'install') trigger_event(p, 'PackChanged', 'install')
end end
@@ -694,10 +694,11 @@ local function install_list(plug_list, confirm)
run_list(plug_list, do_install, 'Installing plugins') run_list(plug_list, do_install, 'Installing plugins')
end end
-- Ensure that not installed plugins are absent in lockfile -- Ensure that not fully installed plugins are absent on disk and in lockfile
for _, p in ipairs(plug_list) do for _, p in ipairs(plug_list) do
if not p.info.installed then if not p.info.installed then
plugin_lock.plugins[p.spec.name] = nil plugin_lock.plugins[p.spec.name] = nil
vim.fs.rm(p.path, { recursive = true, force = true })
end end
end end
end end
@@ -800,7 +801,8 @@ end
--- immediately to clean install from the new source. Otherwise do nothing. --- immediately to clean install from the new source. Otherwise do nothing.
--- - If doesn't exist, install it by downloading from `src` into `name` --- - If doesn't exist, install it by downloading from `src` into `name`
--- subdirectory (via partial blobless `git clone`) and update revision --- subdirectory (via partial blobless `git clone`) and update revision
--- to match `version` (via `git checkout`). --- to match `version` (via `git checkout`). Plugin will not be on disk if
--- any step resulted in an error.
--- - For each plugin execute |:packadd| (or customizable `load` function) making --- - For each plugin execute |:packadd| (or customizable `load` function) making
--- it reachable by Nvim. --- it reachable by Nvim.
--- ---

View File

@@ -575,10 +575,9 @@ describe('vim.pack', function()
local pluginerr_hash = git_get_hash('main', 'pluginerr') local pluginerr_hash = git_get_hash('main', 'pluginerr')
local ref_lockfile = { local ref_lockfile = {
-- Should be no entry for `repo_not_exist` -- Should be no entry for `repo_not_exist` and `basic` as they did not
-- fully install
plugins = { plugins = {
-- No `rev` because there was no relevant checkout
basic = { src = repos_src.basic, version = "'not-exist'" },
-- Error during sourcing 'plugin/' should not affect lockfile -- Error during sourcing 'plugin/' should not affect lockfile
pluginerr = { rev = pluginerr_hash, src = repos_src.pluginerr, version = "'main'" }, pluginerr = { rev = pluginerr_hash, src = repos_src.pluginerr, version = "'main'" },
}, },
@@ -605,14 +604,12 @@ describe('vim.pack', function()
eq(false, vim.tbl_contains(rtp, after_dir)) eq(false, vim.tbl_contains(rtp, after_dir))
end) end)
it('does not checkout on bad `version`', function() it('does not install on bad `version`', function()
local err = pcall_err(exec_lua, function() local err = pcall_err(exec_lua, function()
vim.pack.add({ { src = repos_src.basic, version = 'not-exist' } }) vim.pack.add({ { src = repos_src.basic, version = 'not-exist' } })
end) end)
matches('`not%-exist` is not a branch/tag/commit', err) matches('`not%-exist` is not a branch/tag/commit', err)
local plug_path = pack_get_plug_path('basic') eq(false, pack_exists('basic'))
local entries = vim.iter(vim.fs.dir(plug_path)):totable()
eq({ { '.git', 'directory' } }, entries)
end) end)
it('allows changing `src` of installed plugin', function() it('allows changing `src` of installed plugin', function()