commit c03d635a1243287dd0a0b7d1f471ea12a5786a87
parent ef0386fe9ae2ccd7df20c73ce37a9c2a4ca7d98d
Author: Justin M. Keyes <justinkz@gmail.com>
Date: Thu, 25 Dec 2025 23:44:08 -0500
Merge #37097 feat(pack)!: improve handling of `src` change
Diffstat:
3 files changed, 130 insertions(+), 87 deletions(-)
diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt
@@ -230,9 +230,9 @@ plugins from the lockfile will be installed at once and at lockfile's revision
data for installed plugins is repaired (including after deleting whole file),
but `version` fields will be missing for not yet added plugins.
-Example workflows ~
+ *vim.pack-examples*
-Basic install and management:
+Basic install and management ~
• Add |vim.pack.add()| call(s) to 'init.lua': >lua
vim.pack.add({
@@ -272,31 +272,43 @@ 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.
-
-Freeze plugin from being updated:
+Use shorter source ~
+
+Create custom Lua helpers: >lua
+
+ local gh = function(x) return 'https://github.com/' .. x end
+ local cb = function(x) return 'https://codeberg.org/' .. x end
+ vim.pack.add({ gh('user/plugin1'), cb('user/plugin2') })
+<
+
+Another approach is to utilize Git's `insteadOf` configuration:
+• `git config --global url."https://github.com/".insteadOf "gh:"`
+• `git config --global url."https://codeberg.org/".insteadOf "cb:"`
+• In 'init.lua': `vim.pack.add({ 'gh:user/plugin1', 'cb:user/plugin2' })`.
+ These sources will be used verbatim in |vim.pack-lockfile|, so reusing the
+ config on different machine will require the same Git configuration.
+
+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
it from |vim.pack-lockfile| (plugin's field `rev`; looks like `abc12345`).
• |:restart|.
-Unfreeze plugin to start receiving updates:
+Unfreeze plugin to start receiving updates ~
• Update 'init.lua' for plugin to have `version` set to whichever version you
want it to be updated.
• |:restart|.
-Revert plugin after an update:
+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.
@@ -308,12 +320,12 @@ Revert plugin after an update:
state on disk follow target revision. |:restart|.
• When ready to deal with updating plugin, unfreeze it.
-Remove plugins from disk:
+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.
-Available events to hook into ~
+ *vim.pack-events*
• *PackChangedPre* - before trying to change plugin's state.
• *PackChanged* - after plugin's state has changed.
diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua
@@ -24,9 +24,9 @@
---(including after deleting whole file), but `version` fields will be missing
---for not yet added plugins.
---
----Example workflows ~
+---[vim.pack-examples]()
---
----Basic install and management:
+---Basic install and management ~
---
---- Add |vim.pack.add()| call(s) to 'init.lua':
---```lua
@@ -70,31 +70,49 @@
--- 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' })`.
+---Use shorter source ~
+---
+--- Create custom Lua helpers:
+---
+---```lua
+---
+---local gh = function(x) return 'https://github.com/' .. x end
+---local cb = function(x) return 'https://codeberg.org/' .. x end
+---vim.pack.add({ gh('user/plugin1'), cb('user/plugin2') })
+---```
+---
+---Another approach is to utilize Git's `insteadOf` configuration:
+---- `git config --global url."https://github.com/".insteadOf "gh:"`
+---- `git config --global url."https://codeberg.org/".insteadOf "cb:"`
+---- In 'init.lua': `vim.pack.add({ 'gh:user/plugin1', 'cb:user/plugin2' })`.
+--- These sources will be used verbatim in |vim.pack-lockfile|, so reusing
+--- the config on different machine will require the same Git configuration.
+---
+---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()|.
+--- `version` change 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 ~
---
----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`).
---- |:restart|.
---
----Unfreeze plugin to start receiving updates:
+---Unfreeze plugin to start receiving updates ~
+---
---- Update 'init.lua' for plugin to have `version` set to whichever version
---you want it to be updated.
---- |:restart|.
---
----Revert plugin after an update:
+---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.
@@ -106,11 +124,12 @@
--- state on disk follow target revision. |:restart|.
---- When ready to deal with updating plugin, unfreeze it.
---
----Remove plugins from disk:
+---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.
---
----Available events to hook into ~
+---[vim.pack-events]()
---
---- [PackChangedPre]() - before trying to change plugin's state.
---- [PackChanged]() - after plugin's state has changed.
@@ -922,12 +941,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 +1185,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 +1218,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()