aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/managers
diff options
context:
space:
mode:
Diffstat (limited to 'lua/mason-core/managers')
-rw-r--r--lua/mason-core/managers/cargo/init.lua167
-rw-r--r--lua/mason-core/managers/github/client.lua26
2 files changed, 143 insertions, 50 deletions
diff --git a/lua/mason-core/managers/cargo/init.lua b/lua/mason-core/managers/cargo/init.lua
index b8cd4fa7..8890daf6 100644
--- a/lua/mason-core/managers/cargo/init.lua
+++ b/lua/mason-core/managers/cargo/init.lua
@@ -5,6 +5,8 @@ local a = require "mason-core.async"
local Optional = require "mason-core.optional"
local installer = require "mason-core.installer"
local client = require "mason-core.managers.cargo.client"
+local github = require "mason-core.managers.github"
+local github_client = require "mason-core.managers.github.client"
local _ = require "mason-core.functional"
local get_bin_path = _.compose(path.concat, function(executable)
@@ -23,32 +25,50 @@ local M = {}
---@async
---@param crate string The crate to install.
----@param opts {git: boolean | string, features: string?, bin: string[]? }?
+---@param opts { git: { url: string, tag: boolean? }, features: string?, bin: string[]? }?
function M.crate(crate, opts)
return function()
- M.install(crate, opts).with_receipt()
+ if opts and opts.git and opts.git.tag then
+ local ctx = installer.context()
+ local repo = assert(opts.git.url:match "^https://github%.com/(.+)$", "git url needs to be github.com")
+ local source = github.tag { repo = repo }
+ source.with_receipt()
+ ctx.requested_version = Optional.of(source.tag)
+ M.install(crate, opts)
+ else
+ M.install(crate, opts).with_receipt()
+ end
end
end
---@async
---@param crate string The crate to install.
----@param opts {git: boolean | string, features: string?, bin: string[]? }?
+---@param opts { git: { url: string, tag: boolean? }, features: string?, bin: string[]? }?
function M.install(crate, opts)
local ctx = installer.context()
opts = opts or {}
- ctx.requested_version:if_present(function()
- assert(not opts.git, "Providing a version when installing a git crate is not allowed.")
- end)
- ---@type string | string[]
- local final_crate = crate
+ local version
if opts.git then
- final_crate = { "--git" }
- if type(opts.git) == "string" then
- table.insert(final_crate, opts.git)
+ if opts.git.tag then
+ assert(ctx.requested_version:is_present(), "version is required when installing tagged git crate.")
end
- table.insert(final_crate, crate)
+ version = ctx.requested_version
+ :map(function(version)
+ if opts.git.tag then
+ return { "--tag", version }
+ else
+ return { "--rev", version }
+ end
+ end)
+ :or_else(vim.NIL)
+ else
+ version = ctx.requested_version
+ :map(function(version)
+ return { "--version", version }
+ end)
+ :or_else(vim.NIL)
end
ctx.spawn.cargo {
@@ -56,13 +76,10 @@ function M.install(crate, opts)
"--root",
".",
"--locked",
- ctx.requested_version
- :map(function(version)
- return { "--version", version }
- end)
- :or_else(vim.NIL),
+ version,
+ opts.git and { "--git", opts.git.url } or vim.NIL,
opts.features and { "--features", opts.features } or vim.NIL,
- final_crate,
+ crate,
}
if opts.bin then
@@ -76,42 +93,39 @@ function M.install(crate, opts)
}
end
+---@alias InstalledCrate { name: string, version: string, github_ref: { owner: string, repo: string, ref: string }? }
+
+---@param line string
+---@return InstalledCrate? crate
+local function parse_installed_crate(line)
+ local name, version, context = line:match "^(.+)%s+v([^%s:]+) ?(.*):$"
+ if context then
+ local owner, repo, ref = context:match "^%(https://github%.com/(.+)/([^?]+).*#(.+)%)$"
+ if ref then
+ return { name = name, version = ref, github_ref = { owner = owner, repo = repo, ref = ref } }
+ end
+ end
+ if name and version then
+ return { name = name, version = version }
+ end
+end
+
---@param output string The `cargo install --list` output.
----@return table<string, string> # Key is the crate name, value is its version.
+---@return table<string, InstalledCrate> # Key is the crate name, value is its version.
function M.parse_installed_crates(output)
local installed_crates = {}
for _, line in ipairs(vim.split(output, "\n")) do
- local name, version = line:match "^(.+)%s+v([.%S]+)[%s:]"
- if name and version then
- installed_crates[name] = version
+ local installed_crate = parse_installed_crate(line)
+ if installed_crate then
+ installed_crates[installed_crate.name] = installed_crate
end
end
return installed_crates
end
---@async
----@param receipt InstallReceipt<InstallReceiptPackageSource>
---@param install_dir string
-function M.check_outdated_primary_package(receipt, install_dir)
- return M.get_installed_primary_package_version(receipt, install_dir):map_catching(function(installed_version)
- ---@type CrateResponse
- local crate_response = client.fetch_crate(receipt.primary_source.package):get_or_throw()
- if installed_version ~= crate_response.crate.max_stable_version then
- return {
- name = receipt.primary_source.package,
- current_version = installed_version,
- latest_version = crate_response.crate.max_stable_version,
- }
- else
- error "Primary package is not outdated."
- end
- end)
-end
-
----@async
----@param receipt InstallReceipt<InstallReceiptPackageSource>
----@param install_dir string
-function M.get_installed_primary_package_version(receipt, install_dir)
+local function get_installed_crates(install_dir)
return spawn
.cargo({
"install",
@@ -121,13 +135,68 @@ function M.get_installed_primary_package_version(receipt, install_dir)
cwd = install_dir,
})
:map_catching(function(result)
- local installed_crates = M.parse_installed_crates(result.stdout)
- if vim.in_fast_event() then
- a.scheduler() -- needed because vim.fn.* call
- end
- local pkg = vim.fn.fnamemodify(receipt.primary_source.package, ":t")
- return Optional.of_nilable(installed_crates[pkg]):or_else_throw "Failed to find cargo package version."
+ return M.parse_installed_crates(result.stdout)
end)
end
+---@async
+---@param receipt InstallReceipt<InstallReceiptPackageSource>
+---@param install_dir string
+function M.check_outdated_primary_package(receipt, install_dir)
+ if vim.in_fast_event() then
+ a.scheduler()
+ end
+ local crate_name = vim.fn.fnamemodify(receipt.primary_source.package, ":t")
+ return get_installed_crates(install_dir)
+ :ok()
+ :map(_.prop(crate_name))
+ :map(
+ ---@param installed_crate InstalledCrate
+ function(installed_crate)
+ if installed_crate.github_ref then
+ ---@type GitHubCommit
+ local latest_commit = github_client
+ .fetch_commits(
+ ("%s/%s"):format(installed_crate.github_ref.owner, installed_crate.github_ref.repo),
+ { page = 1, per_page = 1 }
+ )
+ :get_or_throw("Failed to fetch latest commits.")[1]
+ if not vim.startswith(latest_commit.sha, installed_crate.github_ref.ref) then
+ return {
+ name = receipt.primary_source.package,
+ current_version = installed_crate.github_ref.ref,
+ latest_version = latest_commit.sha,
+ }
+ end
+ else
+ ---@type CrateResponse
+ local crate_response = client.fetch_crate(crate_name):get_or_throw()
+ if installed_crate.version ~= crate_response.crate.max_stable_version then
+ return {
+ name = receipt.primary_source.package,
+ current_version = installed_crate.version,
+ latest_version = crate_response.crate.max_stable_version,
+ }
+ end
+ end
+ end
+ )
+ :ok_or(_.always "Primary package is not outdated.")
+end
+
+---@async
+---@param receipt InstallReceipt<InstallReceiptPackageSource>
+---@param install_dir string
+function M.get_installed_primary_package_version(receipt, install_dir)
+ return get_installed_crates(install_dir):map(function(pkgs)
+ if vim.in_fast_event() then
+ a.scheduler()
+ end
+ local pkg = vim.fn.fnamemodify(receipt.primary_source.package, ":t")
+ return Optional.of_nilable(pkgs[pkg])
+ :map(_.prop "version")
+ :or_else_throw "Failed to find cargo package version."
+ end)
+end
+
return M
diff --git a/lua/mason-core/managers/github/client.lua b/lua/mason-core/managers/github/client.lua
index 8f4545e8..788b402b 100644
--- a/lua/mason-core/managers/github/client.lua
+++ b/lua/mason-core/managers/github/client.lua
@@ -8,10 +8,16 @@ local M = {}
---@alias GitHubReleaseAsset {url: string, id: integer, name: string, browser_download_url: string, created_at: string, updated_at: string, size: integer, download_count: integer}
---@alias GitHubRelease {tag_name: string, prerelease: boolean, draft: boolean, assets:GitHubReleaseAsset[]}
---@alias GitHubTag {name: string}
+---@alias GitHubCommit {sha: string}
---@param path string
+---@param opts { params: table<string, any>? }?
---@return Result # JSON decoded response.
-local function api_call(path)
+local function api_call(path, opts)
+ if opts and opts.params then
+ local params = _.join("&", _.map(_.join "=", _.sort_by(_.head, _.to_pairs(opts.params))))
+ path = ("%s?%s"):format(path, params)
+ end
return spawn
.gh({ "api", path, env = { CLICOLOR_FORCE = 0 } })
:map(_.prop "stdout")
@@ -21,6 +27,8 @@ local function api_call(path)
:map_catching(vim.json.decode)
end
+M.api_call = api_call
+
---@async
---@param repo string The GitHub repo ("username/repo").
---@return Result # Result<GitHubRelease[]>
@@ -105,6 +113,22 @@ function M.fetch_latest_tag(repo)
:map(_.prop "tag")
end
+---@async
+---@param repo string The GitHub repo ("username/repo").
+---@param opts { page: integer?, per_page: integer? }?
+---@return Result # Result<GitHubCommit[]>
+function M.fetch_commits(repo, opts)
+ local path = ("repos/%s/commits"):format(repo)
+ return api_call(path, {
+ params = {
+ page = opts and opts.page or 1,
+ per_page = opts and opts.per_page or 30,
+ },
+ }):map_err(function()
+ return ("Failed to fetch commits for GitHub repository %s."):format(repo)
+ end)
+end
+
---@alias GitHubRateLimit {limit: integer, remaining: integer, reset: integer, used: integer}
---@alias GitHubRateLimitResponse {resources: { core: GitHubRateLimit }}