Skip to content

Commit 87dab83

Browse files
echasnovskidundargoc
authored andcommitted
feat(pack): vim.pack.get() gets VCS info neovim#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.
1 parent b2df852 commit 87dab83

File tree

3 files changed

+168
-102
lines changed

3 files changed

+168
-102
lines changed

runtime/doc/pack.txt

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,8 @@ Switch plugin's version:
271271
run |vim.pack.update()|.
272272

273273
Freeze plugin from being updated:
274-
• Update 'init.lua' for plugin to have `version` set to current commit hash.
275-
You can get it by running `vim.pack.update({ 'plugin-name' })` and yanking
276-
the word describing current state (looks like `abc12345`).
274+
• Update 'init.lua' for plugin to have `version` set to current revision. Get
275+
it with `:=vim.pack.get({ 'plug-name' })[1].rev` (looks like `abc12345`).
277276
|:restart|.
278277

279278
Unfreeze plugin to start receiving updates:
@@ -355,16 +354,27 @@ del({names}) *vim.pack.del()*
355354
be managed by |vim.pack|, not necessarily already added to
356355
current session.
357356

358-
get() *vim.pack.get()*
359-
Get data about all plugins managed by |vim.pack|
357+
get({names}, {opts}) *vim.pack.get()*
358+
Gets |vim.pack| plugin info, optionally filtered by `names`.
359+
360+
Parameters: ~
361+
{names} (`string[]?`) List of plugin names. Default: all plugins
362+
managed by |vim.pack|.
363+
{opts} (`table?`) A table with the following fields:
364+
{info} (`boolean`) Whether to include extra plugin info.
365+
Default `true`.
360366

361367
Return: ~
362368
(`table[]`) A list of objects with the following fields:
363-
{spec} (`vim.pack.SpecResolved`) A |vim.pack.Spec| with defaults
364-
made explicit.
365-
{path} (`string`) Plugin's path on disk.
366369
{active} (`boolean`) Whether plugin was added via |vim.pack.add()|
367370
to current session.
371+
{branches}? (`string[]`) Available Git branches (first is default).
372+
Missing if `info=false`.
373+
{path} (`string`) Plugin's path on disk.
374+
{rev}? (`string`) Current Git revision. Missing if `info=false`.
375+
{spec} (`vim.pack.SpecResolved`) A |vim.pack.Spec| with resolved
376+
`name`.
377+
{tags}? (`string[]`) Available Git tags. Missing if `info=false`.
368378

369379
update({names}, {opts}) *vim.pack.update()*
370380
Update plugins

runtime/lua/vim/pack.lua

Lines changed: 78 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,8 @@
6868
---you run |vim.pack.update()|.
6969
---
7070
---Freeze plugin from being updated:
71-
---- Update 'init.lua' for plugin to have `version` set to current commit hash.
72-
---You can get it by running `vim.pack.update({ 'plugin-name' })` and yanking
73-
---the word describing current state (looks like `abc12345`).
71+
---- Update 'init.lua' for plugin to have `version` set to current revision.
72+
---Get it with `:=vim.pack.get({ 'plug-name' })[1].rev` (looks like `abc12345`).
7473
---- |:restart|.
7574
---
7675
---Unfreeze plugin to start receiving updates:
@@ -148,13 +147,13 @@ local function git_clone(url, path)
148147
end
149148

150149
--- @async
151-
--- @param rev string
150+
--- @param ref string
152151
--- @param cwd string
153152
--- @return string
154-
local function git_get_hash(rev, cwd)
155-
-- Using `rev-list -1` shows a commit of revision, while `rev-parse` shows
156-
-- hash of revision. Those are different for annotated tags.
157-
return git_cmd({ 'rev-list', '-1', '--abbrev-commit', rev }, cwd)
153+
local function git_get_hash(ref, cwd)
154+
-- Using `rev-list -1` shows a commit of reference, while `rev-parse` shows
155+
-- hash of reference. Those are different for annotated tags.
156+
return git_cmd({ 'rev-list', '-1', '--abbrev-commit', ref }, cwd)
158157
end
159158

