From 8d100483e0662916cd30e99ceb7216532c58f903 Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Sun, 17 May 2026 10:49:48 +0300 Subject: [PATCH 1/3] feat(pack): update `get()` to take `rev` from actual repo if `info=true` Problem: `vim.pack.get()` always uses lockfile as the source for the `rev` field. This is fast, but may be misleading in case of a corrupted lockfile. Solution: Compute revision from Git repo on disk if `info=true` (default), use lockfile otherwise. This does increase execution time (as a result of one extra `git ...` call for every plugin), but `info=true` is already designed to be informative and not necessarily fast. --- runtime/doc/pack.txt | 3 ++- runtime/lua/vim/pack.lua | 3 ++- test/functional/plugin/pack_spec.lua | 29 ++++++++++++++++++++-------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt index b5cb447d6f..2a495ae3df 100644 --- a/runtime/doc/pack.txt +++ b/runtime/doc/pack.txt @@ -496,7 +496,8 @@ get({names}, {opts}) *vim.pack.get()* • {branches}? (`string[]`) Available Git branches (first is default). Missing if `info=false`. • {path} (`string`) Plugin's path on disk. - • {rev} (`string`) Current Git revision. + • {rev} (`string`) Current Git revision. Taken from + |vim.pack-lockfile| if `info=false`. • {spec} (`vim.pack.SpecResolved`) A |vim.pack.Spec| with resolved `name`. • {tags}? (`string[]`) Available Git tags. Missing if `info=false`. diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index bd693904cd..655d6f14d6 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -1434,7 +1434,7 @@ end --- @field active boolean Whether plugin was added via |vim.pack.add()| to current session. --- @field branches? string[] Available Git branches (first is default). Missing if `info=false`. --- @field path string Plugin's path on disk. ---- @field rev string Current Git revision. +--- @field rev string Current Git revision. Taken from |vim.pack-lockfile| if `info=false`. --- @field spec vim.pack.SpecResolved A |vim.pack.Spec| with resolved `name`. --- @field tags? string[] Available Git tags. Missing if `info=false`. @@ -1450,6 +1450,7 @@ local function add_p_data_info(p_data_list) --- @async funs[i] = function() p_data.branches = git_get_branches(path) + p_data.rev = git_get_hash('HEAD', path) p_data.tags = git_get_tags(path) end end diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua index 646456a555..f692e54e15 100644 --- a/test/functional/plugin/pack_spec.lua +++ b/test/functional/plugin/pack_spec.lua @@ -2073,11 +2073,13 @@ describe('vim.pack', function() local function make_basic_data(active, info) local spec = { name = 'basic', src = repos_src.basic, version = 'feat-branch' } local path = pack_get_plug_path('basic') - local rev = git_get_hash('feat-branch', 'basic') - local res = { active = active, path = path, spec = spec, rev = rev } + local res = { active = active, path = path, spec = spec } if info then res.branches = { 'main', 'feat-branch' } + res.rev = git_get_hash('feat-branch', 'basic') res.tags = { 'some-tag' } + else + res.rev = get_lock_tbl().plugins.basic.rev end return res end @@ -2085,11 +2087,13 @@ describe('vim.pack', function() local function make_defbranch_data(active, info) local spec = { name = 'defbranch', src = repos_src.defbranch } local path = pack_get_plug_path('defbranch') - local rev = git_get_hash('dev', 'defbranch') - local res = { active = active, path = path, spec = spec, rev = rev } + local res = { active = active, path = path, spec = spec } if info then res.branches = { 'dev', 'main' } + res.rev = git_get_hash('dev', 'defbranch') res.tags = {} + else + res.rev = get_lock_tbl().plugins.defbranch.rev end return res end @@ -2098,11 +2102,13 @@ describe('vim.pack', function() local spec = { name = 'plugindirs', src = repos_src.plugindirs, version = vim.version.range('*') } local path = pack_get_plug_path('plugindirs') - local rev = git_get_hash('v0.0.1', 'plugindirs') - local res = { active = active, path = path, spec = spec, rev = rev } + local res = { active = active, path = path, spec = spec } if info then res.branches = { 'main' } + res.rev = git_get_hash('v0.0.1', 'plugindirs') res.tags = { 'v0.0.1' } + else + res.rev = get_lock_tbl().plugins.plugindirs.rev end return res end @@ -2124,8 +2130,15 @@ describe('vim.pack', function() -- Should preserve order in which plugins were `vim.pack.add()`ed eq({ defbranch_data, basic_data, plugindirs_data }, exec_lua('return vim.pack.get()')) - -- Should also list non-active plugins + -- Should also list non-active plugins and use proper source for `rev` + local lock_tbl = get_lock_tbl() + lock_tbl.plugins.defbranch.rev = 'aaa' + lock_tbl.plugins.basic.rev = 'bbb' + lock_tbl.plugins.plugindirs.rev = 'ccc' + local lockfile_text = vim.json.encode(lock_tbl, { indent = ' ', sort_keys = true }) + fn.writefile(vim.split(lockfile_text, '\n'), get_lock_path()) n.clear() + vim_pack_add({ repos_src.defbranch }) defbranch_data = make_defbranch_data(true, true) basic_data = make_basic_data(false, true) @@ -2182,9 +2195,9 @@ describe('vim.pack', function() -- Should not include removed plugins immediately after they are removed, -- while still returning list without holes - exec_lua('vim.pack.del({ "defbranch" }, { force = true })') local defbranch_data = make_defbranch_data(true, true) local basic_data = make_basic_data(true, true) + exec_lua('vim.pack.del({ "defbranch" }, { force = true })') eq({ { defbranch_data, basic_data }, { basic_data } }, exec_lua('return _G.get_log')) end) From b9c4329c35552a5be71f99899528699018b2562c Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Sun, 17 May 2026 10:51:09 +0300 Subject: [PATCH 2/3] feat(pack): update `get()` to return revision of a pending update Problem: No convenient way to programmatically get the revision that would be checked out after `vim.pack.update()` (with `offline=true`). Doing this manually requires resolving `spec.version` which is not trivial. This data can be useful for custom reporting of pending updates or third party confirmation step. Solution: Make `get()` include a new field for the revision that points at the state after applying pending update. This is also the same as the revision of resolved `spec.version`. --- runtime/doc/news.txt | 1 + runtime/doc/pack.txt | 3 +++ runtime/lua/vim/pack.lua | 10 +++++++++- test/functional/plugin/pack_spec.lua | 25 +++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index f984be6552..bdcb470308 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -191,6 +191,7 @@ LUA but returns `nil` on error. • |vim.pos| can now convert between positions and buffer offsets. • |vim.log| for easily creating loggers. +• |vim.pack.get()| output includes revision of a pending update. OPTIONS diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt index 2a495ae3df..b17f9c1d79 100644 --- a/runtime/doc/pack.txt +++ b/runtime/doc/pack.txt @@ -498,6 +498,9 @@ get({names}, {opts}) *vim.pack.get()* • {path} (`string`) Plugin's path on disk. • {rev} (`string`) Current Git revision. Taken from |vim.pack-lockfile| if `info=false`. + • {rev_to}? (`string`) Git revision of a pending update. The same as + used during |vim.pack.update()| and which points to a resolved + `spec.version`. Missing if `info=false`. • {spec} (`vim.pack.SpecResolved`) A |vim.pack.Spec| with resolved `name`. • {tags}? (`string[]`) Available Git tags. Missing if `info=false`. diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index 655d6f14d6..7eddacc3e2 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -1435,6 +1435,9 @@ end --- @field branches? string[] Available Git branches (first is default). Missing if `info=false`. --- @field path string Plugin's path on disk. --- @field rev string Current Git revision. Taken from |vim.pack-lockfile| if `info=false`. +--- Git revision of a pending update. The same as used during |vim.pack.update()| and which +--- points to a resolved `spec.version`. Missing if `info=false`. +--- @field rev_to? string --- @field spec vim.pack.SpecResolved A |vim.pack.Spec| with resolved `name`. --- @field tags? string[] Available Git tags. Missing if `info=false`. @@ -1445,13 +1448,18 @@ end --- @param p_data_list vim.pack.PlugData[] local function add_p_data_info(p_data_list) local funs = {} --- @type (async fun())[] + local plug_dir = get_plug_dir() for i, p_data in ipairs(p_data_list) do + local plug = new_plug(p_data.spec, plug_dir) local path = p_data.path --- @async funs[i] = function() p_data.branches = git_get_branches(path) - p_data.rev = git_get_hash('HEAD', path) p_data.tags = git_get_tags(path) + + infer_revisions(plug) + p_data.rev = plug.info.sha_head + p_data.rev_to = plug.info.sha_target end end async_join_run_wait(funs) diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua index f692e54e15..3963234642 100644 --- a/test/functional/plugin/pack_spec.lua +++ b/test/functional/plugin/pack_spec.lua @@ -2077,6 +2077,7 @@ describe('vim.pack', function() if info then res.branches = { 'main', 'feat-branch' } res.rev = git_get_hash('feat-branch', 'basic') + res.rev_to = res.rev res.tags = { 'some-tag' } else res.rev = get_lock_tbl().plugins.basic.rev @@ -2091,6 +2092,7 @@ describe('vim.pack', function() if info then res.branches = { 'dev', 'main' } res.rev = git_get_hash('dev', 'defbranch') + res.rev_to = res.rev res.tags = {} else res.rev = get_lock_tbl().plugins.defbranch.rev @@ -2106,6 +2108,7 @@ describe('vim.pack', function() if info then res.branches = { 'main' } res.rev = git_get_hash('v0.0.1', 'plugindirs') + res.rev_to = res.rev res.tags = { 'v0.0.1' } else res.rev = get_lock_tbl().plugins.plugindirs.rev @@ -2168,6 +2171,28 @@ describe('vim.pack', function() eq({ defbranch_data }, exec_lua('return vim.pack.get({ "defbranch" }, { info = false })')) end) + it('reports potential revision after update', function() + -- Install and set up different version without running `vim.pack.update()` + vim_pack_add({ repos_src.defbranch, { src = repos_src.basic, version = 'feat-branch' } }) + pack_assert_content('defbranch', 'return "defbranch dev"') + pack_assert_content('basic', 'return "basic feat-branch"') + + n.clear() + vim_pack_add({ { src = repos_src.defbranch, version = 'main' }, repos_src.basic }) + n.clear() + + -- Should report correct `rev_to` with active and not active plugins + vim_pack_add({ { src = repos_src.defbranch, version = 'main' } }) + local defbranch_data = make_defbranch_data(true, true) + defbranch_data.spec.version = 'main' + defbranch_data.rev_to = git_get_hash('main', 'defbranch') + local basic_data = make_basic_data(false, true) + basic_data.spec.version = nil + basic_data.rev_to = git_get_hash('main', 'basic') + + eq({ defbranch_data, basic_data }, exec_lua('return vim.pack.get()')) + end) + it('respects `data` field', function() vim_pack_add({ { src = repos_src.basic, version = 'feat-branch', data = { test = 'value' } }, From 8f379be2619a7d6d25c102937e8425fdc952b1ab Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Sun, 17 May 2026 10:52:19 +0300 Subject: [PATCH 3/3] feat(pack): update `get()` to be able to fetch data from plugin source Problem: There is currently no convenient way to programmatically check for new updates from plugin source. Running `vim.pack.update()` is one approach, but it opens a confirmation buffer that requires a manual action to close. Solution: Add `opts.offline` to `vim.pack.get()` that will first fetch new updates from plugin source before computing the output. --- runtime/doc/news.txt | 1 + runtime/doc/pack.txt | 9 +++++- runtime/lua/vim/pack.lua | 33 ++++++++++++++----- test/functional/plugin/pack_spec.lua | 47 ++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 8 deletions(-) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index bdcb470308..fb56f69ed5 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -192,6 +192,7 @@ LUA • |vim.pos| can now convert between positions and buffer offsets. • |vim.log| for easily creating loggers. • |vim.pack.get()| output includes revision of a pending update. +• |vim.pack.get()| can fetch new updates before computing the output. OPTIONS diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt index b17f9c1d79..721b39182b 100644 --- a/runtime/doc/pack.txt +++ b/runtime/doc/pack.txt @@ -350,6 +350,11 @@ Remove plugins from disk ~ • Use |:packdel| with plugin names to remove. Use `:packdel ++all` to delete all inactive plugins. +Check for pending updates ~ +• Run `vim.pack.get(nil, { offline = false })` and check the output for items + with different `rev` and `rev_to` fields. To not download new updates from + source, use plain `vim.pack.get()`. + Commands *vim.pack-commands* *E5807* @@ -486,8 +491,10 @@ get({names}, {opts}) *vim.pack.get()* • {names} (`string[]?`) List of plugin names. Default: all plugins managed by |vim.pack|. • {opts} (`table?`) A table with the following fields: - • {info} (`boolean`) Whether to include extra plugin info. + • {info}? (`boolean`) Whether to include extra plugin info. Default `true`. + • {offline}? (`boolean`) Whether to skip downloading new + updates. Requires `info=true`. Default: `true`. Return: ~ (`table[]`) A list of objects with the following fields: diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index 7eddacc3e2..0d2472d082 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -156,6 +156,12 @@ ---- Use |:packdel| with plugin names to remove. Use `:packdel ++all` to delete --- all inactive plugins. --- +---Check for pending updates ~ +--- +---- Run `vim.pack.get(nil, { offline = false })` and check the output for items +--- with different `rev` and `rev_to` fields. To not download new updates +--- from source, use plain `vim.pack.get()`. +--- ---
help
 --- Commands                                             *vim.pack-commands* *E5807*
 ---
@@ -317,6 +323,14 @@ local function git_get_hash(ref, cwd)
   return git_cmd({ 'rev-list', '-1', ref }, cwd)
 end
 
+--- @async
+--- @param cwd string
+local function git_fetch(cwd)
+  -- Using '--tags --force' means conflicting tags will be synced with remote
+  local args = { 'fetch', '--quiet', '--tags', '--force', '--recurse-submodules=yes', 'origin' }
+  git_cmd(args, cwd)
+end
+
 --- @async
 --- @param cwd string
 --- @return string
@@ -1313,9 +1327,7 @@ function M.update(names, opts)
 
     -- Fetch
     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)
+      git_fetch(p.path)
     end
 
     -- Compute change info: changelog if any, new tags if nothing to update
@@ -1443,10 +1455,13 @@ end
 
 --- @class vim.pack.keyset.get
 --- @inlinedoc
---- @field info boolean Whether to include extra plugin info. Default `true`.
+--- @field info? boolean Whether to include extra plugin info. Default `true`.
+--- Whether to skip downloading new updates. Requires `info=true`. Default: `true`.
+--- @field offline? boolean
 
 --- @param p_data_list vim.pack.PlugData[]
-local function add_p_data_info(p_data_list)
+--- @param offline boolean
+local function add_p_data_info(p_data_list, offline)
   local funs = {} --- @type (async fun())[]
   local plug_dir = get_plug_dir()
   for i, p_data in ipairs(p_data_list) do
@@ -1457,6 +1472,10 @@ local function add_p_data_info(p_data_list)
       p_data.branches = git_get_branches(path)
       p_data.tags = git_get_tags(path)
 
+      if not offline then
+        git_fetch(path)
+      end
+
       infer_revisions(plug)
       p_data.rev = plug.info.sha_head
       p_data.rev_to = plug.info.sha_target
@@ -1471,7 +1490,7 @@ end
 --- @return vim.pack.PlugData[]
 function M.get(names, opts)
   vim.validate('names', names, vim.islist, true, 'list')
-  opts = vim.tbl_extend('force', { info = true }, opts or {})
+  opts = vim.tbl_extend('force', { info = true, offline = true }, opts or {})
 
   -- Process active plugins in order they were added. Take into account that
   -- there might be "holes" after `vim.pack.del()`.
@@ -1520,7 +1539,7 @@ function M.get(names, opts)
 
   if opts.info then
     git_ensure_exec()
-    add_p_data_info(res)
+    add_p_data_info(res, opts.offline)
   end
 
   return res
diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua
index 3963234642..c6bb3121a1 100644
--- a/test/functional/plugin/pack_spec.lua
+++ b/test/functional/plugin/pack_spec.lua
@@ -2193,6 +2193,53 @@ describe('vim.pack', function()
       eq({ defbranch_data, basic_data }, exec_lua('return vim.pack.get()'))
     end)
 
+    describe('opts.offline', function()
+      after_each(function()
+        n.rmdir(repo_get_path('fetch'))
+      end)
+
+      it('can fetch new updates', function()
+        -- Create a dedicated clean repo for which "push changes" will be mocked
+        init_test_repo('fetch')
+
+        repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch init"')
+        git_add_commit('Initial commit', 'fetch')
+
+        local fetch_head = git_get_hash('HEAD', 'fetch')
+
+        -- Install initial versions of tested plugins
+        vim_pack_add({ repos_src.fetch })
+
+        -- Mock remote repo update
+        -- - Force push
+        repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new"')
+        git_cmd({ 'add', '*' }, 'fetch')
+        git_cmd({ 'commit', '--amend', '-m', 'Second commit' }, 'fetch')
+        local fetch_new = git_get_hash('HEAD', 'fetch')
+        t.neq(fetch_head, fetch_new)
+
+        -- Should not fetch new data with `offline=true` (default)
+        -- or if `info=false`
+        local spec = { name = 'fetch', src = repos_src.fetch }
+        local path = pack_get_plug_path('fetch')
+        local fetch_data = { active = true, path = path, spec = spec }
+        fetch_data.branches = { 'main' }
+        fetch_data.tags = {}
+        fetch_data.rev = fetch_head
+        fetch_data.rev_to = fetch_head
+
+        exec_lua('vim.pack.get(nil, { info = false, offline = false })')
+        eq({ fetch_data }, exec_lua('return vim.pack.get()'))
+
+        -- Should fetch new data with `offline=false`
+        fetch_data.rev_to = fetch_new
+        eq({ fetch_data }, exec_lua('return vim.pack.get(nil, { offline = false })'))
+
+        -- Should keep using already fetched data with `offline=true`
+        eq({ fetch_data }, exec_lua('return vim.pack.get(nil, {})'))
+      end)
+    end)
+
     it('respects `data` field', function()
       vim_pack_add({
         { src = repos_src.basic, version = 'feat-branch', data = { test = 'value' } },