commit ea124068f246eeb5e436c1fc62c9379d9b936f15
parent b4513442a158f416eac37cdfa9ba282f58372679
Author: Justin M. Keyes <justinkz@gmail.com>
Date: Sat, 4 Oct 2025 12:48:29 -0400
feat(vim.pack): lockfile support #35827
Diffstat:
3 files changed, 366 insertions(+), 68 deletions(-)
diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt
@@ -221,6 +221,13 @@ Uses Git to manage plugins and requires present `git` executable of at least
version 2.36. Target plugins should be Git repositories with versions as named
tags following semver convention `v<major>.<minor>.<patch>`.
+The latest state of all managed plugins is stored inside a *vim.pack-lockfile*
+located at `$XDG_CONFIG_HOME/nvim/nvim-pack-lock.json`. It is a JSON file that
+is used to persistently track data about plugins. For a more robust config
+treat lockfile like its part: put under version control, etc. In this case
+initial install prefers revision from the lockfile instead of inferring from
+`version`. Should not be edited by hand or deleted.
+
Example workflows ~
Basic install and management:
@@ -254,17 +261,20 @@ Basic install and management:
plugin1 = require('plugin1')
<
• Restart Nvim (for example, with |:restart|). Plugins that were not yet
- installed will be available on disk in target state after `add()` call.
+ installed will be available on disk after `add()` call. Their revision is
+ taken from |vim.pack-lockfile| (if present) or inferred from the `version`.
• To update all plugins with new changes:
• Execute |vim.pack.update()|. This will download updates from source and
show confirmation buffer in a separate tabpage.
• Review changes. To confirm all updates execute |:write|. 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 state on disk is not yet changed.
+• |:restart|. The plugin's actual state 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
@@ -272,7 +282,7 @@ Switch plugin's version:
Freeze plugin from being updated:
• Update 'init.lua' for plugin to have `version` set to current revision. Get
- it with `:=vim.pack.get({ 'plug-name' })[1].rev` (looks like `abc12345`).
+ it from |vim.pack-lockfile| (plugin's field `rev`; looks like `abc12345`).
• |:restart|.
Unfreeze plugin to start receiving updates:
@@ -371,7 +381,7 @@ 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. Missing if `info=false`.
+ • {rev} (`string`) Current Git revision.
• {spec} (`vim.pack.SpecResolved`) A |vim.pack.Spec| with resolved
`name`.
• {tags}? (`string[]`) Available Git tags. Missing if `info=false`.
@@ -402,8 +412,7 @@ update({names}, {opts}) *vim.pack.update()*
Parameters: ~
• {names} (`string[]?`) List of plugin names to update. Must be managed
by |vim.pack|, not necessarily already added to current
- session. Default: names of all plugins added to current
- session via |vim.pack.add()|.
+ session. Default: names of all plugins managed by |vim.pack|.
• {opts} (`table?`) A table with the following fields:
• {force}? (`boolean`) Whether to skip confirmation and make
updates immediately. Default `false`.
diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua
@@ -14,6 +14,13 @@
---least version 2.36. Target plugins should be Git repositories with versions
---as named tags following semver convention `v<major>.<minor>.<patch>`.
---
+---The latest state of all managed plugins is stored inside a [vim.pack-lockfile]()
+---located at `$XDG_CONFIG_HOME/nvim/nvim-pack-lock.json`. It is a JSON file that
+---is used to persistently track data about plugins.
+---For a more robust config treat lockfile like its part: put under version control, etc.
+---In this case initial install prefers revision from the lockfile instead of
+---inferring from `version`. Should not be edited by hand or deleted.
+---
---Example workflows ~
---
---Basic install and management:
@@ -50,18 +57,21 @@
---```
---
---- Restart Nvim (for example, with |:restart|). Plugins that were not yet
----installed will be available on disk in target state after `add()` call.
+---installed will be available on disk after `add()` call. Their revision is
+---taken from |vim.pack-lockfile| (if present) or inferred from the `version`.
---
---- To update all plugins with new changes:
--- - Execute |vim.pack.update()|. This will download updates from source and
--- show confirmation buffer in a separate tabpage.
--- - Review changes. To confirm all updates execute |:write|.
--- 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 state 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
@@ -69,7 +79,7 @@
---
---Freeze plugin from being updated:
---- Update 'init.lua' for plugin to have `version` set to current revision.
----Get it with `:=vim.pack.get({ 'plug-name' })[1].rev` (looks like `abc12345`).
+---Get it from |vim.pack-lockfile| (plugin's field `rev`; looks like `abc12345`).
---- |:restart|.
---
---Unfreeze plugin to start receiving updates:
@@ -190,13 +200,72 @@ local function git_get_tags(cwd)
return tags == '' and {} or vim.split(tags, '\n')
end
--- Plugin operations ----------------------------------------------------------
+-- Lockfile -------------------------------------------------------------------
--- @return string
local function get_plug_dir()
return vim.fs.joinpath(vim.fn.stdpath('data'), 'site', 'pack', 'core', 'opt')
end
+--- @class (private) vim.pack.LockData
+--- @field rev string Latest recorded revision.
+--- @field src string Plugin source.
+--- @field version? string|vim.VersionRange Plugin `version`, as supplied in `spec`.
+
+--- @class (private) vim.pack.Lock
+--- @field plugins table<string, vim.pack.LockData> Map from plugin name to its lock data.
+
+--- @type vim.pack.Lock
+local plugin_lock
+
+local function lock_get_path()
+ return vim.fs.joinpath(vim.fn.stdpath('config'), 'nvim-pack-lock.json')
+end
+
+local function lock_read()
+ if plugin_lock then
+ return
+ end
+ local fd = uv.fs_open(lock_get_path(), 'r', 438)
+ if not fd then
+ plugin_lock = { plugins = {} }
+ return
+ end
+ local stat = assert(uv.fs_fstat(fd))
+ local data = assert(uv.fs_read(fd, stat.size, 0))
+ assert(uv.fs_close(fd))
+ plugin_lock = vim.json.decode(data) --- @type vim.pack.Lock
+
+ -- Deserialize `version`
+ for _, l_data in pairs(plugin_lock.plugins) do
+ local version = l_data.version
+ if type(version) == 'string' then
+ l_data.version = version:match("^'(.+)'$") or vim.version.range(version)
+ end
+ end
+end
+
+local function lock_write()
+ -- Serialize `version`
+ local lock = vim.deepcopy(plugin_lock)
+ for _, l_data in pairs(lock.plugins) do
+ local version = l_data.version
+ if version then
+ l_data.version = type(version) == 'string' and ("'%s'"):format(version) or tostring(version)
+ end
+ end
+
+ local path = lock_get_path()
+ vim.fn.mkdir(vim.fs.dirname(path), 'p')
+ local fd = assert(uv.fs_open(path, 'w', 438))
+
+ local data = vim.json.encode(lock, { indent = ' ', sort_keys = true })
+ assert(uv.fs_write(fd, data))
+ assert(uv.fs_close(fd))
+end
+
+-- Plugin operations ----------------------------------------------------------
+
--- @param msg string|string[]
--- @param level ('DEBUG'|'TRACE'|'INFO'|'WARN'|'ERROR')?
local function notify(msg, level)
@@ -334,15 +403,8 @@ local function plug_list_from_names(names)
local plug_dir = get_plug_dir()
local plugs = {} --- @type vim.pack.Plug[]
for _, p_data in ipairs(p_data_list) do
- -- NOTE: By default include only active plugins (and not all on disk). Using
- -- not active plugins might lead to a confusion as default `version` and
- -- user's desired one might mismatch.
- -- TODO(echasnovski): Change this when there is lockfile.
- if names ~= nil or p_data.active then
- plugs[#plugs + 1] = new_plug(p_data.spec, plug_dir)
- end
+ plugs[#plugs + 1] = new_plug(p_data.spec, plug_dir)
end
-
return plugs
end
@@ -532,6 +594,8 @@ local function checkout(p, timestamp, skip_same_sha)
git_cmd({ 'checkout', '--quiet', p.info.sha_target }, p.path)
+ plugin_lock.plugins[p.spec.name].rev = p.info.sha_target
+
trigger_event(p, 'PackChanged', 'update')
-- (Re)Generate help tags according to the current help files.
@@ -561,6 +625,11 @@ local function install_list(plug_list, confirm)
git_clone(p.spec.src, p.path)
p.info.installed = true
+ plugin_lock.plugins[p.spec.name].src = p.spec.src
+
+ -- Prefer revision from the lockfile instead of using `version`
+ p.info.sha_target = (plugin_lock.plugins[p.spec.name] or {}).rev
+
-- Do not skip checkout even if HEAD and target have same commit hash to
-- have new repo in expected detached HEAD state and generated help files.
checkout(p, timestamp, false)
@@ -698,17 +767,34 @@ function M.add(specs, opts)
end
plugs = normalize_plugs(plugs)
- -- Install
- --- @param p vim.pack.Plug
- local plugs_to_install = vim.tbl_filter(function(p)
- return not p.info.installed
- end, plugs)
+ -- Pre-process
+ lock_read()
+ local plugs_to_install = {} --- @type vim.pack.Plug[]
+ local needs_lock_write = false
+ for _, p in ipairs(plugs) do
+ -- TODO(echasnovski): check that lock's `src` is the same as in spec.
+ -- If not - cleanly reclone (delete directory and mark as not installed).
+ local p_lock = plugin_lock.plugins[p.spec.name] or {}
+ needs_lock_write = needs_lock_write or p_lock.version ~= p.spec.version
+ p_lock.version = p.spec.version
+ plugin_lock.plugins[p.spec.name] = p_lock
+
+ if not p.info.installed then
+ plugs_to_install[#plugs_to_install + 1] = p
+ needs_lock_write = true
+ end
+ end
+ -- Install
if #plugs_to_install > 0 then
git_ensure_exec()
install_list(plugs_to_install, opts.confirm)
end
+ if needs_lock_write then
+ lock_write()
+ end
+
-- Register and load those actually on disk while collecting errors
-- Delay showing all errors to have "good" plugins added first
local errors = {} --- @type string[]
@@ -887,7 +973,7 @@ end
---
--- @param names? string[] List of plugin names to update. Must be managed
--- by |vim.pack|, not necessarily already added to current session.
---- Default: names of all plugins added to current session via |vim.pack.add()|.
+--- Default: names of all plugins managed by |vim.pack|.
--- @param opts? vim.pack.keyset.update
function M.update(names, opts)
vim.validate('names', names, vim.islist, true, 'list')
@@ -899,6 +985,7 @@ function M.update(names, opts)
return
end
git_ensure_exec()
+ lock_read()
-- Perform update
local timestamp = get_timestamp()
@@ -925,6 +1012,7 @@ function M.update(names, opts)
run_list(plug_list, do_update, progress_title)
if opts.force then
+ lock_write()
feedback_log(plug_list)
return
end
@@ -950,6 +1038,7 @@ function M.update(names, opts)
end
run_list(plugs_to_checkout, do_checkout, 'Applying updates')
+ lock_write()
feedback_log(plugs_to_checkout)
end)
end
@@ -967,6 +1056,8 @@ function M.del(names)
return
end
+ lock_read()
+
for _, p in ipairs(plug_list) do
trigger_event(p, 'PackChangedPre', 'delete')
@@ -974,8 +1065,12 @@ function M.del(names)
active_plugins[p.path] = nil
notify(("Removed plugin '%s'"):format(p.spec.name), 'INFO')
+ plugin_lock.plugins[p.spec.name] = nil
+
trigger_event(p, 'PackChanged', 'delete')
end
+
+ lock_write()
end
--- @inlinedoc
@@ -983,7 +1078,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. Missing if `info=false`.
+--- @field rev string Current Git revision.
--- @field spec vim.pack.SpecResolved A |vim.pack.Spec| with resolved `name`.
--- @field tags? string[] Available Git tags. Missing if `info=false`.
@@ -999,7 +1094,6 @@ 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
@@ -1025,30 +1119,29 @@ function M.get(names, opts)
active[p_active.id] = p_active.plug
end
+ lock_read()
local res = {} --- @type vim.pack.PlugData[]
local used_names = {} --- @type table<string,boolean>
for i = 1, n_active_plugins do
if active[i] and (not names or vim.tbl_contains(names, active[i].spec.name)) then
- res[#res + 1] = { spec = vim.deepcopy(active[i].spec), path = active[i].path, active = true }
- used_names[active[i].spec.name] = true
+ local name = active[i].spec.name
+ local spec = vim.deepcopy(active[i].spec)
+ local rev = (plugin_lock.plugins[name] or {}).rev
+ res[#res + 1] = { spec = spec, path = active[i].path, rev = rev, active = true }
+ used_names[name] = true
end
end
- --- @async
- local function do_get()
- -- Process not active plugins
- local plug_dir = get_plug_dir()
- for n, t in vim.fs.dir(plug_dir, { depth = 1 }) do
- local path = vim.fs.joinpath(plug_dir, n)
- local is_in_names = not names or vim.tbl_contains(names, n)
- if t == 'directory' and not active_plugins[path] and is_in_names then
- local spec = { name = n, src = git_cmd({ 'remote', 'get-url', 'origin' }, path) }
- res[#res + 1] = { spec = spec, path = path, active = false }
- used_names[n] = true
- end
+ local plug_dir = get_plug_dir()
+ for name, l_data in vim.spairs(plugin_lock.plugins) do
+ local path = vim.fs.joinpath(plug_dir, name)
+ local is_in_names = not names or vim.tbl_contains(names, name)
+ if not active_plugins[path] and is_in_names then
+ local spec = { name = name, src = l_data.src, version = l_data.version }
+ res[#res + 1] = { spec = spec, path = path, rev = l_data.rev, active = false }
+ used_names[name] = true
end
end
- async.run(do_get):wait()
if names ~= nil then
-- Align result with input
diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua
@@ -135,6 +135,12 @@ end
function repos_setup.plugindirs()
init_test_repo('plugindirs')
+ -- Add semver tag
+ repo_write_file('plugindirs', 'lua/plugindirs.lua', 'return "plugindirs v0.0.1"')
+ git_add_commit('Add version v0.0.1', 'plugindirs')
+ git_cmd({ 'tag', 'v0.0.1' }, 'plugindirs')
+
+ -- Add various 'plugin/' files
repo_write_file('plugindirs', 'lua/plugindirs.lua', 'return "plugindirs main"')
repo_write_file('plugindirs', 'plugin/dirs.lua', 'vim.g._plugin = true')
repo_write_file('plugindirs', 'plugin/dirs_log.lua', '_G.DL = _G.DL or {}; DL[#DL+1] = "p"')
@@ -310,6 +316,14 @@ local function is_jit()
return exec_lua('return package.loaded.jit ~= nil')
end
+local function get_lock_path()
+ return vim.fs.joinpath(fn.stdpath('config'), 'nvim-pack-lock.json')
+end
+
+local function get_lock_tbl()
+ return vim.json.decode(fn.readblob(get_lock_path()))
+end
+
-- Tests ======================================================================
describe('vim.pack', function()
@@ -326,6 +340,9 @@ describe('vim.pack', function()
after_each(function()
vim.fs.rm(pack_get_dir(), { force = true, recursive = true })
+ vim.fs.rm(get_lock_path(), { force = true })
+ local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log')
+ pcall(vim.fs.rm, log_path, { force = true })
end)
teardown(function()
@@ -413,6 +430,117 @@ describe('vim.pack', function()
eq('plugindirs main', exec_lua('return require("plugindirs")'))
end)
+ it('creates lockfile', function()
+ local helptags_rev = git_get_hash('HEAD', 'helptags')
+ exec_lua(function()
+ vim.pack.add({
+ { src = repos_src.basic, version = 'some-tag' },
+ { src = repos_src.defbranch, version = 'main' },
+ { src = repos_src.helptags, version = helptags_rev },
+ { src = repos_src.plugindirs },
+ { src = repos_src.semver, version = vim.version.range('*') },
+ })
+ end)
+
+ local basic_rev = git_get_hash('some-tag', 'basic')
+ local defbranch_rev = git_get_hash('main', 'defbranch')
+ local plugindirs_rev = git_get_hash('HEAD', 'plugindirs')
+ local semver_rev = git_get_hash('v1.0.0', 'semver')
+
+ -- Should properly format as indented JSON
+ local ref_lockfile_lines = {
+ '{',
+ ' "plugins": {',
+ ' "basic": {',
+ ' "rev": "' .. basic_rev .. '",',
+ ' "src": "' .. repos_src.basic .. '",',
+ -- Branch, tag, and commit should be serialized like `'value'` to be
+ -- distinguishable from version ranges
+ ' "version": "\'some-tag\'"',
+ ' },',
+ ' "defbranch": {',
+ ' "rev": "' .. defbranch_rev .. '",',
+ ' "src": "' .. repos_src.defbranch .. '",',
+ ' "version": "\'main\'"',
+ ' },',
+ ' "helptags": {',
+ ' "rev": "' .. helptags_rev .. '",',
+ ' "src": "' .. repos_src.helptags .. '",',
+ ' "version": "\'' .. helptags_rev .. '\'"',
+ ' },',
+ ' "plugindirs": {',
+ ' "rev": "' .. plugindirs_rev .. '",',
+ ' "src": "' .. repos_src.plugindirs .. '"',
+ -- Absent `version` should be missing and not autoresolved
+ ' },',
+ ' "semver": {',
+ ' "rev": "' .. semver_rev .. '",',
+ ' "src": "' .. repos_src.semver .. '",',
+ ' "version": ">=0.0.0"',
+ ' }',
+ ' }',
+ '}',
+ }
+ eq(ref_lockfile_lines, fn.readfile(get_lock_path()))
+ end)
+
+ it('updates lockfile', function()
+ exec_lua(function()
+ vim.pack.add({ repos_src.basic })
+ end)
+ local ref_lockfile = {
+ plugins = {
+ basic = { rev = git_get_hash('main', 'basic'), src = repos_src.basic },
+ },
+ }
+ eq(ref_lockfile, get_lock_tbl())
+
+ n.clear()
+ exec_lua(function()
+ vim.pack.add({ { src = repos_src.basic, version = 'main' } })
+ end)
+
+ ref_lockfile.plugins.basic.version = "'main'"
+ eq(ref_lockfile, get_lock_tbl())
+ end)
+
+ it('uses lockfile revision during install', function()
+ exec_lua(function()
+ vim.pack.add({ { src = repos_src.basic, version = 'feat-branch' } })
+ end)
+
+ -- Mock clean initial install, but with lockfile present
+ n.clear()
+ local basic_plug_path = vim.fs.joinpath(pack_get_dir(), 'basic')
+ vim.fs.rm(basic_plug_path, { force = true, recursive = true })
+
+ local basic_rev = git_get_hash('feat-branch', 'basic')
+ local ref_lockfile = {
+ plugins = {
+ basic = { rev = basic_rev, src = repos_src.basic, version = "'feat-branch'" },
+ },
+ }
+ eq(ref_lockfile, get_lock_tbl())
+
+ exec_lua(function()
+ -- Should use revision from lockfile (pointing at latest 'feat-branch'
+ -- commit) and not use latest `main` commit
+ vim.pack.add({ { src = repos_src.basic, version = 'main' } })
+ end)
+ local basic_lua_file = vim.fs.joinpath(pack_get_plug_path('basic'), 'lua', 'basic.lua')
+ eq({ 'return "basic feat-branch"' }, fn.readfile(basic_lua_file))
+
+ -- Running `update()` should still update to use `main`
+ exec_lua(function()
+ vim.pack.update(nil, { force = true })
+ end)
+ eq({ 'return "basic main"' }, fn.readfile(basic_lua_file))
+
+ ref_lockfile.plugins.basic.rev = git_get_hash('main', 'basic')
+ ref_lockfile.plugins.basic.version = "'main'"
+ eq(ref_lockfile, get_lock_tbl())
+ end)
+
it('installs at proper version', function()
local out = exec_lua(function()
vim.pack.add({
@@ -597,9 +725,8 @@ describe('vim.pack', function()
eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }'))
-- Plugins should still be marked as "active", since they were added
- plugindirs_data.active = true
- basic_data.active = true
- eq({ plugindirs_data, basic_data }, exec_lua('return vim.pack.get(nil, { info = false })'))
+ eq(true, exec_lua('return vim.pack.get({ "plugindirs" })[1].active'))
+ eq(true, exec_lua('return vim.pack.get({ "basic" })[1].active'))
end
-- Works on initial install
@@ -759,7 +886,7 @@ describe('vim.pack', function()
-- Install initial versions of tested plugins
exec_lua(function()
vim.pack.add({
- repos_src.fetch,
+ { src = repos_src.fetch, version = 'main' },
{ src = repos_src.semver, version = 'v0.3.0' },
repos_src.defbranch,
})
@@ -777,6 +904,11 @@ describe('vim.pack', function()
repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new 2"')
git_add_commit('Commit to be added 2', 'fetch')
+
+ -- Make `dev` default remote branch to check that `version` is respected
+ git_cmd({ 'checkout', '-b', 'dev' }, 'fetch')
+ repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch dev"')
+ git_add_commit('Commit from default `dev` branch', 'fetch')
end)
after_each(function()
@@ -854,8 +986,8 @@ describe('vim.pack', function()
local screen
screen = Screen.new(85, 35)
- hashes.fetch_new = git_get_hash('HEAD', 'fetch')
- hashes.fetch_new_prev = git_get_hash('HEAD~', 'fetch')
+ hashes.fetch_new = git_get_hash('main', 'fetch')
+ hashes.fetch_new_prev = git_get_hash('main~', 'fetch')
hashes.semver_head = git_get_hash('v0.3.0', 'semver')
local tab_name = 'n' .. (t.is_os('win') and ':' or '') .. '//2/confirm-update'
@@ -1087,12 +1219,27 @@ describe('vim.pack', function()
local confirm_text = table.concat(api.nvim_buf_get_lines(0, 0, -1, false), '\n')
matches('Available newer versions:\n• v1%.0%.0\n• v0%.4\n• 0%.3%.1$', confirm_text)
end)
+
+ it('updates lockfile', function()
+ exec_lua(function()
+ vim.pack.add({ repos_src.fetch })
+ end)
+ local ref_fetch_lock = { rev = hashes.fetch_head, src = repos_src.fetch }
+ eq(ref_fetch_lock, get_lock_tbl().plugins.fetch)
+
+ exec_lua('vim.pack.update()')
+ n.exec('write')
+
+ ref_fetch_lock.rev = git_get_hash('main', 'fetch')
+ eq(ref_fetch_lock, get_lock_tbl().plugins.fetch)
+ end)
end)
it('works with not active plugins', function()
+ -- No plugins are added, but they are installed in `before_each()`
exec_lua(function()
- -- No plugins are added, but they are installed in `before_each()`
- vim.pack.update({ 'fetch' })
+ -- By default should also include not active plugins
+ vim.pack.update()
end)
eq({ 'return "fetch main"' }, fn.readfile(fetch_lua_file))
n.exec('write')
@@ -1116,8 +1263,8 @@ describe('vim.pack', function()
eq('', api.nvim_get_option_value('filetype', {}))
-- Write to log file
- hashes.fetch_new = git_get_hash('HEAD', 'fetch')
- hashes.fetch_new_prev = git_get_hash('HEAD~', 'fetch')
+ hashes.fetch_new = git_get_hash('main', 'fetch')
+ hashes.fetch_new_prev = git_get_hash('main~', 'fetch')
local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log')
local log_lines = fn.readfile(log_path)
@@ -1138,17 +1285,21 @@ describe('vim.pack', function()
'',
}
eq(ref_log_lines, vim.list_slice(log_lines, 2))
+
+ -- Should update lockfile
+ eq(hashes.fetch_new, get_lock_tbl().plugins.fetch.rev)
end)
it('shows progress report', function()
track_nvim_echo()
exec_lua(function()
vim.pack.add({ repos_src.fetch, repos_src.defbranch })
+ -- Should also include updates from not active plugins
vim.pack.update()
end)
-- During initial download
- validate_progress_report('Downloading updates', { 'fetch', 'defbranch' })
+ validate_progress_report('Downloading updates', { 'fetch', 'defbranch', 'semver' })
exec_lua('_G.echo_log = {}')
-- During application (only for plugins that have updates)
@@ -1165,7 +1316,7 @@ describe('vim.pack', function()
vim.pack.add({ repos_src.fetch, repos_src.defbranch })
vim.pack.update(nil, { force = true })
end)
- validate_progress_report('Updating', { 'fetch', 'defbranch' })
+ validate_progress_report('Updating', { 'fetch', 'defbranch', 'semver' })
end)
it('triggers relevant events', function()
@@ -1246,10 +1397,10 @@ describe('vim.pack', function()
local make_basic_data = function(active, info)
local spec = { name = 'basic', src = repos_src.basic, version = 'feat-branch' }
local path = pack_get_plug_path('basic')
- local res = { active = active, path = path, spec = spec }
+ local rev = git_get_hash('feat-branch', 'basic')
+ local res = { active = active, path = path, spec = spec, rev = rev }
if info then
res.branches = { 'main', 'feat-branch' }
- res.rev = git_get_hash('feat-branch', 'basic')
res.tags = { 'some-tag' }
end
return res
@@ -1258,50 +1409,74 @@ describe('vim.pack', function()
local make_defbranch_data = function(active, info)
local spec = { name = 'defbranch', src = repos_src.defbranch }
local path = pack_get_plug_path('defbranch')
- local res = { active = active, path = path, spec = spec }
+ local rev = git_get_hash('dev', 'defbranch')
+ local res = { active = active, path = path, spec = spec, rev = rev }
if info then
res.branches = { 'dev', 'main' }
- res.rev = git_get_hash('dev', 'defbranch')
res.tags = {}
end
return res
end
+ local make_plugindirs_data = function(active, info)
+ 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 }
+ if info then
+ res.branches = { 'main' }
+ res.tags = { 'v0.0.1' }
+ end
+ return res
+ end
+
it('returns list with necessary data', function()
- local basic_data, defbranch_data
+ local basic_data, defbranch_data, plugindirs_data
-- Should work just after installation
exec_lua(function()
- vim.pack.add({ repos_src.defbranch, { src = repos_src.basic, version = 'feat-branch' } })
+ vim.pack.add({
+ repos_src.defbranch,
+ { src = repos_src.basic, version = 'feat-branch' },
+ { src = repos_src.plugindirs, version = vim.version.range('*') },
+ })
end)
defbranch_data = make_defbranch_data(true, true)
basic_data = make_basic_data(true, true)
+ plugindirs_data = make_plugindirs_data(true, true)
-- Should preserve order in which plugins were `vim.pack.add()`ed
- eq({ defbranch_data, basic_data }, exec_lua('return vim.pack.get()'))
+ eq({ defbranch_data, basic_data, plugindirs_data }, exec_lua('return vim.pack.get()'))
-- Should also list non-active plugins
n.clear()
exec_lua(function()
- vim.pack.add({ { src = repos_src.basic, version = 'feat-branch' } })
+ vim.pack.add({ repos_src.defbranch })
end)
- defbranch_data = make_defbranch_data(false, true)
- basic_data = make_basic_data(true, true)
- -- Should first list active, then non-active
- eq({ basic_data, defbranch_data }, exec_lua('return vim.pack.get()'))
+ defbranch_data = make_defbranch_data(true, true)
+ basic_data = make_basic_data(false, true)
+ plugindirs_data = make_plugindirs_data(false, true)
+ -- Should first list active, then non-active (including their latest
+ -- set `version` which is inferred from lockfile)
+ eq({ defbranch_data, basic_data, plugindirs_data }, exec_lua('return vim.pack.get()'))
-- Should respect `names` for both active and not active plugins
eq({ basic_data }, exec_lua('return vim.pack.get({ "basic" })'))
eq({ defbranch_data }, exec_lua('return vim.pack.get({ "defbranch" })'))
- eq({ defbranch_data, basic_data }, exec_lua('return vim.pack.get({ "defbranch", "basic" })'))
+ eq({ basic_data, defbranch_data }, exec_lua('return vim.pack.get({ "basic", "defbranch" })'))
local bad_get_cmd = 'return vim.pack.get({ "ccc", "basic", "aaa" })'
matches('Plugin `ccc` is not installed', pcall_err(exec_lua, bad_get_cmd))
-- Should respect `opts.info`
- defbranch_data = make_defbranch_data(false, false)
- basic_data = make_basic_data(true, false)
- eq({ basic_data, defbranch_data }, exec_lua('return vim.pack.get(nil, { info = false })'))
+ defbranch_data = make_defbranch_data(true, false)
+ basic_data = make_basic_data(false, false)
+ plugindirs_data = make_plugindirs_data(false, false)
+ eq(
+ { defbranch_data, basic_data, plugindirs_data },
+ exec_lua('return vim.pack.get(nil, { info = false })')
+ )
eq({ basic_data }, exec_lua('return vim.pack.get({ "basic" }, { info = false })'))
eq({ defbranch_data }, exec_lua('return vim.pack.get({ "defbranch" }, { info = false })'))
end)
@@ -1350,6 +1525,10 @@ describe('vim.pack', function()
eq(true, pack_exists('basic'))
eq(true, pack_exists('plugindirs'))
+ local locked_plugins = vim.tbl_keys(get_lock_tbl().plugins)
+ table.sort(locked_plugins)
+ eq({ 'basic', 'plugindirs' }, locked_plugins)
+
watch_events({ 'PackChangedPre', 'PackChanged' })
n.exec('messages clear')
@@ -1371,6 +1550,23 @@ describe('vim.pack', function()
eq(3, find_in_log(log, 'PackChangedPre', 'delete', 'plugindirs', nil))
eq(4, find_in_log(log, 'PackChanged', 'delete', 'plugindirs', nil))
eq(4, #log)
+
+ -- Should update lockfile
+ eq({ plugins = {} }, get_lock_tbl())
+ end)
+
+ it('works without prior `add()`', function()
+ exec_lua(function()
+ vim.pack.add({ repos_src.basic })
+ end)
+ n.clear()
+
+ eq(true, pack_exists('basic'))
+ exec_lua(function()
+ vim.pack.del({ 'basic' })
+ end)
+ eq(false, pack_exists('basic'))
+ eq({ plugins = {} }, get_lock_tbl())
end)
it('validates input', function()