commit b24522e77b086de051f1050f48ec260c0be33b8c
parent 9833f0da5f357c5dee8c695f4b88867be0e65832
Author: Justin M. Keyes <justinkz@gmail.com>
Date: Tue, 30 Dec 2025 10:40:19 -0500
Merge #37113 from echasnovski/pack-better-revert
Diffstat:
4 files changed, 140 insertions(+), 27 deletions(-)
diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt
@@ -288,12 +288,18 @@ Another approach is to utilize Git's `insteadOf` configuration:
These sources will be used verbatim in |vim.pack-lockfile|, so reusing the
config on different machine will require the same Git configuration.
+Explore installed plugins ~
+• `vim.pack.update(nil, { offline = true })`
+• Navigate between plugins with `[[` and `]]`. List them with `gO`
+ (|vim.lsp.buf.document_symbol()|).
+
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.
+• Execute `vim.pack.update({ 'plugin1' })`. The plugin's source is updated. If
+ only switching version, use `{ offline = true }` option table.
• 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()|.
@@ -309,16 +315,28 @@ Unfreeze plugin to start receiving updates ~
• |: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.
+• Revert the |vim.pack-lockfile| to the state before the update:
+ • If Git tracked: `git checkout HEAD -- nvim-pack-lock.json`
+ • If not tracked: examine log file ("nvim-pack.log" at "log" |stdpath()|),
+ locate the revisions before the latest update, and (carefully) adjust
+ current lockfile to have those revisions.
+• |:restart|.
+• `vim.pack.update({ 'plugin' }, { offline = true, target = 'lockfile' })`.
+ Read and confirm.
+
+Synchronize config across machines ~
+• On main machine:
+ • Add |vim.pack-lockfile| to VCS.
+ • Push to the remote server.
+• On secondary machine:
+ • Pull from the server.
+ • |:restart|. New plugins (not present locally, but present in the lockfile)
+ are installed at proper revision.
+ • `vim.pack.update(nil, { target = 'lockfile' })`. Read and confirm.
+ • Manually delete outdated plugins (present locally, but were not present in
+ the lockfile prior to restart) with `vim.pack.del( { 'plugin' })`. They
+ can be located by examining the VCS difference of the lockfile
+ (`git diff -- nvim-pack-lock.json` for Git).
Remove plugins from disk ~
• Remove plugin specs from |vim.pack.add()| calls in 'init.lua' or they will
@@ -485,6 +503,14 @@ update({names}, {opts}) *vim.pack.update()*
• {opts} (`table?`) A table with the following fields:
• {force}? (`boolean`) Whether to skip confirmation and make
updates immediately. Default `false`.
+ • {offline}? (`boolean`) Whether to skip downloading new
+ updates. Default: `false`.
+ • {target}? (`string`) How to compute a new plugin revision.
+ One of:
+ • "version" (default) - use latest revision matching
+ `version` from plugin specification.
+ • "lockfile" - use revision from the lockfile. Useful for
+ reverting or performing controlled update.
vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl:
diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua
@@ -88,6 +88,12 @@
--- These sources will be used verbatim in |vim.pack-lockfile|, so reusing
--- the config on different machine will require the same Git configuration.
---
+---Explore installed plugins ~
+---
+---- `vim.pack.update(nil, { offline = true })`
+---- Navigate between plugins with `[[` and `]]`. List them with `gO`
+--- (|vim.lsp.buf.document_symbol()|).
+---
---Switch plugin's version and/or source ~
---
---- Update 'init.lua' for plugin to have desired `version` and/or `src`.
@@ -95,6 +101,7 @@
---- |: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.
+--- If only switching version, use `{ offline = true }` option table.
---- 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()|.
@@ -113,16 +120,29 @@
---
---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.
+---- Revert the |vim.pack-lockfile| to the state before the update:
+--- - If Git tracked: `git checkout HEAD -- nvim-pack-lock.json`
+--- - If not tracked: examine log file ("nvim-pack.log" at "log" |stdpath()|),
+--- locate the revisions before the latest update, and (carefully) adjust
+--- current lockfile to have those revisions.
+---- |:restart|.
+---- `vim.pack.update({ 'plugin' }, { offline = true, target = 'lockfile' })`.
+--- Read and confirm.
+---
+---Synchronize config across machines ~
+---
+---- On main machine:
+--- - Add |vim.pack-lockfile| to VCS.
+--- - Push to the remote server.
+---- On secondary machine:
+--- - Pull from the server.
+--- - |:restart|. New plugins (not present locally, but present in the lockfile)
+--- are installed at proper revision.
+--- - `vim.pack.update(nil, { target = 'lockfile' })`. Read and confirm.
+--- - Manually delete outdated plugins (present locally, but were not present
+--- in the lockfile prior to restart) with `vim.pack.del( { 'plugin' })`.
+--- They can be located by examining the VCS difference of the lockfile
+--- (`git diff -- nvim-pack-lock.json` for Git).
---
---Remove plugins from disk ~
---
@@ -1142,6 +1162,14 @@ end
--- @class vim.pack.keyset.update
--- @inlinedoc
--- @field force? boolean Whether to skip confirmation and make updates immediately. Default `false`.
+---
+--- @field offline? boolean Whether to skip downloading new updates. Default: `false`.
+---
+--- How to compute a new plugin revision. One of:
+--- - "version" (default) - use latest revision matching `version` from plugin specification.
+--- - "lockfile" - use revision from the lockfile. Useful for reverting or performing controlled
+--- update.
+--- @field target? string
--- Update plugins
---
@@ -1177,7 +1205,7 @@ end
--- @param opts? vim.pack.keyset.update
function M.update(names, opts)
vim.validate('names', names, vim.islist, true, 'list')
- opts = vim.tbl_extend('force', { force = false }, opts or {})
+ opts = vim.tbl_extend('force', { force = false, offline = false, target = 'version' }, opts or {})
local plug_list = plug_list_from_names(names)
if #plug_list == 0 then
@@ -1194,21 +1222,26 @@ function M.update(names, opts)
--- @async
--- @param p vim.pack.Plug
local function do_update(p)
+ local l_data = plugin_lock.plugins[p.spec.name]
-- Ensure proper `origin` if needed
- if plugin_lock.plugins[p.spec.name].src ~= p.spec.src then
+ if l_data.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
+ if not opts.offline then
-- Using '--tags --force' means conflicting tags will be synced with remote
local args = { 'fetch', '--quiet', '--tags', '--force', '--recurse-submodules=yes', 'origin' }
git_cmd(args, p.path)
end
-- Compute change info: changelog if any, new tags if nothing to update
+ if opts.target == 'lockfile' then
+ p.info.version_str = '*lockfile*'
+ p.info.sha_target = l_data.rev
+ end
infer_update_details(p)
-- Checkout immediately if no need to confirm
@@ -1218,8 +1251,8 @@ function M.update(names, opts)
trigger_event(p, 'PackChanged', 'update')
end
end
- local progress_title = opts.force and (opts._offline and 'Applying updates' or 'Updating')
- or 'Downloading updates'
+ local progress_title = opts.force and (opts.offline and 'Applying updates' or 'Updating')
+ or (opts.offline and 'Computing updates' or 'Downloading updates')
run_list(plug_list, do_update, progress_title)
if needs_lock_write then
diff --git a/runtime/lua/vim/pack/_lsp.lua b/runtime/lua/vim/pack/_lsp.lua
@@ -159,7 +159,7 @@ end
local commands = {
update_plugin = function(plug_data)
- vim.pack.update({ plug_data.name }, { force = true, _offline = true })
+ vim.pack.update({ plug_data.name }, { force = true, offline = true })
end,
skip_update_plugin = function(_) end,
delete_plugin = function(plug_data)
diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua
@@ -1708,6 +1708,38 @@ describe('vim.pack', function()
eq(hashes.fetch_new, get_lock_tbl().plugins.fetch.rev)
end)
+ it('can use lockfile revision as a target', function()
+ exec_lua(function()
+ vim.pack.add({ repos_src.fetch })
+ end)
+ eq('return "fetch main"', fn.readblob(fetch_lua_file))
+
+ -- Mock "update -> revert lockfile -> revert plugin"
+ local lock_path = get_lock_path()
+ local lockfile_before = fn.readblob(lock_path)
+ hashes.fetch_new = git_get_hash('main', 'fetch')
+
+ -- - Update
+ exec_lua('vim.pack.update({ "fetch" }, { force = true })')
+ eq('return "fetch new 2"', fn.readblob(fetch_lua_file))
+
+ -- - Revert lockfile
+ fn.writefile(vim.split(lockfile_before, '\n'), lock_path)
+ n.clear()
+
+ -- - Revert plugin
+ eq('return "fetch new 2"', fn.readblob(fetch_lua_file))
+ exec_lua('vim.pack.update({ "fetch" }, { target = "lockfile" })')
+ local confirm_lines = api.nvim_buf_get_lines(0, 0, -1, false)
+ n.exec('write')
+ eq('return "fetch main"', fn.readblob(fetch_lua_file))
+ eq(hashes.fetch_head, get_lock_tbl().plugins.fetch.rev)
+
+ -- - Should mention that new revision comes from *lockfile*
+ eq(confirm_lines[6], ('Revision before: %s'):format(hashes.fetch_new))
+ eq(confirm_lines[7], ('Revision after: %s (*lockfile*)'):format(hashes.fetch_head))
+ end)
+
it('can change `src` of installed plugin', function()
local basic_src = repos_src.basic
local defbranch_src = repos_src.defbranch
@@ -1754,6 +1786,28 @@ describe('vim.pack', function()
assert_origin(basic_src)
end)
+ it('can do offline update', function()
+ local defbranch_path = pack_get_plug_path('defbranch')
+ local defbranch_lua_file = vim.fs.joinpath(defbranch_path, 'lua', 'defbranch.lua')
+
+ n.exec_lua(function()
+ vim.pack.add({ { src = repos_src.defbranch, version = 'main' } })
+ end)
+
+ track_nvim_echo()
+
+ eq('return "defbranch dev"', fn.readblob(defbranch_lua_file))
+ n.exec_lua(function()
+ vim.pack.update({ 'defbranch' }, { offline = true })
+ end)
+
+ -- There should be no progress report about downloading updates
+ assert_progress_report('Computing updates', { 'defbranch' })
+
+ n.exec('write')
+ eq('return "defbranch main"', fn.readblob(defbranch_lua_file))
+ end)
+
it('shows progress report', function()
track_nvim_echo()
exec_lua(function()