aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/managers/github/client.lua
blob: 788b402bd221c98e8d1a361cde60c11ceeeb1063 (plain) (blame)
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