aboutsummaryrefslogtreecommitdiff
path: root/lua/paq.lua
blob: d69c4cde3c21d29942fdc8271d6dfb7c3eb12dd9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
local vim = vim.api.nvim_call_function("has", {"nvim-0.5"}) and vim or require("paq.compat")
local uv = vim.loop
local print_err = vim.api.nvim_err_writeln

local cfg = {
    paqdir = vim.fn.stdpath("data") .. "/site/pack/paqs/",
    verbose = true,
}
local LOGFILE = vim.fn.stdpath("cache") .. "/paq.log"
local packages = {} -- 'name' = {options} pairs
local num_pkgs = 0
local last_ops = {} -- 'name' = 'op' pairs
local counters = {}
local messages = {
    install = {
        ok = "installed %s",
        err = "failed to install %s",
    },
    update = {
        ok = "updated %s",
        err = "failed to update %s",
        nop = "(up-to-date) %s",
    },
    remove = {
        ok = "removed %s",
        err = "failed to remove %s",
    },
    hook = {
        ok = "ran hook for %s",
        err = "failed to run hook for %s",
    }
}

local function Counter(op) counters[op] = {ok=0, err=0, nop=0} end

local function update_count(op, result, _, total)
    local c, t = counters[op]
    if not c then return end
    c[result] = c[result] + 1
    t = c[result]
    if c.ok + c.err + c.nop == total then
        Counter(op)
        vim.cmd("packloadall! | silent! helptags ALL")
    end
    return t
end

local function report(op, result, name, total)
    local total = total or num_pkgs
    local cur = update_count(op, result, nil, total)
    local count = cur and string.format("%d/%d", cur, total) or ""
    local msg = messages[op][result]
    local p = result == "err" and print_err or print
    p(string.format("Paq [%s] " .. msg, count, name))
end

local function call_proc(process, args, cwd, cb)
    local log, stderr, handle
    log = uv.fs_open(LOGFILE, "a+", 0x1A4)
    stderr = uv.new_pipe(false)
    stderr:open(log)
    handle = uv.spawn(
        process,
        {args=args, cwd=cwd, stdio={nil, nil, stderr}, env={"GIT_TERMINAL_PROMPT=0"}},
        vim.schedule_wrap(function(code)
            uv.fs_close(log)
            stderr:close()
            handle:close()
            cb(code == 0)
        end)
    )
end

local function run_hook(pkg)
    local t = type(pkg.run)
    if t == "function" then
        vim.cmd("packadd " .. pkg.name)
        local ok = pcall(pkg.run)
        report("hook", ok and "ok" or "err", pkg.name)
    elseif t == "string" then
        local args = {}
        for word in pkg.run:gmatch("%S+") do
            table.insert(args, word)
        end
        local post_hook = function(ok) report("hook", ok and "ok" or "err", pkg.name) end
        call_proc(table.remove(args, 1), args, pkg.dir, post_hook)
    end
end

local function install(pkg)
    if pkg.exists then return update_count("install", "nop", nil, num_pkgs) end
    local args = {"clone", pkg.url, "--depth=1", "--recurse-submodules", "--shallow-submodules"}
    if pkg.branch then vim.list_extend(args, {"-b", pkg.branch}) end
    vim.list_extend(args, {pkg.dir})
    local post_install = function(ok)
        if ok then
            pkg.exists = true
            last_ops[pkg.name] = "install"
            if pkg.run then run_hook(pkg) end
        end
        report("install", ok and "ok" or "err", pkg.name)
    end
    call_proc("git", args, nil, post_install)
end

local function get_git_hash(dir)
    local first_line = function(path)
        local file = io.open(path)
        if file then
            local line = file:read()
            file:close()
            return line
        end
    end
    local head_ref = first_line(dir .. "/.git/HEAD")
    return head_ref and first_line(dir .. "/.git/" .. head_ref:gsub("ref: ", ""))
end

