commit 2e533e364e2d52bcfe531d8ddd4a148d3f3bfed1
parent a39171f5327e44162aebd17f16d4cb7ffddef1df
Author: Evgeni Chasnovski <evgeni.chasnovski@gmail.com>
Date: Thu, 13 Nov 2025 15:49:16 +0200
feat(pack): update `add()` to handle source change for installed plugin
Problem: Changing `src` of an existing plugin cleanly requires manual
`vim.pack.del()` prior to executing `vim.pack.add()` with a new `src`.
Solution: Autodetect `src` change for an existing plugin (by comparing
against lockfile data). If different - properly delete immediately and
treat this as new plugin installation.
Alternative solution might be to update `origin` remote in the
installed plugin after calling `vim.pack.update()`. Although, doable,
this 1) requires more code; and 2) works only for Git plugins (which
might be not the only type of plugins in the future). Automatic
"delete and clean install" feels more robust.
Diffstat:
3 files changed, 65 insertions(+), 7 deletions(-)
diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt
@@ -280,6 +280,10 @@ Switch plugin's version:
changes in 'init.lua' as well or you will be prompted again next time you
run |vim.pack.update()|.
+Switch plugin's source:
+• Update 'init.lua' for plugin to have desired `src`.
+• |:restart|. This will cleanly reinstall plugin from the new source.
+
Freeze plugin from being updated:
• Update 'init.lua' for plugin to have `version` set to current revision. Get
it from |vim.pack-lockfile| (plugin's field `rev`; looks like `abc12345`).
@@ -354,10 +358,12 @@ add({specs}, {opts}) *vim.pack.add()*
Add plugin to current session
• For each specification check that plugin exists on disk in
|vim.pack-directory|:
- • If exists, do nothing in this step.
+ • If exists, check if its `src` is the same as input. If not - delete
+ immediately to clean install from the new source. Otherwise do
+ nothing.
• If doesn't exist, install it by downloading from `src` into `name`
- subdirectory (via `git clone`) and update revision to follow `version`
- (via `git checkout`).
+ subdirectory (via partial blobless `git clone`) and update revision to
+ match `version` (via `git checkout`).
• For each plugin execute |:packadd| (or customizable `load` function)
making it reachable by Nvim.
diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua
@@ -77,6 +77,10 @@
---any changes in 'init.lua' as well or you will be prompted again next time
---you run |vim.pack.update()|.
---
+---Switch plugin's source:
+---- Update 'init.lua' for plugin to have desired `src`.
+---- |:restart|. This will cleanly reinstall plugin from the new source.
+---
---Freeze plugin from being updated:
---- Update 'init.lua' for plugin to have `version` set to current revision.
---Get it from |vim.pack-lockfile| (plugin's field `rev`; looks like `abc12345`).
@@ -769,9 +773,11 @@ end
--- Add plugin to current session
---
--- - For each specification check that plugin exists on disk in |vim.pack-directory|:
---- - If exists, do nothing in this step.
+--- - If exists, check if its `src` is the same as input. If not - delete
+--- immediately to clean install from the new source. Otherwise do nothing.
--- - If doesn't exist, install it by downloading from `src` into `name`
---- subdirectory (via `git clone`) and update revision to follow `version` (via `git checkout`).
+--- subdirectory (via partial blobless `git clone`) and update revision
+--- to match `version` (via `git checkout`).
--- - For each plugin execute |:packadd| (or customizable `load` function) making
--- it reachable by Nvim.
---
@@ -804,13 +810,19 @@ function M.add(specs, opts)
local plugs_to_install = {} --- @type vim.pack.Plug[]
local needs_lock_write = false
for _, p in ipairs(plugs) do
- -- TODO(echasnovski): check that lock's `src` is the same as in spec.
- -- If not - cleanly reclone (delete directory and mark as not installed).
+ -- Allow to cleanly change `src` of an already installed plugin
+ if p.info.installed and plugin_lock.plugins[p.spec.name].src ~= p.spec.src then
+ M.del({ p.spec.name })
+ p.info.installed = false
+ end
+
+ -- Detect `version` change
local p_lock = plugin_lock.plugins[p.spec.name] or {}
needs_lock_write = needs_lock_write or p_lock.version ~= p.spec.version
p_lock.version = p.spec.version
plugin_lock.plugins[p.spec.name] = p_lock
+ -- Register for install
if not p.info.installed then
plugs_to_install[#plugs_to_install + 1] = p
needs_lock_write = true
diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua
@@ -611,6 +611,46 @@ describe('vim.pack', function()
eq({ { '.git', 'directory' } }, entries)
end)
+ it('allows changing `src` of installed plugin', function()
+ local basic_src = repos_src.basic
+ local defbranch_src = repos_src.defbranch
+ exec_lua(function()
+ vim.pack.add({ basic_src })
+ end)
+ eq('basic main', exec_lua('return require("basic")'))
+
+ n.clear()
+ watch_events({ 'PackChangedPre', 'PackChanged' })
+ exec_lua(function()
+ vim.pack.add({ { src = defbranch_src, name = 'basic' } })
+ end)
+ eq('defbranch dev', exec_lua('return require("defbranch")'))
+
+ -- Should first properly delete and then cleanly install
+ local log_simple = vim.tbl_map(function(x) --- @param x table
+ return { x.event, x.data.kind, x.data.spec }
+ end, exec_lua('return _G.event_log'))
+
+ local ref_log_simple = {
+ { 'PackChangedPre', 'delete', { name = 'basic', src = basic_src } },
+ { 'PackChanged', 'delete', { name = 'basic', src = basic_src } },
+ { 'PackChangedPre', 'install', { name = 'basic', src = defbranch_src } },
+ { 'PackChanged', 'install', { name = 'basic', src = defbranch_src } },
+ }
+ eq(ref_log_simple, log_simple)
+
+ local ref_messages = table.concat({
+ "vim.pack: Removed plugin 'basic'",
+ 'vim.pack: Installing plugins (0/1)',
+ 'vim.pack: 100% Installing plugins (1/1)',
+ }, '\n')
+ eq(ref_messages, n.exec_capture('messages'))
+
+ local defbranch_rev = git_get_hash('dev', 'defbranch')
+ local ref_lock_tbl = { plugins = { basic = { rev = defbranch_rev, src = defbranch_src } } }
+ eq(ref_lock_tbl, get_lock_tbl())
+ end)
+
it('can install from the Internet', function()
t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test')
exec_lua(function()