Files
arti-pack/lua/arti-pack/fs.lua
T
unkin-agent 460dc2c5c8
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
Install plugins from a branch
Many upstream plugins publish no tags at all, so a tag-only installer cannot
manage them.

- Add a branch spec field, mutually exclusive with tag and validated the same way
- Build archive/refs/heads/<branch> URLs for branch specs
- Refetch a branch only on :ArtiPackUpdate or a forced sync, never from cache
- Record the branch, archive digest and fetch time in the lockfile
- Report a branch plugin as tracked and fetched, never as up to date
2026-09-12 00:08:18 +10:00

109 lines
2.5 KiB
Lua

local uv = vim.uv or vim.loop
local M = {}
local ZIP_MAGIC = 'PK\3\4'
local GZIP_MAGIC = '\31\139'
function M.join(...)
return table.concat({ ... }, '/')
end
function M.mkdirp(path)
vim.fn.mkdir(path, 'p')
return path
end
function M.rm_rf(path)
if uv.fs_stat(path) then
vim.fn.delete(path, 'rf')
end
end
function M.exists(path)
return uv.fs_stat(path) ~= nil
end
function M.entries(dir)
local handle = uv.fs_scandir(dir)
if not handle then
return {}
end
local out = {}
while true do
local name, kind = uv.fs_scandir_next(handle)
if not name then
break
end
table.insert(out, { name = name, kind = kind })
end
return out
end
-- Tag archives wrap everything in a single "<repo>-<tag>" directory. Detect it
-- rather than reconstructing the name, since the leading "v" is dropped.
function M.archive_root(dir)
local entries = M.entries(dir)
if #entries == 1 and entries[1].kind == 'directory' then
return M.join(dir, entries[1].name)
end
return dir
end
function M.read_head(path, bytes)
local fd = io.open(path, 'rb')
if not fd then
return nil
end
local head = fd:read(bytes)
fd:close()
return head
end
-- Identifies what was actually fetched. A branch name says nothing about
-- content, so the digest is what makes a branch install reportable. Archive
-- bytes reach sha256() as a Blob, which it refuses, so they are hashed in their
-- base64 form: an identity for the content, not the file's sha256sum.
function M.digest(path)
local fd = io.open(path, 'rb')
if not fd then
return nil
end
local data = fd:read('*a')
fd:close()
if not data then
return nil
end
return vim.fn.sha256(vim.base64.encode(data))
end
function M.verify_archive(path, ext)
local stat = uv.fs_stat(path)
if not stat or stat.size == 0 then
return false, 'downloaded archive is empty'
end
local magic = ext == 'zip' and ZIP_MAGIC or GZIP_MAGIC
local head = M.read_head(path, #magic)
if head ~= magic then
return false, string.format('downloaded archive is not a %s', ext)
end
return true
end
-- Extraction is pure Lua: no unzip or tar on PATH, and entry paths are vetted
-- here rather than trusted to an external tool.
function M.extract(archive, dest, ext)
local ok, res, err = pcall(require('arti-pack.archive').extract, archive, dest, ext)
if not ok then
return nil, tostring(res)
end
return res, err
end
function M.rename(from, to)
return uv.fs_rename(from, to)
end
return M