aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/pack.lua
diff options
context:
space:
mode:
authorEvgeni Chasnovski <evgeni.chasnovski@gmail.com>2025-09-07 19:59:31 +0300
committerGitHub <noreply@github.com>2025-09-07 09:59:31 -0700
commit7853cde29a08aa43bcaecc9848a8b43248bf8d66 (patch)
tree20d86fbcb99f5a3dd8cfcf38de790a8c631c1581 /runtime/lua/vim/pack.lua
parentfd59e72b477002cef76c2cb8f59226c862ca9fb7 (diff)
downloadrneovim-7853cde29a08aa43bcaecc9848a8b43248bf8d66.tar.gz
rneovim-7853cde29a08aa43bcaecc9848a8b43248bf8d66.tar.bz2
rneovim-7853cde29a08aa43bcaecc9848a8b43248bf8d66.zip
feat(pack): vim.pack.get() gets VCS info #35631
Problem: Force resolve `spec.version` overrides the information about whether a user supplied `version` or not. Knowing it might be useful in some use cases (like comparing to previously set `spec` to detect if it has changed). Solution: Do not resolve `spec.version`. This also improves speed when triggering events and calling `get()`. - Place default branch first when listing all branches. - Use correct terminology in `get_hash` helper. - Do not return `{ '' }` if there are no tags. Problem: There is no way to get more information about installed plugins, like current revision or default branch (necessary if resolving default `spec.version` manually). As computing Git data migth take some time, also allow `get()` to limit output to only necessary set of plugins. Solution: - introduce arguments to `get(names, opts)`, which follows other `vim.pack` functions. Plugin extra info is returned by default and should be opt-out via `opts.info = false`. - Examples: - Get current revision: `get({ 'plug-name' })[1].rev` - Get default branch: `get({ 'plug_name' })[1].branches[1]` - `update()` and `del()` act on plugins in the same order their names are supplied. This is less surprising. - default `opts.info` to `true` since this simplifies logic for the common user, while still leaving the door open for a faster `get()` if needed.
Diffstat (limited to 'runtime/lua/vim/pack.lua')
-rw-r--r--runtime/lua/vim/pack.lua127
1 files changed, 78 insertions, 49 deletions
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<string,boolean>
- -- 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`.
+
+--- @class vim.pack.keyset.get
+--- @inlinedoc
+--- @field info boolean Whether to include extra plugin info. Default `true`.
---- Get data about all plugins managed by |vim.pack|
+--- @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<integer,vim.pack.Plug?>
@@ -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<string,boolean>
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,20 +1031,33 @@ 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 }
+ used_names[n] = true
end
end
+ end
+ async.run(do_get):wait()
- -- 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)
+ if names ~= nil then
+ -- Align result with input
+ local names_order = {} --- @type table<string,integer>
+ 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
- async.run(do_get):wait()
return res
end