commit 0353d33b00977fff4f8804d4d94c3a32ebba7807
parent 5a1a92cc7a3d3a338fe5f610190910e220f074f6
Author: Evgeni Chasnovski <evgeni.chasnovski@gmail.com>
Date: Thu, 25 Dec 2025 12:53:49 +0200
feat(pack)!: change `src` of installed plugin inside `update()` in place
Problem: Changing `src` of already installed plugin currently takes
effect immediately inside `vim.pack.add()` and acts as "delete and
later fresh install". Although more robust, this might lead to
unintentional data loss (since plugin is deleted) if the plugin was
manually modified or the new source is not valid.
Also this introduces unnecessary differentiation between "change
`version`" and "change `src`" of already installed plugin.
Solution: Require an explicit `vim.pack.update()` to change plugin's
source. It is done by conditionally changing `origin` remote of the
Git repo. The effect does not require update confirmation in order to
have new changes fetched from the new `src` right away.
If in the future there are more types of plugins supported (i.e. not
only Git repos), also do extra work (like delete + install) during
`vim.pack.update()`.
Diffstat:
3 files changed, 77 insertions(+), 73 deletions(-)
diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt
@@ -272,19 +272,15 @@ Basic install and management:
updates execute |:quit|.
• (Optionally) |:restart| to start using code from updated plugins.
-Switch plugin's version:
-• Update 'init.lua' for plugin to have desired `version`. Let's say, plugin
- named 'plugin1' has changed to `vim.version.range('*')`.
-• |:restart|. The plugin's actual revision on disk is not yet changed. Only
- plugin's `version` in |vim.pack-lockfile| is updated.
-• Execute `vim.pack.update({ 'plugin1' })`.
-• Review changes and either confirm or discard them. If discarded, revert 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.
+Switch plugin's version and/or source:
+• Update 'init.lua' for plugin to have desired `version` and/or `src`. Let's
+ say, the switch is for plugin named 'plugin1'.
+• |:restart|. The plugin's state on disk (revision and/or tracked source) is
+ not yet changed. Only plugin's `version` in |vim.pack-lockfile| is updated.
+• Execute `vim.pack.update({ 'plugin1' })`. The plugin's source is updated.
+• Review changes and either confirm or discard them. If discarded, revert
+ `version` change in 'init.lua' as well or you will be prompted again next
+ time you run |vim.pack.update()|.
Freeze plugin from being updated:
• Update 'init.lua' for plugin to have `version` set to current revision. Get
diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua
@@ -70,19 +70,15 @@
--- To discard updates execute |:quit|.
--- - (Optionally) |:restart| to start using code from updated plugins.
---
----Switch plugin's version:
----- Update 'init.lua' for plugin to have desired `version`. Let's say, plugin
----named 'plugin1' has changed to `vim.version.range('*')`.
----- |:restart|. The plugin's actual revision on disk is not yet changed.
---- Only plugin's `version` in |vim.pack-lockfile| is updated.
----- Execute `vim.pack.update({ 'plugin1' })`.
+---Switch plugin's version and/or source:
+---- Update 'init.lua' for plugin to have desired `version` and/or `src`.
+--- Let's say, the switch is for plugin named 'plugin1'.
+---- |:restart|. The plugin's state on disk (revision and/or tracked source)
+--- is not yet changed. Only plugin's `version` in |vim.pack-lockfile| is updated.
+---- Execute `vim.pack.update({ 'plugin1' })`. The plugin's source is updated.
---- Review changes and either confirm or discard them. If discarded, revert
----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.
+--- `version` change in 'init.lua' as well or you will be prompted again next time
+--- you run |vim.pack.update()|.
---
---Freeze plugin from being updated:
---- Update 'init.lua' for plugin to have `version` set to current revision.
@@ -922,12 +918,6 @@ function M.add(specs, opts)
local plugs_to_install = {} --- @type vim.pack.Plug[]
local needs_lock_write = false
for _, p in ipairs(plugs) do
- -- 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
@@ -1172,10 +1162,18 @@ function M.update(names, opts)
-- Perform update
local timestamp = get_timestamp()
+ local needs_lock_write = opts.force --- @type boolean
--- @async
--- @param p vim.pack.Plug
local function do_update(p)
+ -- Ensure proper `origin` if needed
+ if plugin_lock.plugins[p.spec.name].src ~= p.spec.src then
+ git_cmd({ 'remote', 'set-url', 'origin', p.spec.src }, p.path)
+ plugin_lock.plugins[p.spec.name].src = p.spec.src
+ needs_lock_write = true
+ end
+
-- Fetch
if not opts._offline then
-- Using '--tags --force' means conflicting tags will be synced with remote
@@ -1197,8 +1195,11 @@ function M.update(names, opts)
or 'Downloading updates'
run_list(plug_list, do_update, progress_title)
- if opts.force then
+ if needs_lock_write then
lock_write()
+ end
+
+ if opts.force then
feedback_log(plug_list)
return
end
diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua
@@ -790,46 +790,6 @@ describe('vim.pack', function()
eq(false, pack_exists('basic'))
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()
@@ -1142,7 +1102,7 @@ describe('vim.pack', function()
describe('update()', function()
-- Lua source code for the tested plugin named "fetch"
- local fetch_lua_file = vim.fs.joinpath(pack_get_plug_path('fetch'), 'lua', 'fetch.lua')
+ local fetch_lua_file
-- Tables with hashes used to test confirmation buffer and log content
local hashes --- @type table<string,string>
local short_hashes --- @type table<string,string>
@@ -1157,6 +1117,7 @@ describe('vim.pack', function()
repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch main"')
git_add_commit('Commit from `main` to be removed', 'fetch')
+ fetch_lua_file = vim.fs.joinpath(pack_get_plug_path('fetch'), 'lua', 'fetch.lua')
hashes = { fetch_head = git_get_hash('HEAD', 'fetch') }
short_hashes = { fetch_head = git_get_short_hash('HEAD', 'fetch') }
@@ -1726,6 +1687,52 @@ describe('vim.pack', function()
eq(hashes.fetch_new, get_lock_tbl().plugins.fetch.rev)
end)
+ it('can change `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)
+
+ local function assert_origin(ref)
+ -- Should be in sync both on disk and in lockfile
+ local opts = { cwd = pack_get_plug_path('basic') }
+ local real_origin = system_sync({ 'git', 'remote', 'get-url', 'origin' }, opts)
+ eq(ref, vim.trim(real_origin.stdout))
+
+ eq(ref, get_lock_tbl().plugins.basic.src)
+ end
+
+ n.clear()
+ watch_events({ 'PackChangedPre', 'PackChanged' })
+
+ assert_origin(basic_src)
+ exec_lua(function()
+ vim.pack.add({ { src = defbranch_src, name = 'basic' } })
+ end)
+ -- Should not yet (after `add()`) affect plugin source
+ assert_origin(basic_src)
+
+ -- Should update source immediately (to work if updates are discarded)
+ exec_lua(function()
+ vim.pack.update({ 'basic' })
+ end)
+ assert_origin(defbranch_src)
+
+ -- Should not revert source change even if update is discarded
+ n.exec('quit')
+ assert_origin(defbranch_src)
+ eq({}, exec_lua('return _G.event_log'))
+
+ -- Should work with forced update
+ n.clear()
+ exec_lua(function()
+ vim.pack.add({ basic_src })
+ vim.pack.update({ 'basic' }, { force = true })
+ end)
+ assert_origin(basic_src)
+ end)
+
it('shows progress report', function()
track_nvim_echo()
exec_lua(function()