diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt index 285ad08bfc..e80d3639ff 100644 --- a/runtime/doc/pack.txt +++ b/runtime/doc/pack.txt @@ -271,9 +271,8 @@ Switch plugin's version: run |vim.pack.update()|. Freeze plugin from being updated: -• Update 'init.lua' for plugin to have `version` set to current commit hash. - You can get it by running `vim.pack.update({ 'plugin-name' })` and yanking - the word describing current state (looks like `abc12345`). +• 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`). • |:restart|. Unfreeze plugin to start receiving updates: @@ -355,16 +354,27 @@ del({names}) *vim.pack.del()* be managed by |vim.pack|, not necessarily already added to current session. -get() *vim.pack.get()* - Get data about all plugins managed by |vim.pack| +get({names}, {opts}) *vim.pack.get()* + Gets |vim.pack| plugin info, optionally filtered by `names`. + + Parameters: ~ + • {names} (`string[]?`) List of plugin names. Default: all plugins + managed by |vim.pack|. + • {opts} (`table?`) A table with the following fields: + • {info} (`boolean`) Whether to include extra plugin info. + Default `true`. Return: ~ (`table[]`) A list of objects with the following fields: - • {spec} (`vim.pack.SpecResolved`) A |vim.pack.Spec| with defaults - made explicit. - • {path} (`string`) Plugin's path on disk. • {active} (`boolean`) Whether plugin was added via |vim.pack.add()| to current session. + • {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`. + • {spec} (`vim.pack.SpecResolved`) A |vim.pack.Spec| with resolved + `name`. + • {tags}? (`string[]`) Available Git tags. Missing if `info=false`. update({names}, {opts}) *vim.pack.update()* Update plugins diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index ae999e95e7..de3a64c325 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -68,9 +68,8 @@ ---you run |vim.pack.update()|. --- ---Freeze plugin from being updated: ----- Update 'init.lua' for plugin to have `version` set to current commit hash. ----You can get it by running `vim.pack.update({ 'plugin-name' })` and yanking ----the word describing current state (looks like `abc12345`). +---- 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`). ---- |:restart|. --- ---Unfreeze plugin to start receiving updates: @@ -148,13 +147,13 @@ local function git_clone(url, path) end --- @async ---- @param rev string +--- @param ref string --- @param cwd string --- @return string -local function git_get_hash(rev, cwd) - -- Using `rev-list -1` shows a commit of revision, while `rev-parse` shows - -- hash of revision. Those are different for annotated tags. - return git_cmd({ 'rev-list', '-1', '--abbrev-commit', rev }, cwd) +local function git_get_hash(ref, cwd) + -- Using `rev-list -1` shows a commit of reference, while `rev-parse` shows + -- hash of reference. Those are different for annotated tags. + return git_cmd({ 'rev-list', '-1', '--abbrev-commit', ref }, cwd) end --- @async @@ -169,11 +168,14 @@ end --- @param cwd string --- @return string[] local function git_get_branches(cwd) + local def_branch = git_get_default_branch(cwd) local cmd = { 'branch', '--remote', '--list', '--format=%(refname:short)', '--', 'origin/**' } local stdout = git_cmd(cmd, cwd) local res = {} --- @type string[] for l in vim.gsplit(stdout, '\n') do - res[#res + 1] = l:match('^origin/(.+)$') + local branch = l:match('^origin/(.+)$') + local pos = branch == def_branch and 1 or (#res + 1) + table.insert(res, pos, branch) end return res end @@ -182,8 +184,8 @@ end --- @param cwd string --- @return string[] local function git_get_tags(cwd) - local cmd = { 'tag', '--list', '--sort=-v:refname' } - return vim.split(git_cmd(cmd, cwd), '\n') + local tags = git_cmd({ 'tag', '--list', '--sort=-v:refname' }, cwd) + return tags == '' and {} or vim.split(tags, '\n') end -- Plugin operations ---------------------------------------------------------- @@ -323,34 +325,22 @@ local function normalize_plugs(plugs) return res end ---- @param names string[]? +--- @param names? string[] --- @return vim.pack.Plug[] local function plug_list_from_names(names) - local all_plugins = M.get() + local p_data_list = M.get(names, { info = false }) local plug_dir = get_plug_dir() local plugs = {} --- @type vim.pack.Plug[] - local used_names = {} --- @type table - -- Preserve plugin order; might be important during checkout or event trigger - for _, p_data in ipairs(all_plugins) 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): Consider changing this if/when there is lockfile. - --- @cast names string[] - if (not names and p_data.active) or vim.tbl_contains(names or {}, p_data.spec.name) then + -- 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) - used_names[p_data.spec.name] = true end end - if vim.islist(names) and #plugs ~= #names then - --- @param n string - local unused = vim.tbl_filter(function(n) - return not used_names[n] - end, names) - error('The following plugins are not installed: ' .. table.concat(unused, ', ')) - end - return plugs end @@ -358,13 +348,7 @@ end --- @param event_name 'PackChangedPre'|'PackChanged' --- @param kind 'install'|'update'|'delete' local function trigger_event(p, event_name, kind) - local spec = vim.deepcopy(p.spec) - -- Infer default branch for fuller `event-data` (if possible) - -- Doing it only on event trigger level allows keeping `spec` close to what - -- user supplied without performance issues during startup. - spec.version = spec.version or (uv.fs_stat(p.path) and git_get_default_branch(p.path)) - - local data = { kind = kind, spec = spec, path = p.path } + local data = { kind = kind, spec = vim.deepcopy(p.spec), path = p.path } api.nvim_exec_autocmds(event_name, { pattern = p.path, data = data }) end @@ -463,7 +447,7 @@ end --- @param p vim.pack.Plug local function resolve_version(p) local function list_in_line(name, list) - return #list == 0 and '' or ('\n' .. name .. ': ' .. table.concat(list, ', ')) + return ('\n%s: %s'):format(name, table.concat(list, ', ')) end -- Resolve only once @@ -987,13 +971,44 @@ end --- @inlinedoc --- @class vim.pack.PlugData ---- @field spec vim.pack.SpecResolved A |vim.pack.Spec| with defaults made explicit. ---- @field path string Plugin's path on disk. --- @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 spec vim.pack.SpecResolved A |vim.pack.Spec| with resolved `name`. +--- @field tags? string[] Available Git tags. Missing if `info=false`. ---- Get data about all plugins managed by |vim.pack| +--- @class vim.pack.keyset.get +--- @inlinedoc +--- @field info boolean Whether to include extra plugin info. Default `true`. + +--- @param p_data_list vim.pack.PlugData[] +local function add_p_data_info(p_data_list) + local funs = {} --- @type (async fun())[] + for i, p_data in ipairs(p_data_list) do + local path = p_data.path + --- @async + funs[i] = function() + p_data.branches = git_get_branches(path) + p_data.rev = git_get_hash('HEAD', path) + p_data.tags = git_get_tags(path) + end + end + --- @async + local function joined_f() + async.join(n_threads, funs) + end + async.run(joined_f):wait() +end + +--- Gets |vim.pack| plugin info, optionally filtered by `names`. +--- @param names? string[] List of plugin names. Default: all plugins managed by |vim.pack|. +--- @param opts? vim.pack.keyset.get --- @return vim.pack.PlugData[] -function M.get() +function M.get(names, opts) + vim.validate('names', names, vim.islist, true, 'list') + opts = vim.tbl_extend('force', { info = true }, opts or {}) + -- Process active plugins in order they were added. Take into account that -- there might be "holes" after `vim.pack.del()`. local active = {} --- @type table @@ -1001,11 +1016,12 @@ function M.get() active[p_active.id] = p_active.plug end - --- @type vim.pack.PlugData[] - local res = {} + local res = {} --- @type vim.pack.PlugData[] + local used_names = {} --- @type table for i = 1, n_active_plugins do - if active[i] 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 } + used_names[active[i].spec.name] = true end end @@ -1015,21 +1031,34 @@ function M.get() 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) - if t == 'directory' and not active_plugins[path] then + 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 } - end - end - - -- Make default `version` explicit - for _, p_data in ipairs(res) do - if not p_data.spec.version then - p_data.spec.version = git_get_default_branch(p_data.path) + used_names[n] = true end end end async.run(do_get):wait() + if names ~= nil then + -- Align result with input + local names_order = {} --- @type table + for i, n in ipairs(names) do + if not used_names[n] then + error(('Plugin `%s` is not installed'):format(tostring(n))) + end + names_order[n] = i + end + table.sort(res, function(a, b) + return names_order[a.spec.name] < names_order[b.spec.name] + end) + end + + if opts.info then + add_p_data_info(res) + end + return res end diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua index ca285a5706..a8664b80b9 100644 --- a/test/functional/plugin/pack_spec.lua +++ b/test/functional/plugin/pack_spec.lua @@ -465,7 +465,7 @@ describe('vim.pack', function() watch_events({ 'PackChangedPre', 'PackChanged' }) exec_lua(function() - -- Should provide event-data respecting manual and inferred default `version` + -- Should provide event-data respecting manual `version` without inferring default vim.pack.add({ { src = repos_src.basic, version = 'feat-branch' }, repos_src.defbranch }) end) @@ -473,11 +473,11 @@ describe('vim.pack', function() local installpre_basic = find_in_log(log, 'PackChangedPre', 'install', 'basic', 'feat-branch') local installpre_defbranch = find_in_log(log, 'PackChangedPre', 'install', 'defbranch', nil) local updatepre_basic = find_in_log(log, 'PackChangedPre', 'update', 'basic', 'feat-branch') - local updatepre_defbranch = find_in_log(log, 'PackChangedPre', 'update', 'defbranch', 'dev') + local updatepre_defbranch = find_in_log(log, 'PackChangedPre', 'update', 'defbranch', nil) local update_basic = find_in_log(log, 'PackChanged', 'update', 'basic', 'feat-branch') - local update_defbranch = find_in_log(log, 'PackChanged', 'update', 'defbranch', 'dev') + local update_defbranch = find_in_log(log, 'PackChanged', 'update', 'defbranch', nil) local install_basic = find_in_log(log, 'PackChanged', 'install', 'basic', 'feat-branch') - local install_defbranch = find_in_log(log, 'PackChanged', 'install', 'defbranch', 'dev') + local install_defbranch = find_in_log(log, 'PackChanged', 'install', 'defbranch', nil) eq(8, #log) -- NOTE: There is no guaranteed installation order among separate plugins (as it is async) @@ -571,11 +571,9 @@ 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.spec.version = 'main' plugindirs_data.active = true - basic_data.spec.version = 'main' basic_data.active = true - eq({ plugindirs_data, basic_data }, n.exec_lua('return vim.pack.get()')) + eq({ plugindirs_data, basic_data }, exec_lua('return vim.pack.get(nil, { info = false })')) end -- Works on initial install @@ -623,7 +621,8 @@ describe('vim.pack', function() '`basic`:\n', -- Should report available branches and tags if revision is absent '`wrong%-version`', - 'Available:\nTags: some%-tag\nBranches: feat%-branch, main', + -- Should list default branch first + 'Available:\nTags: some%-tag\nBranches: main, feat%-branch', -- Should report available branches and versions if no constraint match '`semver`', 'Available:\nVersions: v1%.0%.0, v0%.4, 0%.3%.1, v0%.3%.0.*\nBranches: main\n', @@ -1143,8 +1142,8 @@ describe('vim.pack', function() -- Should trigger relevant events only for actually updated plugins n.exec('write') local log = exec_lua('return _G.event_log') - eq(1, find_in_log(log, 'PackChangedPre', 'update', 'fetch', 'main')) - eq(2, find_in_log(log, 'PackChanged', 'update', 'fetch', 'main')) + eq(1, find_in_log(log, 'PackChangedPre', 'update', 'fetch', nil)) + eq(2, find_in_log(log, 'PackChanged', 'update', 'fetch', nil)) eq(2, #log) end) @@ -1180,7 +1179,7 @@ describe('vim.pack', function() vim.pack.add({ repos_src.basic }) end) - validate('The following plugins are not installed: aaa, ccc', { 'aaa', 'basic', 'ccc' }) + validate('Plugin `ccc` is not installed', { 'ccc', 'basic', 'aaa' }) -- Empty list is allowed with warning n.exec('messages clear') @@ -1192,39 +1191,73 @@ describe('vim.pack', function() end) describe('get()', function() - local basic_spec = { name = 'basic', src = repos_src.basic, version = 'main' } - local basic_path = pack_get_plug_path('basic') - local defbranch_spec = { name = 'defbranch', src = repos_src.defbranch, version = 'dev' } - local defbranch_path = pack_get_plug_path('defbranch') + 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 } + if info then + res.branches = { 'main', 'feat-branch' } + res.rev = git_get_hash('feat-branch', 'basic') + res.tags = { 'some-tag' } + end + return res + end + + 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 } + if info then + res.branches = { 'dev', 'main' } + res.rev = git_get_hash('dev', 'defbranch') + res.tags = {} + end + return res + end + + it('returns list with necessary data', function() + local basic_data, defbranch_data - it('returns list of available plugins', function() -- Should work just after installation exec_lua(function() - vim.pack.add({ repos_src.defbranch, repos_src.basic }) + vim.pack.add({ repos_src.defbranch, { src = repos_src.basic, version = 'feat-branch' } }) end) - eq({ - -- Should preserve order in which plugins were `vim.pack.add()`ed - { active = true, path = defbranch_path, spec = defbranch_spec }, - { active = true, path = basic_path, spec = basic_spec }, - }, exec_lua('return vim.pack.get()')) + defbranch_data = make_defbranch_data(true, true) + basic_data = make_basic_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()')) -- Should also list non-active plugins n.clear() exec_lua(function() - vim.pack.add({ repos_src.basic }) + vim.pack.add({ { src = repos_src.basic, version = 'feat-branch' } }) end) - eq({ - -- Should first list active, then non-active - { active = true, path = basic_path, spec = basic_spec }, - { active = false, path = defbranch_path, spec = defbranch_spec }, - }, exec_lua('return vim.pack.get()')) + 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()')) + + -- 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" })')) + + 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 })')) + 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) it('respects `data` field', function() local out = exec_lua(function() vim.pack.add({ - { src = repos_src.basic, data = { test = 'value' } }, + { src = repos_src.basic, version = 'feat-branch', data = { test = 'value' } }, { src = repos_src.defbranch, data = 'value' }, }) local plugs = vim.pack.get() @@ -1236,7 +1269,7 @@ describe('vim.pack', function() it('works with `del()`', function() exec_lua(function() - vim.pack.add({ repos_src.defbranch, repos_src.basic }) + vim.pack.add({ repos_src.defbranch, { src = repos_src.basic, version = 'feat-branch' } }) end) exec_lua(function() @@ -1251,15 +1284,9 @@ describe('vim.pack', function() -- Should not include removed plugins immediately after they are removed, -- while still returning list without holes exec_lua('vim.pack.del({ "defbranch" })') - eq({ - { - { active = true, path = defbranch_path, spec = defbranch_spec }, - { active = true, path = basic_path, spec = basic_spec }, - }, - { - { active = true, path = basic_path, spec = basic_spec }, - }, - }, exec_lua('return _G.get_log')) + local defbranch_data = make_defbranch_data(true, true) + local basic_data = make_basic_data(true, true) + eq({ { defbranch_data, basic_data }, { basic_data } }, exec_lua('return _G.get_log')) end) end) @@ -1281,16 +1308,16 @@ describe('vim.pack', function() eq(false, pack_exists('plugindirs')) eq( - "vim.pack: Removed plugin 'plugindirs'\nvim.pack: Removed plugin 'basic'", + "vim.pack: Removed plugin 'basic'\nvim.pack: Removed plugin 'plugindirs'", n.exec_capture('messages') ) -- Should trigger relevant events in order as specified in `vim.pack.add()` local log = exec_lua('return _G.event_log') - eq(1, find_in_log(log, 'PackChangedPre', 'delete', 'plugindirs', 'main')) - eq(2, find_in_log(log, 'PackChanged', 'delete', 'plugindirs', 'main')) - eq(3, find_in_log(log, 'PackChangedPre', 'delete', 'basic', 'feat-branch')) - eq(4, find_in_log(log, 'PackChanged', 'delete', 'basic', 'feat-branch')) + eq(1, find_in_log(log, 'PackChangedPre', 'delete', 'basic', 'feat-branch')) + eq(2, find_in_log(log, 'PackChanged', 'delete', 'basic', 'feat-branch')) + eq(3, find_in_log(log, 'PackChangedPre', 'delete', 'plugindirs', nil)) + eq(4, find_in_log(log, 'PackChanged', 'delete', 'plugindirs', nil)) eq(4, #log) end) @@ -1310,7 +1337,7 @@ describe('vim.pack', function() vim.pack.add({ repos_src.basic }) end) - validate('The following plugins are not installed: aaa, ccc', { 'aaa', 'basic', 'ccc' }) + validate('Plugin `ccc` is not installed', { 'ccc', 'basic', 'aaa' }) eq(true, pack_exists('basic')) -- Empty list is allowed with warning