health.lua (8502B)
1 local M = {} 2 3 local health = vim.health 4 5 local function get_lockfile_path() 6 return vim.fs.joinpath(vim.fn.stdpath('config'), 'nvim-pack-lock.json') 7 end 8 9 local function get_plug_dir() 10 return vim.fs.joinpath(vim.fn.stdpath('data'), 'site', 'pack', 'core', 'opt') 11 end 12 13 local function git_cmd(cmd, cwd) 14 cmd = vim.list_extend({ 'git', '-c', 'gc.auto=0' }, cmd) 15 local env = vim.fn.environ() --- @type table<string,string> 16 env.GIT_DIR, env.GIT_WORK_TREE = nil, nil 17 local sys_opts = { cwd = cwd, text = true, env = env, clear_env = true } 18 local out = vim.system(cmd, sys_opts):wait() --- @type vim.SystemCompleted 19 if out.code ~= 0 then 20 return false, ((out.stderr or ''):gsub('\n+$', '')) 21 end 22 return true, ((out.stdout or ''):gsub('\n+$', '')) 23 end 24 25 local function check_basics() 26 health.start('vim.pack: basics') 27 28 -- Requirements 29 if vim.fn.executable('git') == 0 then 30 health.warn('`git` executable is required. Install it using your package manager') 31 return false, false 32 end 33 34 -- Detect if not used 35 local lockfile_path = get_lockfile_path() 36 local has_lockfile = vim.fn.filereadable(lockfile_path) == 1 37 local plug_dir = get_plug_dir() 38 local has_plug_dir = vim.fn.isdirectory(plug_dir) == 1 39 if not has_lockfile and not has_plug_dir then 40 health.ok('`vim.pack` is not used') 41 return false, false 42 end 43 44 -- General info 45 local git = vim.fn.exepath('git') 46 local _, version = git_cmd({ 'version' }, vim.uv.cwd()) 47 health.info(('Git: %s (%s)'):format(version:gsub('^git%s*', ''), git)) 48 health.info('Lockfile: ' .. lockfile_path) 49 health.info('Plugin directory: ' .. plug_dir) 50 51 if has_lockfile and has_plug_dir then 52 health.ok('') 53 else 54 local lockfile_absent = has_lockfile and 'present' or 'absent' 55 local plug_dir_absent = has_plug_dir and 'present' or 'absent' 56 local msg = ('Lockfile is %s, plugin directory is %s.'):format(lockfile_absent, plug_dir_absent) 57 .. ' Restart Nvim and run `vim.pack.add({})` to ' 58 .. (has_lockfile and 'install plugins from the lockfile' or 'regenerate the lockfile') 59 health.warn(msg) 60 end 61 62 return has_lockfile, has_plug_dir 63 end 64 65 local function is_version(x) 66 return type(x) == 'string' or (type(x) == 'table' and pcall(x.has, x, '1')) 67 end 68 69 local function failed_git_cmd(plug_name, plug_path) 70 local msg = ('Failed Git command inside plugin %s.'):format(vim.inspect(plug_name)) 71 .. ' This is unexpected and should not happen.' 72 .. (' Manually delete directory %s and reinstall plugin'):format(plug_path) 73 health.error(msg) 74 return false 75 end 76 77 --- @return boolean Whether a check is successful 78 local function check_plugin_lock_data(plug_name, lock_data) 79 local name_str = vim.inspect(plug_name) 80 local error_with_del_advice = function(reason) 81 local msg = ('%s %s.'):format(name_str, reason) 82 .. (' Delete %s entry (do not create trailing comma) and '):format(name_str) 83 .. 'restart Nvim to regenerate lockfile data' 84 health.error(msg) 85 return false 86 end 87 88 -- Types 89 if type(plug_name) ~= 'string' then 90 return error_with_del_advice('is not a valid plugin name') 91 end 92 if type(lock_data) ~= 'table' then 93 return error_with_del_advice('entry is not a valid type') 94 end 95 if type(lock_data.rev) ~= 'string' then 96 local reason = '`rev` entry is ' .. (lock_data.rev and 'not a valid type' or 'missing') 97 return error_with_del_advice(reason) 98 end 99 if type(lock_data.src) ~= 'string' then 100 local reason = '`src` entry is ' .. (lock_data.src and 'not a valid type' or 'missing') 101 return error_with_del_advice(reason) 102 end 103 if lock_data.version and not is_version(lock_data.version) then 104 return error_with_del_advice('`version` entry is not a valid type') 105 end 106 107 -- Alignment with what is actually present on disk 108 local plug_path = vim.fs.joinpath(get_plug_dir(), plug_name) 109 if vim.fn.isdirectory(plug_path) ~= 1 then 110 health.warn( 111 ('Plugin %s is not installed but present in the lockfile.'):format(name_str) 112 .. ' Restart Nvim and run `vim.pack.add({})` to autoinstall.' 113 .. (' To fully delete, run `vim.pack.del({ %s }, { force = true })`'):format(name_str) 114 ) 115 return false 116 end 117 118 -- NOTE: `vim.pack` currently only supports Git repos as plugins 119 if not git_cmd({ 'rev-parse', '--git-dir' }, plug_path) then 120 return true 121 end 122 123 local has_head, head = git_cmd({ 'rev-list', '-1', 'HEAD' }, plug_path) 124 if not has_head then 125 return failed_git_cmd(plug_name, plug_path) 126 elseif lock_data.rev ~= head then 127 health.error( 128 ('Plugin %s is not at expected revision\n'):format(name_str) 129 .. ('Expected: %s\nActual: %s\n'):format(lock_data.rev, head) 130 .. 'To synchronize, restart Nvim and run ' 131 .. ('`vim.pack.update({ %s }, { offline = true })`'):format(name_str) 132 ) 133 return false 134 end 135 136 local has_origin, origin = git_cmd({ 'remote', 'get-url', 'origin' }, plug_path) 137 if not has_origin then 138 return failed_git_cmd(plug_name, plug_path) 139 elseif lock_data.src ~= origin then 140 health.error( 141 ('Plugin %s has not expected source\n'):format(name_str) 142 .. ('Expected: %s\nActual: %s\n'):format(lock_data.src, origin) 143 .. 'Delete `src` lockfile entry (do not create trailing comma) and ' 144 .. 'restart Nvim to regenerate lockfile data' 145 ) 146 return false 147 end 148 149 return true 150 end 151 152 local function check_lockfile() 153 health.start('vim.pack: lockfile') 154 155 local can_read, text = pcall(vim.fn.readblob, get_lockfile_path()) 156 if not can_read then 157 health.error('Could not read lockfile. Delete it and restart Nvim.') 158 return 159 end 160 161 local can_parse, data = pcall(vim.json.decode, text) 162 if not can_parse then 163 health.error(('Could not parse lockfile: %s\nDelete it and restart Nvim'):format(data)) 164 return 165 end 166 167 if type(data.plugins) ~= 'table' then 168 health.error('Field `plugins` is not proper type. Delete lockfile and restart Nvim') 169 return 170 end 171 172 local is_good = true 173 --- @cast data { plugins: table<string,table> } 174 for plug_name, lock_data in pairs(data.plugins) do 175 is_good = check_plugin_lock_data(plug_name, lock_data) and is_good 176 end 177 178 if is_good then 179 health.ok('') 180 end 181 end 182 183 --- @return boolean Whether a check is successful 184 local function check_installed_plugin(plug_name) 185 local name_str = vim.inspect(plug_name) 186 local plug_path = vim.fs.joinpath(get_plug_dir(), plug_name) 187 188 if vim.fn.isdirectory(plug_path) ~= 1 then 189 health.error(('%s is not a directory. Delete it'):format(plug_name)) 190 return false 191 end 192 193 if not git_cmd({ 'rev-parse', '--git-dir' }, plug_path) then 194 health.error( 195 ('%s is not a Git repository.'):format(name_str) 196 .. ' It was not installed by `vim.pack` and should not be present in the plugin directory.' 197 .. ' If installed manually, use dedicated `:h packages`' 198 ) 199 return false 200 end 201 202 -- Detached HEAD is a sign that plugin is managed by `vim.pack` 203 local has_head_ref, head_ref = git_cmd({ 'rev-parse', '--abbrev-ref', 'HEAD' }, plug_path) 204 if not has_head_ref then 205 return failed_git_cmd(plug_name, plug_path) 206 elseif head_ref ~= 'HEAD' then 207 health.warn( 208 ('Plugin %s is not at state which is a result of `vim.pack` operation.\n'):format(name_str) 209 .. 'If it was intentional, make sure you know what you are doing.\n' 210 .. 'Otherwise, restart Nvim and run ' 211 .. ('`vim.pack.update({ %s }, { offline = true })`.\n'):format(name_str) 212 .. 'If nothing is updated, plugin is at correct revision and will be managed as expected' 213 ) 214 return false 215 end 216 217 -- Usage data 218 local has_pack_info, info = pcall(vim.pack.get, { plug_name }) 219 if not has_pack_info then 220 health.error('Could not get `vim.pack` usage information for plugin ' .. name_str) 221 return false 222 end 223 224 if not info[1].active then 225 health.info( 226 ('Plugin %s is not active.'):format(name_str) 227 .. ' Is it lazy loaded or did you forget to run `vim.pack.del()`?' 228 ) 229 end 230 231 return true 232 end 233 234 local function check_plug_dir() 235 health.start('vim.pack: plugin directory') 236 237 local is_good = true 238 local plug_dir = get_plug_dir() 239 for plug_name, _ in vim.fs.dir(plug_dir) do 240 is_good = check_installed_plugin(plug_name) and is_good 241 end 242 243 if is_good then 244 health.ok('') 245 end 246 end 247 248 function M.check() 249 local has_lockfile, has_plug_dir = check_basics() 250 if has_lockfile then 251 check_lockfile() 252 end 253 if has_plug_dir then 254 check_plug_dir() 255 end 256 end 257 258 return M