diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index c53d13e738..819b4dc40d 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -201,6 +201,8 @@ LUA • |vim.pos| can now convert between positions and buffer offsets. • |vim.ui.input()| now allows setting input scope. • |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 b5cb447d6f..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: @@ -496,7 +503,11 @@ 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`. + • {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 bd693904cd..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
@@ -1434,23 +1446,39 @@ 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`.
+--- 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`.
 
 --- @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
+    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.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
     end
   end
   async_join_run_wait(funs)
@@ -1462,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()`.
@@ -1511,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 646456a555..c6bb3121a1 100644
--- a/test/functional/plugin/pack_spec.lua
+++ b/test/functional/plugin/pack_spec.lua
@@ -2073,11 +2073,14 @@ 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.rev_to = res.rev
         res.tags = { 'some-tag' }
+      else
+        res.rev = get_lock_tbl().plugins.basic.rev
       end
       return res
     end
@@ -2085,11 +2088,14 @@ 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.rev_to = res.rev
         res.tags = {}
+      else
+        res.rev = get_lock_tbl().plugins.defbranch.rev
       end
       return res
     end
@@ -2098,11 +2104,14 @@ 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.rev_to = res.rev
         res.tags = { 'v0.0.1' }
+      else
+        res.rev = get_lock_tbl().plugins.plugindirs.rev
       end
       return res
     end
@@ -2124,8 +2133,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)
@@ -2155,6 +2171,75 @@ 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)
+
+    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' } },
@@ -2182,9 +2267,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)