commit f0294418d65d005d885eabf80eecc7255358b95e
parent d464dffd2fe5ceee77944fa9946547548b30d9e1
Author: Justin M. Keyes <justinkz@gmail.com>
Date: Sun, 16 Nov 2025 13:21:47 -0800
Merge #36435 vim.pack: improve default `opts.load`, handle `src` change
Diffstat:
3 files changed, 127 insertions(+), 30 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`).
@@ -290,6 +294,18 @@ Unfreeze plugin to start receiving updates:
want it to be updated.
• |:restart|.
+Revert plugin after an update:
+• Locate plugin's revision at working state. For example:
+ • If there is a previous version of |vim.pack-lockfile| (like from version
+ control history), use it to get plugin's `rev` field.
+ • If there is a log file ("nvim-pack.log" at "log" |stdpath()|), open it and
+ navigate to latest updates (at the bottom). Locate lines about plugin
+ update details and use revision from "State before".
+• Freeze plugin to target revision (set `version` and |:restart|).
+• Run `vim.pack.update({ 'plugin-name' }, { force = true })` to make plugin
+ state on disk follow target revision. |:restart|.
+• When ready to deal with updating plugin, unfreeze it.
+
Remove plugins from disk:
• Use |vim.pack.del()| with a list of plugin names to remove. Make sure their
specs are not included in |vim.pack.add()| call in 'init.lua' or they will
@@ -354,10 +370,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.
@@ -379,7 +397,7 @@ add({specs}, {opts}) *vim.pack.add()*
Load `plugin/` files and `ftdetect/` scripts. If `false`,
works like `:packadd!`. If function, called with plugin
data and is fully responsible for loading plugin. Default
- `false` during startup and `true` afterwards.
+ `false` during |init.lua| sourcing and `true` afterwards.
• {confirm}? (`boolean`) Whether to ask user to confirm
initial install. Default `true`.
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`).
@@ -87,6 +91,18 @@
---you want it to be updated.
---- |:restart|.
---
+---Revert plugin after an update:
+---- Locate plugin's revision at working state. For example:
+--- - If there is a previous version of |vim.pack-lockfile| (like from version
+--- control history), use it to get plugin's `rev` field.
+--- - If there is a log file ("nvim-pack.log" at "log" |stdpath()|), open it
+--- and navigate to latest updates (at the bottom). Locate lines about plugin
+--- update details and use revision from "State before".
+---- Freeze plugin to target revision (set `version` and |:restart|).
+---- Run `vim.pack.update({ 'plugin-name' }, { force = true })` to make plugin
+--- state on disk follow target revision. |:restart|.
+---- When ready to deal with updating plugin, unfreeze it.
+---
---Remove plugins from disk:
---- Use |vim.pack.del()| with a list of plugin names to remove. Make sure their specs
---are not included in |vim.pack.add()| call in 'init.lua' or they will be reinstalled.
@@ -165,11 +181,11 @@ end
local git_version = vim.version.parse('1')
local function git_ensure_exec()
- local sys_res = vim.system({ 'git', 'version' }):wait()
- git_version = vim.version.parse(sys_res.stdout)
- if sys_res.stderr ~= '' then
+ local ok, sys = pcall(vim.system, { 'git', 'version' })
+ if not ok then
error('No `git` executable')
end
+ git_version = vim.version.parse(sys:wait().stdout)
end
--- @async
@@ -743,8 +759,9 @@ local function pack_add(plug, load)
-- NOTE: The `:packadd` specifically seems to not handle spaces in dir name
vim.cmd.packadd({ vim.fn.escape(plug.spec.name, ' '), bang = not load, magic = { file = false } })
- -- Execute 'after/' scripts if not during startup (when they will be sourced
- -- automatically), as `:packadd` only sources plain 'plugin/' files.
+ -- The `:packadd` only sources plain 'plugin/' files. Execute 'after/' scripts
+ -- if not during startup (when they will be sourced later, even if
+ -- `vim.pack.add` is inside user's 'plugin/')
-- See https://github.com/vim/vim/issues/15584
-- Deliberately do so after executing all currently known 'plugin/' files.
if vim.v.vim_did_enter == 1 and load then
@@ -760,7 +777,7 @@ end
--- @inlinedoc
--- Load `plugin/` files and `ftdetect/` scripts. If `false`, works like `:packadd!`.
--- If function, called with plugin data and is fully responsible for loading plugin.
---- Default `false` during startup and `true` afterwards.
+--- Default `false` during |init.lua| sourcing and `true` afterwards.
--- @field load? boolean|fun(plug_data: {spec: vim.pack.Spec, path: string})
---
--- @field confirm? boolean Whether to ask user to confirm initial install. Default `true`.
@@ -768,9 +785,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.
---
@@ -788,7 +807,7 @@ end
--- @param opts? vim.pack.keyset.add
function M.add(specs, opts)
vim.validate('specs', specs, vim.islist, false, 'list')
- opts = vim.tbl_extend('force', { load = vim.v.vim_did_enter == 1, confirm = true }, opts or {})
+ opts = vim.tbl_extend('force', { load = vim.v.vim_did_init == 1, confirm = true }, opts or {})
vim.validate('opts', opts, 'table')
local plug_dir = get_plug_dir()
@@ -803,13 +822,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()
@@ -620,35 +660,42 @@ describe('vim.pack', function()
end)
describe('startup', function()
- local init_lua = ''
+ local config_dir, pack_add_cmd = '', ''
+
before_each(function()
- init_lua = vim.fs.joinpath(fn.stdpath('config'), 'init.lua')
- fn.mkdir(vim.fs.dirname(init_lua), 'p')
+ config_dir = fn.stdpath('config')
+ fn.mkdir(vim.fs.joinpath(config_dir, 'plugin'), 'p')
+
+ pack_add_cmd = ('vim.pack.add({ %s })'):format(vim.inspect(repos_src.plugindirs))
end)
+
after_each(function()
- pcall(vim.fs.rm, init_lua, { force = true })
+ vim.fs.rm(config_dir, { recursive = true, force = true })
end)
- it('works in init.lua', function()
- local pack_add_cmd = ('vim.pack.add({ %s })'):format(vim.inspect(repos_src.plugindirs))
- fn.writefile({ pack_add_cmd, '_G.done = true' }, init_lua)
-
- local function assert_loaded()
- eq('plugindirs main', exec_lua('return require("plugindirs")'))
+ local function assert_loaded()
+ eq('plugindirs main', exec_lua('return require("plugindirs")'))
- -- Should source 'plugin/' and 'after/plugin/' exactly once
- eq({ true, true }, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }'))
- eq({ 'p', 'a' }, n.exec_lua('return _G.DL'))
- end
+ -- Should source 'plugin/' and 'after/plugin/' exactly once
+ eq({ true, true }, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }'))
+ eq({ 'p', 'a' }, n.exec_lua('return _G.DL'))
+ end
+ local function assert_works()
-- Should auto-install but wait before executing code after it
n.clear({ args_rm = { '-u' } })
n.exec_lua('vim.wait(500, function() return _G.done end, 50)')
assert_loaded()
- -- Should only `:packadd!` already installed plugin
+ -- Should only `:packadd!`/`:packadd` already installed plugin
n.clear({ args_rm = { '-u' } })
assert_loaded()
+ end
+
+ it('works in init.lua', function()
+ local init_lua = vim.fs.joinpath(config_dir, 'init.lua')
+ fn.writefile({ pack_add_cmd, '_G.done = true' }, init_lua)
+ assert_works()
-- Should not load plugins if `--noplugin`, only adjust 'runtimepath'
n.clear({ args = { '--noplugin' }, args_rm = { '-u' } })
@@ -656,6 +703,13 @@ describe('vim.pack', function()
eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }'))
eq(vim.NIL, n.exec_lua('return _G.DL'))
end)
+
+ it('works in plugin/', function()
+ local plugin_file = vim.fs.joinpath(config_dir, 'plugin', 'mine.lua')
+ fn.writefile({ pack_add_cmd, '_G.done = true' }, plugin_file)
+ -- Should source plugin's 'plugin/' files without explicit `load=true`
+ assert_works()
+ end)
end)
it('shows progress report during installation', function()