diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt index e80d3639ff..557f1bc762 100644 --- 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..`. +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 index 40f1786ac5..6790350bc3 100644 --- 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..`. --- +---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 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 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 index fc7415c602..4ea9a0eca7 100644 --- 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()