neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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:
Mruntime/doc/pack.txt | 54+++++++++++++++++++++++++++++++++---------------------
Mruntime/lua/vim/pack.lua | 74+++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mtest/functional/plugin/pack_spec.lua | 89+++++++++++++++++++++++++++++++++++++++++++------------------------------------
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()