local function update(pkg)
    if not pkg.exists or pkg.pin then return update_count("update", "nop", nil, num_pkgs) end
    local hash = get_git_hash(pkg.dir)
    local post_update = function(ok)
        if not ok then
            return report("update", "err", pkg.name)
        elseif get_git_hash(pkg.dir) ~= hash then
            last_ops[pkg.name] = "update"
            report("update", "ok", pkg.name)
            if pkg.run then run_hook(pkg) end
        else
            (cfg.verbose and report or update_count)("update", "nop", pkg.name, num_pkgs) -- blursed
        end
    end
    call_proc("git", {"pull", "--recurse-submodules", "--update-shallow"}, pkg.dir, post_update)
end

local function remove(packdir)
    local name, dir, pkg
    local to_rm = {}
    local c = 0
    local handle = uv.fs_scandir(packdir)
    while handle do
        name = uv.fs_scandir_next(handle)
        if not name then break end
        pkg = packages[name]
        dir = packdir .. name
        if not (pkg and pkg.dir == dir) then
            to_rm[name] = dir
            c = c + 1
        end
    end
    for name, dir in pairs(to_rm) do
        if name ~= "paq-nvim" then
            local ok = vim.fn.delete(dir, "rf")
            report("remove", ok == 0 and "ok" or "err", name, c)
        end
    end
end

local function list()
    local installed = vim.tbl_filter(function(name) return packages[name].exists end, vim.tbl_keys(packages))
    local removed = vim.tbl_filter(function(name) return last_ops[name] == "remove" end, vim.tbl_keys(last_ops))
    table.sort(installed)
    table.sort(removed)
    local sym_tbl = {install = "+", update = "*", remove = " "}
    for header, pkgs in pairs {["Installed packages:"] = installed, ["Recently removed:"] = removed} do
        if #pkgs ~= 0 then
            print(header)
            for _, name in ipairs(pkgs) do
                print("  ", sym_tbl[last_ops[name]] or " ", name)
            end
        end
    end
end

local function register(args)
    if type(args) == "string" then args = {args} end
    local name, src
    if args.as then
        name = args.as
    elseif args.url then
        name = args.url:gsub("%.git$", ""):match("/([%w-_.]+)$")
        src = args.url
    else
        name = args[1]:match("^[%w-]+/([%w-_.]+)$")
        src = args[1]
    end
    if not name then
        return print_err("Paq: Failed to parse " .. src)
    elseif packages[name] then
        return
    end

    local dir = cfg.paqdir .. (args.opt and "opt/" or "start/") .. name

    packages[name] = {
        name = name,
        branch = args.branch,
        dir = dir,
        exists = vim.fn.isdirectory(dir) ~= 0,
        pin = args.pin,
        run = args.run or args.hook, -- DEPRECATE 1.0
        url = args.url or "https://github.com/" .. args[1] .. ".git"
    }
    num_pkgs = num_pkgs + 1
end

do
    vim.tbl_map(vim.cmd, {
        "command! PaqInstall  lua require('paq'):install()",
        "command! PaqUpdate   lua require('paq'):update()",
        "command! PaqClean    lua require('paq'):clean()",
        "command! PaqRunHooks lua require('paq'):run_hooks()",
        "command! PaqSync     lua require('paq'):sync()",
        "command! PaqList     lua require('paq').list()",
        "command! PaqLogOpen  lua require('paq').log_open()",
        "command! PaqLogClean lua require('paq').log_clean()"
    })
end

return setmetatable({
    paq = register, -- DEPRECATE 1.0
    install = function(self) Counter "install" vim.tbl_map(install, packages) return self end,
    update = function(self) Counter "update" vim.tbl_map(update, packages) return self end,
    clean = function(self) Counter "remove" remove(cfg.paqdir .. "start/") remove(cfg.paqdir .. "opt/") return self end,
    sync = function(self) self:clean():update():install() return self end,
    run_hooks = function(self) vim.tbl_map(run_hook, packages) return self end,
    list = list,
    setup = function(self, args) for k,v in pairs(args) do cfg[k] = v end return self end,
    log_open = function(self) vim.cmd("sp " .. LOGFILE) return self end,
    log_clean = function(self) uv.fs_unlink(LOGFILE) print("Paq log file deleted") return self end,
}, {__call = function(self, tbl) packages = {} num_pkgs = 0 vim.tbl_map(register, tbl) return self end}
)