160159
--- @async
@@ -169,11 +168,14 @@ end
169168
--- @param cwd string
170169
--- @return string[]
171170
local function git_get_branches(cwd)
171+
local def_branch = git_get_default_branch(cwd)
172172
local cmd = { 'branch', '--remote', '--list', '--format=%(refname:short)', '--', 'origin/**' }
173173
local stdout = git_cmd(cmd, cwd)
174174
local res = {} --- @type string[]
175175
for l in vim.gsplit(stdout, '\n') do
176-
res[#res + 1] = l:match('^origin/(.+)$')
176+
local branch = l:match('^origin/(.+)$')
177+
local pos = branch == def_branch and 1 or (#res + 1)
178+
table.insert(res, pos, branch)
177179
end
178180
return res
179181
end
@@ -182,8 +184,8 @@ end
182184
--- @param cwd string
183185
--- @return string[]
184186
local function git_get_tags(cwd)
185-
local cmd = { 'tag', '--list', '--sort=-v:refname' }
186-
return vim.split(git_cmd(cmd, cwd), '\n')
187+
local tags = git_cmd({ 'tag', '--list', '--sort=-v:refname' }, cwd)
188+
return tags == '' and {} or vim.split(tags, '\n')
187189
end
188190

189191
-- Plugin operations ----------------------------------------------------------
@@ -323,48 +325,30 @@ local function normalize_plugs(plugs)
323325
return res
324326
end
325327

326-
--- @param names string[]?
328+
--- @param names? string[]
327329
--- @return vim.pack.Plug[]
328330
local function plug_list_from_names(names)
329-
local all_plugins = M.get()
331+
local p_data_list = M.get(names, { info = false })
330332
local plug_dir = get_plug_dir()
331333
local plugs = {} --- @type vim.pack.Plug[]
332-
local used_names = {} --- @type table<string,boolean>
333-
-- Preserve plugin order; might be important during checkout or event trigger
334-
for _, p_data in ipairs(all_plugins) do
334+
for _, p_data in ipairs(p_data_list) do
335335
-- NOTE: By default include only active plugins (and not all on disk). Using
336336
-- not active plugins might lead to a confusion as default `version` and
337337
-- user's desired one might mismatch.
338-
-- TODO(echasnovski): Consider changing this if/when there is lockfile.
339-
--- @cast names string[]
340-
if (not names and p_data.active) or vim.tbl_contains(names or {}, p_data.spec.name) then
338+
-- TODO(echasnovski): Change this when there is lockfile.
339+
if names ~= nil or p_data.active then
341340
plugs[#plugs + 1] = new_plug(p_data.spec, plug_dir)
342-
used_names[p_data.spec.name] = true
343341
end
344342
end
345343

346-
if vim.islist(names) and #plugs ~= #names then
347-
--- @param n string
348-
local unused = vim.tbl_filter(function(n)
349-
return not used_names[n]
350-
end, names)
351-
error('The following plugins are not installed: ' .. table.concat(unused, ', '))
352-
end
353-
354344
return plugs
355345
end
356346

357347
--- @param p vim.pack.Plug
358348
--- @param event_name 'PackChangedPre'|'PackChanged'
359349
--- @param kind 'install'|'update'|'delete'
360350
local function trigger_event(p, event_name, kind)
361-
local spec = vim.deepcopy(p.spec)
362-
-- Infer default branch for fuller `event-data` (if possible)
363-
-- Doing it only on event trigger level allows keeping `spec` close to what
364-
-- user supplied without performance issues during startup.
365-
spec.version = spec.version or (uv.fs_stat(p.path) and git_get_default_branch(p.path))
366-
367-
local data = { kind = kind, spec = spec, path = p.path }
351+
local data = { kind = kind, spec = vim.deepcopy(p.spec), path = p.path }
368352
api.nvim_exec_autocmds(event_name, { pattern = p.path, data = data })
369353
end
370354

@@ -463,7 +447,7 @@ end
463447
--- @param p vim.pack.Plug
464448
local function resolve_version(p)
465449
local function list_in_line(name, list)
466-
return #list == 0 and '' or ('\n' .. name .. ': ' .. table.concat(list, ', '))
450+
return ('\n%s: %s'):format(name, table.concat(list, ', '))
467451
end
468452

469453
-- Resolve only once
@@ -987,25 +971,57 @@ end
987971

988972
--- @inlinedoc
989973
--- @class vim.pack.PlugData
990-
--- @field spec vim.pack.SpecResolved A |vim.pack.Spec| with defaults made explicit.
991-
--- @field path string Plugin's path on disk.
992974
--- @field active boolean Whether plugin was added via |vim.pack.add()| to current session.
975+
--- @field branches? string[] Available Git branches (first is default). Missing if `info=false`.
976+
--- @field path string Plugin's path on disk.
977+
--- @field rev? string Current Git revision. Missing if `info=false`.
978+
--- @field spec vim.pack.SpecResolved A |vim.pack.Spec| with resolved `name`.
979+
--- @field tags? string[] Available Git tags. Missing if `info=false`.
980+
981+
--- @class vim.pack.keyset.get
982+
--- @inlinedoc
983+
--- @field info boolean Whether to include extra plugin info. Default `true`.
993984

994-
--- Get data about all plugins managed by |vim.pack|
985+
--- @param p_data_list vim.pack.PlugData[]
986+
local function add_p_data_info(p_data_list)
987+
local funs = {} --- @type (async fun())[]
988+
for i, p_data in ipairs(p_data_list) do
989+
local path = p_data.path
990+
--- @async
991+
funs[i] = function()
992+
p_data.branches = git_get_branches(path)
993+
p_data.rev = git_get_hash('HEAD', path)
994+
p_data.tags = git_get_tags(path)
995+
end
996+
end
997+
--- @async
998+
local function joined_f()
999+
async.join(n_threads, funs)
1000+
end
1001+
async.run(joined_f):wait()
1002+
end
1003+
1004+
--- Gets |vim.pack| plugin info, optionally filtered by `names`.
1005+
--- @param names? string[] List of plugin names. Default: all plugins managed by |vim.pack|.
1006+
--- @param opts? vim.pack.keyset.get
9951007
--- @return vim.pack.PlugData[]
996-
function M.get()
1008+
function M.get(names, opts)
1009+
vim.validate('names', names, vim.islist, true, 'list')
1010+
opts = vim.tbl_extend('force', { info = true }, opts or {})
1011+
9971012
-- Process active plugins in order they were added. Take into account that
9981013
-- there might be "holes" after `vim.pack.del()`.
9991014
local active = {} --- @type table<integer,vim.pack.Plug?>
10001015
for _, p_active in pairs(active_plugins) do
10011016
active[p_active.id] = p_active.plug
10021017
end
10031018

1004-
--- @type vim.pack.PlugData[]
1005-
local res = {}
1019+
local res = {} --- @type vim.pack.PlugData[]
1020+
local used_names = {} --- @type table<string,boolean>
10061021
for i = 1, n_active_plugins do
1007-
if active[i] then
1022+
if active[i] and (not names or vim.tbl_contains(names, active[i].spec.name)) then
10081023
res[#res + 1] = { spec = vim.deepcopy(active[i].spec), path = active[i].path, active = true }
1024+
used_names[active[i].spec.name] = true
10091025
end
10101026
end
10111027

@@ -1015,20 +1031,33 @@ function M.get()
10151031
local plug_dir = get_plug_dir()
10161032
for n, t in vim.fs.dir(plug_dir, { depth = 1 }) do
10171033
local path = vim.fs.joinpath(plug_dir, n)
1018-
if t == 'directory' and not active_plugins[path] then
1034+
local is_in_names = not names or vim.tbl_contains(names, n)
1035+
if t == 'directory' and not active_plugins[path] and is_in_names then
10191036
local spec = { name = n, src = git_cmd({ 'remote', 'get-url', 'origin' }, path) }
10201037
res[#res + 1] = { spec = spec, path = path, active = false }
1038+
used_names[n] = true
10211039
end
10221040
end
1041+
end
1042+
async.run(do_get):wait()
10231043

1024-
-- Make default `version` explicit
1025-
for _, p_data in ipairs(res) do
1026-
if not p_data.spec.version then
1027-
p_data.spec.version = git_get_default_branch(p_data.path)
1044+
if names ~= nil then
1045+
-- Align result with input
1046+
local names_order = {} --- @type table<string,integer>
1047+
for i, n in ipairs(names) do
1048+
if not used_names[n] then
1049+
error(('Plugin `%s` is not installed'):format(tostring(n)))
10281050
end
1051+
names_order[n] = i
10291052
end
1053+
table.sort(res, function(a, b)
1054+
return names_order[a.spec.name] < names_order[b.spec.name]
1055+
end)
1056+
end
1057+
1058+
if opts.info then
1059+
add_p_data_info(res)
10301060
end
1031-
async.run(do_get):wait()
10321061

10331062
return res
10341063
end

0 commit comments

Comments
 (0)