Fix many bugs and make things actually work

This commit is contained in:
phosphene47
2021-11-14 18:04:59 +11:00
parent a8e711124f
commit 78f440dcdd
3 changed files with 497 additions and 208 deletions

View File

@ -3,21 +3,44 @@ local proc = require("dep/proc")
logger:open() logger:open()
local base_dir local initialized, config_path, base_dir
local packages, package_roots local packages, package_roots
local function register(arg) local function get_name(id)
if type(arg) ~= "table" then local name = id:match("^[%w-_.]+/([%w-_.]+)$")
arg = { arg } if name then
return name
else
error(string.format('invalid name "%s"; must be in the format "user/package"', id))
end
end
local function link_dependency(parent, child)
if not parent.dependents[child.id] then
parent.dependents[child.id] = child
parent.dependents[#parent.dependents + 1] = child
end end
local id = arg[1] if not child.dependencies[parent.id] then
child.dependencies[parent.id], child.root = parent, false
child.dependencies[#child.dependencies + 1] = parent
end
end
local function register(spec, overrides)
overrides = overrides or {}
if type(spec) ~= "table" then
spec = { spec }
end
local id = spec[1]
local package = packages[id] local package = packages[id]
if not package then if not package then
package = { package = {
id = id, id = id,
enabled = false, enabled = true,
exists = false, exists = false,
added = false, added = false,
configured = false, configured = false,
@ -31,51 +54,96 @@ local function register(arg)
} }
packages[id] = package packages[id] = package
packages[#packages + 1] = package
end end
local prev_dir = package.dir -- optimization local prev_dir = package.dir -- optimization
-- meta -- meta
package.name = arg.as or package.name or id:match("^[%w-_.]+/([%w-_.]+)$") package.name = spec.as or package.name or get_name(id)
package.url = arg.url or package.url or ("https://github.com/" .. id .. ".git") package.url = spec.url or package.url or ("https://github.com/" .. id .. ".git")
package.branch = arg.branch or package.branch package.branch = spec.branch or package.branch
package.dir = base_dir .. package.name package.dir = base_dir .. package.name
package.pin = arg.pin or package.pin package.pin = overrides.pin or spec.pin or package.pin
package.enabled = not arg.disabled and package.enabled package.enabled = not overrides.disable and not spec.disable and package.enabled
if prev_dir ~= package.dir then if prev_dir ~= package.dir then
package.exists = vim.fn.isdirectory(package.dir) ~= 0 package.exists = vim.fn.isdirectory(package.dir) ~= 0
package.configured = package.exists package.configured = package.exists
end end
table.insert(package.on_setup, arg.setup) package.on_setup[#package.on_setup + 1] = spec.setup
table.insert(package.on_config, arg.config) package.on_config[#package.on_config + 1] = spec.config
table.insert(package.on_load, arg[2]) package.on_load[#package.on_load + 1] = spec[2]
for _, req in ipairs(type(arg.requires) == "table" and arg.requires or { arg.requires }) do if type(spec.requires) == "table" then
local parent, child = register(req), package for i = 1, #spec.requires do
parent.dependents[child.id] = child link_dependency(register(spec.requires[i]), package)
child.dependencies[parent.id], child.root = parent, false end
elseif spec.requires then
link_dependency(register(spec.requires), package)
end end
for _, dep in ipairs(type(arg.deps) == "table" and arg.deps or { arg.deps }) do if type(spec.deps) == "table" then
local parent, child = package, register(dep) for i = 1, #spec.deps do
parent.dependents[child.id] = child link_dependency(package, register(spec.deps[i]))
child.dependencies[parent.id], child.root = parent, false end
elseif spec.deps then
link_dependency(package, register(spec.deps))
end
return package
end
local function register_recursive(list, overrides)
overrides = overrides or {}
overrides = {
pin = overrides.pin or list.pin,
disable = overrides.disable or list.disable,
}
for i = 1, #list do
local ok, err = pcall(register, list[i], overrides)
if not ok then
error(string.format("%s (spec=%s)", err, vim.inspect(list[i])))
end
end
if list.modules then
for i = 1, #list.modules do
local name, module = "<unnamed module>", list.modules[i]
if type(module) == "string" then
name, module = module, require(module)
end
name = module.name or name
local ok, err = pcall(register_recursive, module, overrides)
if not ok then
error(string.format("%s <- %s", err, name))
end
end
end end
end end
local function register_recursive(list) local function sort_dependencies()
for _, arg in ipairs(list) do local function compare(a, b)
register(arg) local a_deps = #a.dependencies
local b_deps = #b.dependencies
if a_deps == b_deps then
return a.id < b.id
else
return a_deps < b_deps
end
end end
for _, module in ipairs(list.modules or {}) do table.sort(packages, compare)
if type(module) == "string" then
module = require(module)
end
register_recursive(module) for i = 1, #packages do
table.sort(packages[i].dependencies, compare)
table.sort(packages[i].dependents, compare)
end end
end end
@ -83,23 +151,25 @@ local function find_cycle()
local index = 0 local index = 0
local indexes = {} local indexes = {}
local lowlink = {} local lowlink = {}
local set = {}
local stack = {} local stack = {}
-- use tarjan algorithm to find circular dependencies (strongly connected components)
local function connect(package) local function connect(package)
indexes[package.id], lowlink[package.id], set[package.id] = index, index, true indexes[package.id], lowlink[package.id] = index, index
stack[#stack + 1], stack[package.id] = package, true
index = index + 1 index = index + 1
table.insert(stack, package)
for _, dependent in pairs(package.dependents) do for i = 1, #package.dependents do
if indexes[dependent.id] == nil then local dependent = package.dependents[i]
if not indexes[dependent.id] then
local cycle = connect(dependent) local cycle = connect(dependent)
if cycle then if cycle then
return cycle return cycle
else else
lowlink[package.id] = math.min(lowlink[package.id], lowlink[dependent.id]) lowlink[package.id] = math.min(lowlink[package.id], lowlink[dependent.id])
end end
elseif set[dependent.id] then elseif stack[dependent.id] then
lowlink[package.id] = math.min(lowlink[package.id], indexes[dependent.id]) lowlink[package.id] = math.min(lowlink[package.id], indexes[dependent.id])
end end
end end
@ -109,20 +179,23 @@ local function find_cycle()
local node local node
repeat repeat
node = table.remove(stack) node = stack[#stack]
set[node.id] = nil stack[#stack], stack[node.id] = nil, nil
table.insert(cycle, node) cycle[#cycle + 1] = node
until node == package until node == package
-- only consider multi-node components -- a node is by definition strongly connected to itself
if #cycle > 2 then -- ignore single-node components unless it explicitly specified itself as a dependency
if #cycle > 2 or package.dependents[package.id] then
return cycle return cycle
end end
end end
end end
for _, package in pairs(packages) do for i = 1, #packages do
if indexes[package.id] == nil then local package = packages[i]
if not indexes[package.id] then
local cycle = connect(package) local cycle = connect(package)
if cycle then if cycle then
return cycle return cycle
@ -131,22 +204,41 @@ local function find_cycle()
end end
end end
local function ensure_acyclic()
local cycle = find_cycle()
if cycle then
local names = {}
for i = 1, #cycle do
names[i] = cycle[i].id
end
error("circular dependency detected in package graph: " .. table.concat(names, " -> "))
end
end
local function find_roots() local function find_roots()
for _, package in pairs(packages) do for i = 1, #packages do
local package = packages[i]
if package.root then if package.root then
table.insert(package_roots, package) package_roots[#package_roots + 1] = package
end end
end end
end end
local function run_hooks(package, type) local function run_hooks(package, type)
for _, cb in ipairs(package["on_" .. type]) do local hooks = package[type]
local ok, err = pcall(cb)
for i = 1, #hooks do
local ok, err = pcall(hooks[i])
if not ok then if not ok then
return false, err return false, err
end end
end end
if #hooks ~= 0 then
logger:log("hook", string.format("ran %d %s for %s", #hooks, #hooks == 1 and "hook" or "hooks", package.id))
end
return true return true
end end
@ -155,6 +247,7 @@ local function ensure_added(package)
local ok, err = pcall(vim.cmd, "packadd " .. package.name) local ok, err = pcall(vim.cmd, "packadd " .. package.name)
if ok then if ok then
package.added = true package.added = true
logger:log("vim", string.format("packadd completed for %s", package.id))
else else
return false, err return false, err
end end
@ -163,13 +256,21 @@ local function ensure_added(package)
return true return true
end end
local function configure_recursive(package, force) local function configure_recursive(package)
if not package.exists or not package.enabled then if not package.exists or not package.enabled then
return return
end end
if not package.configured or force then for i = 1, #package.dependencies do
local ok, err = run_hooks(package, "setup") if not package.dependencies[i].configured then
return
end
end
local propagate = false
if not package.configured then
local ok, err = run_hooks(package, "on_setup")
if not ok then if not ok then
logger:log("error", string.format("failed to set up %s; reason: %s", package.id, err)) logger:log("error", string.format("failed to set up %s; reason: %s", package.id, err))
return return
@ -181,96 +282,128 @@ local function configure_recursive(package, force)
return return
end end
ok, err = run_hooks(package, "config") ok, err = run_hooks(package, "on_config")
if not ok then if not ok then
logger:log("error", string.format("failed to configure %s; reason: %s", package.id, err)) logger:log("error", string.format("failed to configure %s; reason: %s", package.id, err))
return return
end end
package.configured, package.loaded = true, false package.configured, package.loaded = true, false
force = true propagate = true
logger:log("config", string.format("configured %s", package.id))
end end
for _, dependent in pairs(package.dependents) do for i = 1, #package.dependents do
configure_recursive(dependent, force) local dependent = package.dependents[i]
dependent.configured = dependent.configured and not propagate
configure_recursive(dependent)
end end
end end
local function load_recursive(package, force) local function load_recursive(package)
if not package.exists or not package.enabled then if not package.exists or not package.enabled then
return return
end end
if not package.loaded or force then for i = 1, #package.dependencies do
if not package.dependencies[i].loaded then
return
end
end
local propagate = false
if not package.loaded then
local ok, err = ensure_added(package) local ok, err = ensure_added(package)
if not ok then if not ok then
logger:log("error", string.format("failed to configure %s; reason: %s", package.id, err)) logger:log("error", string.format("failed to configure %s; reason: %s", package.id, err))
return return
end end
ok, err = run_hooks(package, "load") ok, err = run_hooks(package, "on_load")
if not ok then if not ok then
logger:log("error", string.format("failed to load %s; reason: %s", package.id, err)) logger:log("error", string.format("failed to load %s; reason: %s", package.id, err))
return return
end end
package.loaded = true package.loaded = true
force = true propagate = true
logger:log("load", string.format("loaded %s", package.id))
end end
for _, dependent in pairs(package.dependents) do for i = 1, #package.dependents do
local dependent = package.dependents[i]
dependent.loaded = dependent.loaded and not propagate
load_recursive(dependent, force) load_recursive(dependent, force)
end end
end end
local function reload_meta() local function reload_meta()
vim.cmd([[ local ok, err = pcall(
silent! helptags ALL vim.cmd,
silent! UpdateRemotePlugins [[
]]) silent! helptags ALL
silent! UpdateRemotePlugins
]]
)
if ok then
logger:log("vim", "reloaded helptags and remote plugins")
else
logger:log("error", string.format("failed to reload helptags and remote plugins; reason: %s", err))
end
end end
local function reload_all() local function reload_all()
for _, package in pairs(package_roots) do for i = 1, #package_roots do
configure_recursive(package) configure_recursive(package_roots[i])
end end
for _, package in pairs(package_roots) do for i = 1, #package_roots do
load_recursive(package) load_recursive(package_roots[i])
end end
reload_meta() reload_meta()
end end
local function clean() local function clean()
vim.loop.fs_scandir(base_dir, function(err, handle) vim.loop.fs_scandir(
if err then base_dir,
logger:log("error", string.format("failed to clean; reason: %s", err)) vim.schedule_wrap(function(err, handle)
else if err then
local queue = {} logger:log("error", string.format("failed to clean; reason: %s", err))
else
local queue = {}
while handle do while handle do
local name = vim.loop.fs_scandir_next(handle) local name = vim.loop.fs_scandir_next(handle)
if name then if name then
queue[name] = base_dir .. name queue[name] = base_dir .. name
else else
break break
end
end
for i = 1, #packages do
queue[packages[i].name] = nil
end
for name, dir in pairs(queue) do
-- todo: make this async
local ok = vim.fn.delete(dir, "rf")
if ok then
logger:log("clean", string.format("deleted %s", name))
else
logger:log("error", string.format("failed to delete %s", name))
end
end end
end end
end)
for _, package in pairs(packages) do )
queue[package.name] = nil
end
for name, dir in pairs(queue) do
-- todo: make this async
local ok = vim.fn.delete(dir, "rf")
if not ok then
logger:log("error", string.format("failed to delete %s", name))
end
end
end
end)
end end
local function sync(package, cb) local function sync(package, cb)
@ -283,34 +416,37 @@ local function sync(package, cb)
return return
end end
local function cb_err(err) local function log_err(err)
logger:log("error", string.format("failed to update %s; reason: %s", package.id, err)) logger:log("error", string.format("failed to update %s; reason: %s", package.id, err))
cb(err)
end end
proc.git_current_commit(package.dir, function(err, before) proc.git_rev_parse(package.dir, "HEAD", function(err, before)
if err then if err then
cb_err(before) log_err(before)
cb(err)
else else
proc.git_fetch(package.dir, package.branch or "HEAD", function(err, message) proc.git_fetch(package.dir, "origin", package.branch or "HEAD", function(err, message)
if err then if err then
cb_err(message) log_err(message)
cb(err)
else else
proc.git_reset(package.dir, package.branch or "HEAD", function(err, message) proc.git_rev_parse(package.dir, "FETCH_HEAD", function(err, after)
if err then if err then
cb_err(message) log_err(after)
cb(err)
elseif before == after then
logger:log("skip", string.format("skipped %s", package.id))
cb(err)
else else
proc.get_current_commit(package.dir, function(err, after) proc.git_reset(package.dir, after, function(err, message)
if err then if err then
cb_err(after) log_err(message)
else else
if before == after then package.added, package.configured = false, false
logger:log("skip", string.format("skipped %s", package.id)) logger:log("update", string.format("updated %s; %s -> %s", package.id, before, after))
else
package.added, package.configured = false, false
logger:log("update", string.format("updated %s", package.id))
end
end end
cb(err)
end) end)
end end
end) end)
@ -326,22 +462,146 @@ local function sync(package, cb)
package.exists, package.added, package.configured = true, false, false package.exists, package.added, package.configured = true, false, false
logger:log("install", string.format("installed %s", package.id)) logger:log("install", string.format("installed %s", package.id))
end end
cb(err)
end) end)
end end
end end
local function sync_list(list) local function sync_list(list)
local progress = 0 local progress = 0
local has_errors = false
for _, package in ipairs(list) do local function done(err)
sync(package, function(err) progress = progress + 1
progress = progress + 1 has_errors = has_errors or err
if progress == #list then
clean() if progress == #list then
reload_all() clean()
reload_all()
if has_errors then
logger:log("error", "there were errors during sync; see :messages or :DepLog for more information")
end end
end) end
end end
for i = 1, #list do
sync(list[i], done)
end
end
local function print_list(list)
local window = vim.api.nvim_get_current_win()
local buffer = vim.api.nvim_create_buf(true, true)
local line = 0
local indent = 0
local function print(chunks)
local concat = {}
local column = 0
for i = 1, indent do
concat[#concat + 1] = " "
column = column + 2
end
if not chunks then
chunks = {}
elseif type(chunks) == "string" then
chunks = { { chunks } }
end
for i = 1, #chunks do
local chunk = chunks[i]
concat[#concat + 1] = chunk[1]
chunk.offset, column = column, column + #chunk[1]
end
vim.api.nvim_buf_set_lines(buffer, line, -1, false, { table.concat(concat) })
for i = 1, #chunks do
local chunk = chunks[i]
if chunk[2] then
vim.api.nvim_buf_add_highlight(buffer, -1, chunk[2], line, chunk.offset, chunk.offset + #chunk[1])
end
end
line = line + 1
end
print("Installed packages:")
indent = 1
local loaded = {}
local function dry_load(package)
for i = 1, #package.dependencies do
if not loaded[package.dependencies[i].id] then
return
end
end
loaded[package.id] = true
local line = {
{ "- ", "Comment" },
{ package.id, "Underlined" },
}
if not package.exists then
line[#line + 1] = { " *not installed", "Comment" }
end
if not package.loaded then
line[#line + 1] = { " *not loaded", "Comment" }
end
if not package.enabled then
line[#line + 1] = { " *disabled", "Comment" }
end
if package.pin then
line[#line + 1] = { " *pinned", "Comment" }
end
print(line)
for i = 1, #package.dependents do
dry_load(package.dependents[i])
end
end
for i = 1, #package_roots do
dry_load(package_roots[i])
end
indent = 0
print()
print("Dependency graph:")
local function walk_graph(package)
indent = indent + 1
print({
{ "| ", "Comment" },
{ package.id, "Underlined" },
})
for i = 1, #package.dependents do
walk_graph(package.dependents[i])
end
indent = indent - 1
end
for i = 1, #package_roots do
walk_graph(package_roots[i])
end
vim.api.nvim_buf_set_option(buffer, "bufhidden", "wipe")
vim.api.nvim_buf_set_option(buffer, "modifiable", false)
vim.api.nvim_win_set_buf(window, buffer)
end end
vim.cmd([[ vim.cmd([[
@ -349,58 +609,77 @@ vim.cmd([[
command! DepList lua require("dep").list() command! DepList lua require("dep").list()
command! DepClean lua require("dep").clean() command! DepClean lua require("dep").clean()
command! DepLog lua require("dep").open_log() command! DepLog lua require("dep").open_log()
command! DepConfig lua require("dep").open_config()
]]) ]])
local function wrap_api(name, fn)
return function(...)
if initialized then
local ok, err = pcall(fn, ...)
if not ok then
logger:log("error", err)
end
else
logger:log("error", string.format("cannot call %s; dep is not initialized", name))
end
end
end
--todo: prevent multiple execution of async routines --todo: prevent multiple execution of async routines
return setmetatable({ return setmetatable({
sync = function() sync = wrap_api("dep.sync", function()
local targets = {} sync_list(packages)
end),
for _, package in pairs(packages) do list = wrap_api("dep.list", function()
table.insert(targets, package) print_list(packages)
end end),
sync_list(targets) clean = wrap_api("dep.clean", clean),
end,
open_log = function() open_log = wrap_api("dep.open_log", function()
vim.cmd("sp " .. logger.path) vim.cmd("sp " .. logger.path)
end, end),
open_config = wrap_api("dep.open_config", function()
vim.cmd("sp " .. config_path)
end),
}, { }, {
__call = function(config) __call = function(self, config)
base_dir = config.base_dir or (vim.fn.stdpath("data") .. "/site/pack/deps/start/") config_path = debug.getinfo(2, "S").source:sub(2)
packages, package_roots = {}, {} initialized, err = pcall(function()
base_dir = config.base_dir or (vim.fn.stdpath("data") .. "/site/pack/deps/start/")
packages, package_roots = {}, {}
register_recursive({ "chiyadev/dep", modules = { config } }) register("chiyadev/dep")
register_recursive(config)
sort_dependencies()
ensure_acyclic()
find_roots()
reload_all()
local cycle = find_cycle() local should_sync = function(package)
if cycle then if config.sync == "new" or config.sync == nil then
local names = {} return not package.exists
for _, package in ipairs(cycle) do else
table.insert(names, package.id) return config.sync == "always"
end
end end
error("circular dependency detected in package graph: " .. table.concat(names, " -> "))
end
find_roots() local targets = {}
reload_all()
local should_sync = function(package) for i = 1, #packages do
if config.sync == "new" or config.sync == nil then local package = packages[i]
return not package.exists if should_sync(package) then
else targets[#targets + 1] = package
return config.sync == "always" end
end end
sync_list(targets)
end)
if not initialized then
logger:log("error", err)
end end
local targets = {}
for _, package in pairs(packages) do
if should_sync(package) then
table.insert(targets, package)
end
end
sync_list(targets)
end, end,
}) })

View File

@ -4,6 +4,8 @@ local logger = {
} }
local colors = { local colors = {
skip = "Constant",
clean = "Boolean",
install = "MoreMsg", install = "MoreMsg",
update = "WarningMsg", update = "WarningMsg",
delete = "Directory", delete = "Directory",
@ -32,28 +34,26 @@ function logger:close()
end end
end end
function logger:log(op, message, cb) function logger:log(op, message)
if not self.silent and colors[op] then local source = debug.getinfo(2, "Sl").short_src
vim.api.nvim_echo({
{ "[dep]", "Identifier" },
{ " " },
{ message, colors[op] },
}, false, {})
end
if self.pipe then vim.schedule(function()
local source = debug.getinfo(2, "Sl").short_src if type(message) ~= "string" then
local message = string.format("[%s] %s: %s\n", os.date(), source, message) message = vim.inspect(message)
end
self.pipe:write( if not self.silent and colors[op] then
message, vim.api.nvim_echo({
vim.schedule_wrap(function(err) { "[dep]", "Identifier" },
if cb then { " " },
cb(err) { message, colors[op] },
end }, true, {})
end) end
)
end if self.pipe then
self.pipe:write(string.format("[%s] %s: %s\n", os.date(), source, message))
end
end)
end end
return logger return logger

View File

@ -2,73 +2,83 @@ local logger = require("dep/log")
local proc = {} local proc = {}
function proc.exec(process, args, cwd, env, cb) function proc.exec(process, args, cwd, env, cb)
local out = vim.loop.new_pipe() local handle, pid, buffer = nil, nil, {}
local buffer = {} local stdout = vim.loop.new_pipe()
local stderr = vim.loop.new_pipe()
local handle = vim.loop.spawn( handle, pid = vim.loop.spawn(
process, process,
{ args = args, cwd = cwd, env = env, stdio = { nil, out, out } }, { args = args, cwd = cwd, env = env, stdio = { nil, stdout, stderr } },
vim.schedule_wrap(function(code) vim.schedule_wrap(function(code)
handle:close() handle:close()
local output = table.concat(buffer) local output = table.concat(buffer)
if output:sub(-1) == "\n" then
output = output:sub(1, -2)
end
logger:log( logger:log(
process, process,
string.format('executed `%s` with args: "%s"\n%s', process, table.concat(args, '", "'), output) string.format(
'executed `%s` (code=%s, pid=%s) with args: "%s"\n%s',
process,
code,
pid,
table.concat(args, '", "'),
output
)
) )
cb(code, output) cb(code ~= 0, output)
end) end)
) )
vim.loop.read_start( vim.loop.read_start(stdout, function(_, data)
out, if data then
vim.schedule_wrap(function(_, data) buffer[#buffer + 1] = data
if data then else
table.insert(buffer, data) stdout:close()
else end
out:close() end)
end
end) vim.loop.read_start(stderr, function(_, data)
) if data then
buffer[#buffer + 1] = data
else
stderr:close()
end
end)
end end
local git_env = { "GIT_TERMINAL_PROMPT=0" } local git_env = { "GIT_TERMINAL_PROMPT=0" }
function proc.git_current_commit(dir, cb) function proc.git_rev_parse(dir, arg, cb)
exec("git", { "rev-parse", "HEAD" }, dir, git_env, cb) local args = { "rev-parse", "--short", arg }
proc.exec("git", args, dir, git_env, cb)
end end
function proc.git_clone(dir, url, branch, cb) function proc.git_clone(dir, url, branch, cb)
local args = { "--depth=1", "--recurse-submodules", "--shallow-submodules", url, dir } local args = { "clone", "--depth=1", "--recurse-submodules", "--shallow-submodules", url, dir }
if branch then if branch then
table.insert(args, "--branch=" .. branch) args[#args + 1] = "--branch=" .. branch
end end
exec("git", args, nil, git_env, cb) proc.exec("git", args, nil, git_env, cb)
end end
function proc.git_fetch(dir, branch, cb) function proc.git_fetch(dir, remote, refspec, cb)
local args = { "--depth=1", "--recurse-submodules" } local args = { "fetch", "--depth=1", "--recurse-submodules", remote, refspec }
if branch then proc.exec("git", args, dir, git_env, cb)
table.insert(args, "origin")
table.insert(args, branch)
end
exec("git", args, dir, git_env, cb)
end end
function proc.git_reset(dir, branch, cb) function proc.git_reset(dir, treeish, cb)
local args = { "--hard", "--recurse-submodules" } local args = { "reset", "--hard", "--recurse-submodules", treeish, "--" }
if branch then proc.exec("git", args, dir, git_env, cb)
table.insert("origin/" .. branch)
end
exec("git", args, dir, git_env, cb)
end end
return proc return proc