feat(vim.pack): lockfile support #35827

This commit is contained in:
Justin M. Keyes
2025-10-04 12:48:29 -04:00
committed by GitHub
3 changed files with 366 additions and 68 deletions

View File

@@ -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 version 2.36. Target plugins should be Git repositories with versions as named
tags following semver convention `v<major>.<minor>.<patch>`. 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 ~ Example workflows ~
Basic install and management: Basic install and management:
@@ -254,17 +261,20 @@ Basic install and management:
plugin1 = require('plugin1') plugin1 = require('plugin1')
< <
• Restart Nvim (for example, with |:restart|). Plugins that were not yet • 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: • To update all plugins with new changes:
• Execute |vim.pack.update()|. This will download updates from source and • Execute |vim.pack.update()|. This will download updates from source and
show confirmation buffer in a separate tabpage. show confirmation buffer in a separate tabpage.
• Review changes. To confirm all updates execute |:write|. To discard • Review changes. To confirm all updates execute |:write|. To discard
updates execute |:quit|. updates execute |:quit|.
• (Optionally) |:restart| to start using code from updated plugins.
Switch plugin's version: Switch plugin's version:
• Update 'init.lua' for plugin to have desired `version`. Let's say, plugin • Update 'init.lua' for plugin to have desired `version`. Let's say, plugin
named 'plugin1' has changed to `vim.version.range('*')`. 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' })`. • Execute `vim.pack.update({ 'plugin1' })`.
• Review changes and either confirm or discard them. If discarded, revert any • 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 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: Freeze plugin from being updated:
• Update 'init.lua' for plugin to have `version` set to current revision. Get • 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|. • |:restart|.
Unfreeze plugin to start receiving updates: Unfreeze plugin to start receiving updates:
@@ -371,7 +381,7 @@ get({names}, {opts}) *vim.pack.get()*
• {branches}? (`string[]`) Available Git branches (first is default). • {branches}? (`string[]`) Available Git branches (first is default).
Missing if `info=false`. Missing if `info=false`.
• {path} (`string`) Plugin's path on disk. • {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 • {spec} (`vim.pack.SpecResolved`) A |vim.pack.Spec| with resolved
`name`. `name`.
• {tags}? (`string[]`) Available Git tags. Missing if `info=false`. • {tags}? (`string[]`) Available Git tags. Missing if `info=false`.
@@ -402,8 +412,7 @@ update({names}, {opts}) *vim.pack.update()*
Parameters: ~ Parameters: ~
• {names} (`string[]?`) List of plugin names to update. Must be managed • {names} (`string[]?`) List of plugin names to update. Must be managed
by |vim.pack|, not necessarily already added to current by |vim.pack|, not necessarily already added to current
session. Default: names of all plugins added to current session. Default: names of all plugins managed by |vim.pack|.
session via |vim.pack.add()|.
• {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`.

View File

@@ -14,6 +14,13 @@
---least version 2.36. Target plugins should be Git repositories with versions ---least version 2.36. Target plugins should be Git repositories with versions
---as named tags following semver convention `v<major>.<minor>.<patch>`. ---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 ~ ---Example workflows ~
--- ---
---Basic install and management: ---Basic install and management:
@@ -50,18 +57,21 @@
---``` ---```
--- ---
---- Restart Nvim (for example, with |:restart|). Plugins that were not yet ---- 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: ---- To update all plugins with new changes:
--- - Execute |vim.pack.update()|. This will download updates from source and --- - Execute |vim.pack.update()|. This will download updates from source and
--- show confirmation buffer in a separate tabpage. --- show confirmation buffer in a separate tabpage.
--- - Review changes. To confirm all updates execute |:write|. --- - Review changes. To confirm all updates execute |:write|.
--- To discard updates execute |:quit|. --- To discard updates execute |:quit|.
--- - (Optionally) |:restart| to start using code from updated plugins.
--- ---
---Switch plugin's version: ---Switch plugin's version:
---- Update 'init.lua' for plugin to have desired `version`. Let's say, plugin ---- Update 'init.lua' for plugin to have desired `version`. Let's say, plugin
---named 'plugin1' has changed to `vim.version.range('*')`. ---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' })`. ---- Execute `vim.pack.update({ 'plugin1' })`.
---- Review changes and either confirm or discard them. If discarded, revert ---- 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 ---any changes in 'init.lua' as well or you will be prompted again next time
@@ -69,7 +79,7 @@
--- ---
---Freeze plugin from being updated: ---Freeze plugin from being updated:
---- Update 'init.lua' for plugin to have `version` set to current revision. ---- 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|. ---- |:restart|.
--- ---
---Unfreeze plugin to start receiving updates: ---Unfreeze plugin to start receiving updates:
@@ -190,13 +200,72 @@ local function git_get_tags(cwd)
return tags == '' and {} or vim.split(tags, '\n') return tags == '' and {} or vim.split(tags, '\n')
end end
-- Plugin operations ---------------------------------------------------------- -- Lockfile -------------------------------------------------------------------
--- @return string --- @return string
local function get_plug_dir() local function get_plug_dir()
return vim.fs.joinpath(vim.fn.stdpath('data'), 'site', 'pack', 'core', 'opt') return vim.fs.joinpath(vim.fn.stdpath('data'), 'site', 'pack', 'core', 'opt')
end 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 msg string|string[]
--- @param level ('DEBUG'|'TRACE'|'INFO'|'WARN'|'ERROR')? --- @param level ('DEBUG'|'TRACE'|'INFO'|'WARN'|'ERROR')?
local function notify(msg, level) local function notify(msg, level)
@@ -334,15 +403,8 @@ local function plug_list_from_names(names)
local plug_dir = get_plug_dir() local plug_dir = get_plug_dir()
local plugs = {} --- @type vim.pack.Plug[] local plugs = {} --- @type vim.pack.Plug[]
for _, p_data in ipairs(p_data_list) do 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) plugs[#plugs + 1] = new_plug(p_data.spec, plug_dir)
end end
end
return plugs return plugs
end end
@@ -532,6 +594,8 @@ local function checkout(p, timestamp, skip_same_sha)
git_cmd({ 'checkout', '--quiet', p.info.sha_target }, p.path) 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') trigger_event(p, 'PackChanged', 'update')
-- (Re)Generate help tags according to the current help files. -- (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) git_clone(p.spec.src, p.path)
p.info.installed = true 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 -- 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. -- have new repo in expected detached HEAD state and generated help files.
checkout(p, timestamp, false) checkout(p, timestamp, false)
@@ -698,17 +767,34 @@ function M.add(specs, opts)
end end
plugs = normalize_plugs(plugs) plugs = normalize_plugs(plugs)
-- Install -- Pre-process
--- @param p vim.pack.Plug lock_read()
local plugs_to_install = vim.tbl_filter(function(p) local plugs_to_install = {} --- @type vim.pack.Plug[]
return not p.info.installed local needs_lock_write = false
end, plugs) 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 if #plugs_to_install > 0 then
git_ensure_exec() git_ensure_exec()
install_list(plugs_to_install, opts.confirm) install_list(plugs_to_install, opts.confirm)
end end
if needs_lock_write then
lock_write()
end
-- Register and load those actually on disk while collecting errors -- Register and load those actually on disk while collecting errors
-- Delay showing all errors to have "good" plugins added first -- Delay showing all errors to have "good" plugins added first
local errors = {} --- @type string[] local errors = {} --- @type string[]
@@ -887,7 +973,7 @@ end
--- ---
--- @param names? string[] List of plugin names to update. Must be managed --- @param names? string[] List of plugin names to update. Must be managed
--- by |vim.pack|, not necessarily already added to current session. --- 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 --- @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')
@@ -899,6 +985,7 @@ function M.update(names, opts)
return return
end end
git_ensure_exec() git_ensure_exec()
lock_read()
-- Perform update -- Perform update
local timestamp = get_timestamp() local timestamp = get_timestamp()
@@ -925,6 +1012,7 @@ function M.update(names, opts)
run_list(plug_list, do_update, progress_title) run_list(plug_list, do_update, progress_title)
if opts.force then if opts.force then
lock_write()
feedback_log(plug_list) feedback_log(plug_list)
return return
end end
@@ -950,6 +1038,7 @@ function M.update(names, opts)
end end
run_list(plugs_to_checkout, do_checkout, 'Applying updates') run_list(plugs_to_checkout, do_checkout, 'Applying updates')
lock_write()
feedback_log(plugs_to_checkout) feedback_log(plugs_to_checkout)
end) end)
end end
@@ -967,6 +1056,8 @@ function M.del(names)
return return
end end
lock_read()
for _, p in ipairs(plug_list) do for _, p in ipairs(plug_list) do
trigger_event(p, 'PackChangedPre', 'delete') trigger_event(p, 'PackChangedPre', 'delete')
@@ -974,8 +1065,12 @@ function M.del(names)
active_plugins[p.path] = nil active_plugins[p.path] = nil
notify(("Removed plugin '%s'"):format(p.spec.name), 'INFO') notify(("Removed plugin '%s'"):format(p.spec.name), 'INFO')
plugin_lock.plugins[p.spec.name] = nil
trigger_event(p, 'PackChanged', 'delete') trigger_event(p, 'PackChanged', 'delete')
end end
lock_write()
end end
--- @inlinedoc --- @inlinedoc
@@ -983,7 +1078,7 @@ end
--- @field active boolean Whether plugin was added via |vim.pack.add()| to current session. --- @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 branches? string[] Available Git branches (first is default). Missing if `info=false`.
--- @field path string Plugin's path on disk. --- @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 spec vim.pack.SpecResolved A |vim.pack.Spec| with resolved `name`.
--- @field tags? string[] Available Git tags. Missing if `info=false`. --- @field tags? string[] Available Git tags. Missing if `info=false`.
@@ -999,7 +1094,6 @@ local function add_p_data_info(p_data_list)
--- @async --- @async
funs[i] = function() funs[i] = function()
p_data.branches = git_get_branches(path) p_data.branches = git_get_branches(path)
p_data.rev = git_get_hash('HEAD', path)
p_data.tags = git_get_tags(path) p_data.tags = git_get_tags(path)
end end
end end
@@ -1025,30 +1119,29 @@ function M.get(names, opts)
active[p_active.id] = p_active.plug active[p_active.id] = p_active.plug
end end
lock_read()
local res = {} --- @type vim.pack.PlugData[] local res = {} --- @type vim.pack.PlugData[]
local used_names = {} --- @type table<string,boolean> local used_names = {} --- @type table<string,boolean>
for i = 1, n_active_plugins do for i = 1, n_active_plugins do
if active[i] and (not names or vim.tbl_contains(names, active[i].spec.name)) then 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 } local name = active[i].spec.name
used_names[active[i].spec.name] = true 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
end end
--- @async
local function do_get()
-- Process not active plugins
local plug_dir = get_plug_dir() local plug_dir = get_plug_dir()
for n, t in vim.fs.dir(plug_dir, { depth = 1 }) do for name, l_data in vim.spairs(plugin_lock.plugins) do
local path = vim.fs.joinpath(plug_dir, n) local path = vim.fs.joinpath(plug_dir, name)
local is_in_names = not names or vim.tbl_contains(names, n) local is_in_names = not names or vim.tbl_contains(names, name)
if t == 'directory' and not active_plugins[path] and is_in_names then if not active_plugins[path] and is_in_names then
local spec = { name = n, src = git_cmd({ 'remote', 'get-url', 'origin' }, path) } local spec = { name = name, src = l_data.src, version = l_data.version }
res[#res + 1] = { spec = spec, path = path, active = false } res[#res + 1] = { spec = spec, path = path, rev = l_data.rev, active = false }
used_names[n] = true used_names[name] = true
end end
end end
end
async.run(do_get):wait()
if names ~= nil then if names ~= nil then
-- Align result with input -- Align result with input

View File

@@ -135,6 +135,12 @@ end
function repos_setup.plugindirs() function repos_setup.plugindirs()
init_test_repo('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', 'lua/plugindirs.lua', 'return "plugindirs main"')
repo_write_file('plugindirs', 'plugin/dirs.lua', 'vim.g._plugin = true') 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"') 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') return exec_lua('return package.loaded.jit ~= nil')
end 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 ====================================================================== -- Tests ======================================================================
describe('vim.pack', function() describe('vim.pack', function()
@@ -326,6 +340,9 @@ describe('vim.pack', function()
after_each(function() after_each(function()
vim.fs.rm(pack_get_dir(), { force = true, recursive = true }) 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) end)
teardown(function() teardown(function()
@@ -413,6 +430,117 @@ describe('vim.pack', function()
eq('plugindirs main', exec_lua('return require("plugindirs")')) eq('plugindirs main', exec_lua('return require("plugindirs")'))
end) 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() it('installs at proper version', function()
local out = exec_lua(function() local out = exec_lua(function()
vim.pack.add({ vim.pack.add({
@@ -597,9 +725,8 @@ describe('vim.pack', function()
eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }')) eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }'))
-- Plugins should still be marked as "active", since they were added -- Plugins should still be marked as "active", since they were added
plugindirs_data.active = true eq(true, exec_lua('return vim.pack.get({ "plugindirs" })[1].active'))
basic_data.active = true eq(true, exec_lua('return vim.pack.get({ "basic" })[1].active'))
eq({ plugindirs_data, basic_data }, exec_lua('return vim.pack.get(nil, { info = false })'))
end end
-- Works on initial install -- Works on initial install
@@ -759,7 +886,7 @@ describe('vim.pack', function()
-- Install initial versions of tested plugins -- Install initial versions of tested plugins
exec_lua(function() exec_lua(function()
vim.pack.add({ vim.pack.add({
repos_src.fetch, { src = repos_src.fetch, version = 'main' },
{ src = repos_src.semver, version = 'v0.3.0' }, { src = repos_src.semver, version = 'v0.3.0' },
repos_src.defbranch, repos_src.defbranch,
}) })
@@ -777,6 +904,11 @@ describe('vim.pack', function()
repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new 2"') repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new 2"')
git_add_commit('Commit to be added 2', 'fetch') 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) end)
after_each(function() after_each(function()
@@ -854,8 +986,8 @@ describe('vim.pack', function()
local screen local screen
screen = Screen.new(85, 35) screen = Screen.new(85, 35)
hashes.fetch_new = git_get_hash('HEAD', 'fetch') hashes.fetch_new = git_get_hash('main', 'fetch')
hashes.fetch_new_prev = git_get_hash('HEAD~', 'fetch') hashes.fetch_new_prev = git_get_hash('main~', 'fetch')
hashes.semver_head = git_get_hash('v0.3.0', 'semver') hashes.semver_head = git_get_hash('v0.3.0', 'semver')
local tab_name = 'n' .. (t.is_os('win') and ':' or '') .. '//2/confirm-update' 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') 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) matches('Available newer versions:\n• v1%.0%.0\n• v0%.4\n• 0%.3%.1$', confirm_text)
end) 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) end)
it('works with not active plugins', function() it('works with not active plugins', function()
exec_lua(function()
-- No plugins are added, but they are installed in `before_each()` -- No plugins are added, but they are installed in `before_each()`
vim.pack.update({ 'fetch' }) exec_lua(function()
-- By default should also include not active plugins
vim.pack.update()
end) end)
eq({ 'return "fetch main"' }, fn.readfile(fetch_lua_file)) eq({ 'return "fetch main"' }, fn.readfile(fetch_lua_file))
n.exec('write') n.exec('write')
@@ -1116,8 +1263,8 @@ describe('vim.pack', function()
eq('', api.nvim_get_option_value('filetype', {})) eq('', api.nvim_get_option_value('filetype', {}))
-- Write to log file -- Write to log file
hashes.fetch_new = git_get_hash('HEAD', 'fetch') hashes.fetch_new = git_get_hash('main', 'fetch')
hashes.fetch_new_prev = git_get_hash('HEAD~', 'fetch') hashes.fetch_new_prev = git_get_hash('main~', 'fetch')
local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log') local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log')
local log_lines = fn.readfile(log_path) 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)) eq(ref_log_lines, vim.list_slice(log_lines, 2))
-- Should update lockfile
eq(hashes.fetch_new, get_lock_tbl().plugins.fetch.rev)
end) end)
it('shows progress report', function() it('shows progress report', function()
track_nvim_echo() track_nvim_echo()
exec_lua(function() exec_lua(function()
vim.pack.add({ repos_src.fetch, repos_src.defbranch }) vim.pack.add({ repos_src.fetch, repos_src.defbranch })
-- Should also include updates from not active plugins
vim.pack.update() vim.pack.update()
end) end)
-- During initial download -- During initial download
validate_progress_report('Downloading updates', { 'fetch', 'defbranch' }) validate_progress_report('Downloading updates', { 'fetch', 'defbranch', 'semver' })
exec_lua('_G.echo_log = {}') exec_lua('_G.echo_log = {}')
-- During application (only for plugins that have updates) -- 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.add({ repos_src.fetch, repos_src.defbranch })
vim.pack.update(nil, { force = true }) vim.pack.update(nil, { force = true })
end) end)
validate_progress_report('Updating', { 'fetch', 'defbranch' }) validate_progress_report('Updating', { 'fetch', 'defbranch', 'semver' })
end) end)
it('triggers relevant events', function() it('triggers relevant events', function()
@@ -1246,10 +1397,10 @@ describe('vim.pack', function()
local make_basic_data = function(active, info) local make_basic_data = function(active, info)
local spec = { name = 'basic', src = repos_src.basic, version = 'feat-branch' } local spec = { name = 'basic', src = repos_src.basic, version = 'feat-branch' }
local path = pack_get_plug_path('basic') 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 if info then
res.branches = { 'main', 'feat-branch' } res.branches = { 'main', 'feat-branch' }
res.rev = git_get_hash('feat-branch', 'basic')
res.tags = { 'some-tag' } res.tags = { 'some-tag' }
end end
return res return res
@@ -1258,50 +1409,74 @@ describe('vim.pack', function()
local make_defbranch_data = function(active, info) local make_defbranch_data = function(active, info)
local spec = { name = 'defbranch', src = repos_src.defbranch } local spec = { name = 'defbranch', src = repos_src.defbranch }
local path = pack_get_plug_path('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 if info then
res.branches = { 'dev', 'main' } res.branches = { 'dev', 'main' }
res.rev = git_get_hash('dev', 'defbranch')
res.tags = {} res.tags = {}
end end
return res return res
end 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() it('returns list with necessary data', function()
local basic_data, defbranch_data local basic_data, defbranch_data, plugindirs_data
-- Should work just after installation -- Should work just after installation
exec_lua(function() 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) end)
defbranch_data = make_defbranch_data(true, true) defbranch_data = make_defbranch_data(true, true)
basic_data = make_basic_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 -- 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 -- Should also list non-active plugins
n.clear() n.clear()
exec_lua(function() exec_lua(function()
vim.pack.add({ { src = repos_src.basic, version = 'feat-branch' } }) vim.pack.add({ repos_src.defbranch })
end) end)
defbranch_data = make_defbranch_data(false, true) defbranch_data = make_defbranch_data(true, true)
basic_data = make_basic_data(true, true) basic_data = make_basic_data(false, true)
-- Should first list active, then non-active plugindirs_data = make_plugindirs_data(false, true)
eq({ basic_data, defbranch_data }, exec_lua('return vim.pack.get()')) -- 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 -- Should respect `names` for both active and not active plugins
eq({ basic_data }, exec_lua('return vim.pack.get({ "basic" })')) eq({ basic_data }, exec_lua('return vim.pack.get({ "basic" })'))
eq({ defbranch_data }, exec_lua('return vim.pack.get({ "defbranch" })')) 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" })' local bad_get_cmd = 'return vim.pack.get({ "ccc", "basic", "aaa" })'
matches('Plugin `ccc` is not installed', pcall_err(exec_lua, bad_get_cmd)) matches('Plugin `ccc` is not installed', pcall_err(exec_lua, bad_get_cmd))
-- Should respect `opts.info` -- Should respect `opts.info`
defbranch_data = make_defbranch_data(false, false) defbranch_data = make_defbranch_data(true, false)
basic_data = make_basic_data(true, false) basic_data = make_basic_data(false, false)
eq({ basic_data, defbranch_data }, exec_lua('return vim.pack.get(nil, { info = 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({ basic_data }, exec_lua('return vim.pack.get({ "basic" }, { info = false })'))
eq({ defbranch_data }, exec_lua('return vim.pack.get({ "defbranch" }, { info = false })')) eq({ defbranch_data }, exec_lua('return vim.pack.get({ "defbranch" }, { info = false })'))
end) end)
@@ -1350,6 +1525,10 @@ describe('vim.pack', function()
eq(true, pack_exists('basic')) eq(true, pack_exists('basic'))
eq(true, pack_exists('plugindirs')) 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' }) watch_events({ 'PackChangedPre', 'PackChanged' })
n.exec('messages clear') n.exec('messages clear')
@@ -1371,6 +1550,23 @@ describe('vim.pack', function()
eq(3, find_in_log(log, 'PackChangedPre', 'delete', 'plugindirs', nil)) eq(3, find_in_log(log, 'PackChangedPre', 'delete', 'plugindirs', nil))
eq(4, find_in_log(log, 'PackChanged', 'delete', 'plugindirs', nil)) eq(4, find_in_log(log, 'PackChanged', 'delete', 'plugindirs', nil))
eq(4, #log) 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) end)
it('validates input', function() it('validates input', function()