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
|
local _ = require "mason-core.functional"
local log = require "mason-core.log"
local fetch = require "mason-core.fetch"
local spawn = require "mason-core.spawn"
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, 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")
:recover_catching(function()
return fetch(("https://api.github.com/%s"):format(path)):get_or_throw()
end)
:map_catching(vim.json.decode)
end
M.api_call = api_call
---@async
---@param repo string The GitHub repo ("username/repo").
---@return Result # Result<GitHubRelease[]>
function M.fetch_releases(repo)
log.fmt_trace("Fetching GitHub releases for repo=%s", repo)
local path = ("repos/%s/releases"):format(repo)
return api_call(path):map_err(function()
return ("Failed to fetch releases for GitHub repository %s."):format(repo)
end)
end
---@async
---@param repo string The GitHub repo ("username/repo").
---@param tag_name string The tag_name of the release to fetch.
function M.fetch_release(repo, tag_name)
log.fmt_trace("Fetching GitHub release for repo=%s, tag_name=%s", repo, tag_name)
local path = ("repos/%s/releases/tags/%s"):format(repo, tag_name)
return api_call(path):map_err(function()
return ("Failed to fetch release %q for GitHub repository %s."):format(tag_name, repo)
end)
end
---@param opts {include_prerelease: boolean, tag_name_pattern: string}
function M.release_predicate(opts)
local is_not_draft = _.prop_eq("draft", false)
local is_not_prerelease = _.prop_eq("prerelease", false)
local tag_name_matches = _.prop_satisfies(_.matches(opts.tag_name_pattern), "tag_name")
return _.all_pass {
_.if_else(_.always(opts.include_prerelease), _.T, is_not_prerelease),
_.if_else(_.always(opts.tag_name_pattern), tag_name_matches, _.T),
is_not_draft,
}
end
---@alias FetchLatestGithubReleaseOpts {tag_name_pattern:string?, include_prerelease: boolean}
---@async
---@param repo string The GitHub repo ("username/repo").
---@param opts FetchLatestGithubReleaseOpts?
---@return Result # Result<GitHubRelease>
function M.fetch_latest_release(repo, opts)
opts = opts or {
tag_name_pattern = nil,
include_prerelease = false,
}
return M.fetch_releases(repo):map_catching(
---@param releases GitHubRelease[]
function(releases)
local is_stable_release = M.release_predicate(opts)
---@type GitHubRelease|nil
local latest_release = _.find_first(is_stable_release, releases)
if not latest_release then
log.fmt_info("Failed to find latest release. repo=%s, opts=%s", repo, opts)
error "Failed to find latest release."
end
log.fmt_debug("Resolved latest version repo=%s, tag_name=%s", repo, latest_release.tag_name)
return latest_release
end
)
end
---@async
---@param repo string The GitHub repo ("username/repo").
---@return Result # Result<GitHubTag[]>
function M.fetch_tags(repo)
local path = ("repos/%s/tags"):format(repo)
return api_call(path):map_err(function()
return ("Failed to fetch tags for GitHub repository %s."):format(repo)
end)
end
---@async
---@param repo string The GitHub repo ("username/repo").
---@return Result # Result<string> The latest tag name.
function M.fetch_latest_tag(repo)
-- https://github.com/williamboman/vercel-github-api-latest-tag-proxy
return fetch(("https://latest-github-tag.redwill.se/api/repo/%s/latest-tag"):format(repo))
:map_catching(vim.json.decode)
: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 }}
---@async
--@return Result @of GitHubRateLimitResponse
function M.fetch_rate_limit()
return api_call "rate_limit"
end
return M
|