feat(pack): allow choosing update target in update()

Problem: There are two fairly common workflows that involve lockfile and
  can be made more straightforward in `vim.pack`:

    1. Revert latest update. Like if it introduced unwanted behavior.

    2. Update to a revision in the lockfile. Like if already updated
       on another machine, verified that everything works, `git add` +
       `git commit` + `git push` the config, and want to have the same
       plugin states on current machine.

Solution: Make `update` allow `opts.target`. By default it uses
  `version` from a plugin specification (like a regular "get new changes
  from source" workflow). But it also allows `"lockfile"` value to
  indicate that target revision after update should be taken from the
  current lockfile verbatim.

  With this, the workflows are:

    1. Revert (somehow) to the lockfile before the update, restart, and
       `vim.pack.update({ 'plugin' }, { target = 'lockfile' })`. If Git
       tracked, revert with `git checkout HEAD -- nvim-pack-lock.json`.
       For non-VCS tracked lockfile, the revisions can be taken from the
       log file. It would be nicer if `update()` would backup a lockfile
       before doing an update, but that might require discussions.

    2. `git pull` + `:restart` +
       `vim.pack.update(nil, { target = 'lockfile' })`.
       The only caveats are for new and deleted plugins:
        - New plugins (not present locally but present in the lockfile)
          will be installed at lockfile revision during restart.
        - Deleted plugins (present locally but not present in the
          lockfile) will still be present: both locally *and* in the
          lockfile. They can be located by
          `git diff -- nvim-pack-lock.json` and require manual
          `vim.pack.del({ 'old-plugin1', 'old-plugin2' })`.
This commit is contained in:
Evgeni Chasnovski committed 2025-12-26 18:32:59 +02:00
1 parent 899ec829be
commit 8c28507fcf
3 files changed
+94 -22

No files matched your search

+27 -10
View File
@@ -309,16 +309,27 @@ Unfreeze plugin to start receiving updates ~
• |:restart|. • |:restart|.
Revert plugin after an update ~ Revert plugin after an update ~
• Locate plugin's revision at working state. For example: • Revert the |vim.pack-lockfile| to the state before the update:
• If there is a previous version of |vim.pack-lockfile| (like from version • If Git tracked: `git checkout HEAD -- nvim-pack-lock.json`
control history), use it to get plugin's `rev` field. • If not tracked: examine log file ("nvim-pack.log" at "log" |stdpath()|),
• If there is a log file ("nvim-pack.log" at "log" |stdpath()|), open it and locate the revisions before the latest update, and (carefully) adjust
navigate to latest updates (at the bottom). Locate lines about plugin current lockfile to have those revisions.
update details and use revision from "State before". • |:restart|.
• Freeze plugin to target revision (set `version` and |:restart|). • `vim.pack.update({ 'plugin' }, { target = 'lockfile' })`. Read and confirm.
• Run `vim.pack.update({ 'plugin-name' }, { force = true })` to make plugin
state on disk follow target revision. |:restart|. Synchronize config across machines ~
• When ready to deal with updating plugin, unfreeze it. • 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 plugins from disk ~
• Use |vim.pack.del()| with a list of plugin names to remove. Make sure their • Use |vim.pack.del()| with a list of plugin names to remove. Make sure their
@@ -480,6 +491,12 @@ update({names}, {opts}) *vim.pack.update()*
• {opts} (`table?`) A table with the following fields: • {opts} (`table?`) A table with the following fields:
• {force}? (`boolean`) Whether to skip confirmation and make • {force}? (`boolean`) Whether to skip confirmation and make
updates immediately. Default `false`. updates immediately. 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: vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl:
+35 -12
View File
@@ -113,16 +113,28 @@
--- ---
---Revert plugin after an update ~ ---Revert plugin after an update ~
--- ---
---- Locate plugin's revision at working state. For example: ---- Revert the |vim.pack-lockfile| to the state before the update:
--- - If there is a previous version of |vim.pack-lockfile| (like from version --- - If Git tracked: `git checkout HEAD -- nvim-pack-lock.json`
--- control history), use it to get plugin's `rev` field. --- - If not tracked: examine log file ("nvim-pack.log" at "log" |stdpath()|),
--- - If there is a log file ("nvim-pack.log" at "log" |stdpath()|), open it --- locate the revisions before the latest update, and (carefully) adjust
--- and navigate to latest updates (at the bottom). Locate lines about plugin --- current lockfile to have those revisions.
--- update details and use revision from "State before". ---- |:restart|.
---- Freeze plugin to target revision (set `version` and |:restart|). ---- `vim.pack.update({ 'plugin' }, { target = 'lockfile' })`. Read and confirm.
---- Run `vim.pack.update({ 'plugin-name' }, { force = true })` to make plugin ---
--- state on disk follow target revision. |:restart|. ---Synchronize config across machines ~
---- When ready to deal with updating plugin, unfreeze it. ---
---- 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 plugins from disk ~
--- ---
@@ -1139,6 +1151,12 @@ end
--- @class vim.pack.keyset.update --- @class vim.pack.keyset.update
--- @inlinedoc --- @inlinedoc
--- @field force? boolean Whether to skip confirmation and make updates immediately. Default `false`. --- @field force? boolean Whether to skip confirmation and make updates immediately. 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 --- Update plugins
--- ---
@@ -1173,7 +1191,7 @@ end
--- @param opts? vim.pack.keyset.update --- @param opts? vim.pack.keyset.update
function M.update(names, opts) function M.update(names, opts)
vim.validate('names', names, vim.islist, true, 'list') vim.validate('names', names, vim.islist, true, 'list')
opts = vim.tbl_extend('force', { force = false }, opts or {}) opts = vim.tbl_extend('force', { force = false, target = 'version' }, opts or {})
local plug_list = plug_list_from_names(names) local plug_list = plug_list_from_names(names)
if #plug_list == 0 then if #plug_list == 0 then
@@ -1190,8 +1208,9 @@ function M.update(names, opts)
--- @async --- @async
--- @param p vim.pack.Plug --- @param p vim.pack.Plug
local function do_update(p) local function do_update(p)
local l_data = plugin_lock.plugins[p.spec.name]
-- Ensure proper `origin` if needed -- 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) git_cmd({ 'remote', 'set-url', 'origin', p.spec.src }, p.path)
plugin_lock.plugins[p.spec.name].src = p.spec.src plugin_lock.plugins[p.spec.name].src = p.spec.src
needs_lock_write = true needs_lock_write = true
@@ -1205,6 +1224,10 @@ function M.update(names, opts)
end end
-- Compute change info: changelog if any, new tags if nothing to update -- 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) infer_update_details(p)
-- Checkout immediately if no need to confirm -- Checkout immediately if no need to confirm
+32
View File
@@ -1687,6 +1687,38 @@ describe('vim.pack', function()
eq(hashes.fetch_new, get_lock_tbl().plugins.fetch.rev) eq(hashes.fetch_new, get_lock_tbl().plugins.fetch.rev)
end) 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() it('can change `src` of installed plugin', function()
local basic_src = repos_src.basic local basic_src = repos_src.basic
local defbranch_src = repos_src.defbranch local defbranch_src = repos_src.defbranch