• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

KSP-CKAN / CKAN / 15908786007

26 Jun 2025 05:43PM UTC coverage: 47.627%. Remained the same
15908786007

push

github

HebaruSan
HttpStatusCode.TooManyRequests isn't defined in net481

3879 of 8730 branches covered (44.43%)

Branch coverage included in aggregate %.

8334 of 16913 relevant lines covered (49.28%)

1.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

40.98
/Netkan/Sources/Github/GithubApi.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Linq;
4
using System.Net;
5
using System.Text.RegularExpressions;
6
using System.Diagnostics.CodeAnalysis;
7

8
using log4net;
9
using Newtonsoft.Json;
10

11
using CKAN.NetKAN.Services;
12

13
// We could use OctoKit for this, but since we're only pinging the
14
// release API, I'm happy enough without yet another dependency.
15

16
namespace CKAN.NetKAN.Sources.Github
17
{
18
    internal sealed class GithubApi : IGithubApi
19
    {
20
        private static readonly ILog Log = LogManager.GetLogger(typeof(GithubApi));
2✔
21
        private static readonly Uri ApiBase = new Uri("https://api.github.com/");
2✔
22

23
        private readonly IHttpService _http;
24
        private readonly string?      _oauthToken;
25

26
        private const string rawMediaType = "application/vnd.github.v3.raw";
27

28
        // https://github.com/<OWNER>/<REPO>/blob/<BRANCH>/<PATH>
29
        // https://github.com/<OWNER>/<REPO>/tree/<BRANCH>/<PATH>
30
        // https://github.com/<OWNER>/<REPO>/raw/<BRANCH>/<PATH>
31
        private static readonly Regex githubUrlRegex = new Regex(
2✔
32
            @"^/(?<owner>[^/]+)/(?<repo>[^/]+)/(?<type>[^/]+)/(?<branch>[^/]+)/(?<path>.+)$",
33
            RegexOptions.Compiled);
34

35
        private static readonly HashSet<string> urlTypes = new HashSet<string>()
2✔
36
        {
37
            "blob", "tree", "raw"
38
        };
39

40
        // https://raw.githubusercontent.com/<OWNER>/<REPO>/<BRANCH>/<PATH>
41
        private static readonly Regex githubUserContentUrlRegex =
2✔
42
            new Regex(@"^/(?<owner>[^/]+)/(?<repo>[^/]+)/(refs/heads/)?(?<branch>[^/]+)/(?<path>.+)$",
43
                      RegexOptions.Compiled);
44

45
        public GithubApi(IHttpService http, string? oauthToken = null)
2✔
46
        {
2✔
47
            _http       = http;
2✔
48
            _oauthToken = oauthToken;
2✔
49
        }
2✔
50

51
        public GithubRepo? GetRepo(GithubRef reference)
52
            => Call($"repos/{reference.Repository}") is string s
2!
53
                   ? JsonConvert.DeserializeObject<GithubRepo>(s)
54
                   : null;
55

56
        public GithubRelease? GetLatestRelease(GithubRef reference, bool? usePrerelease)
57
            => GetAllReleases(reference, usePrerelease).FirstOrDefault();
2✔
58

59
        private static bool ReleaseTypeMatches(bool? UsePrerelease, bool isPreRelease)
60
            => !UsePrerelease.HasValue
2!
61
               || UsePrerelease.Value == isPreRelease;
62

63
        public IEnumerable<GithubRelease> GetAllReleases(GithubRef reference, bool? usePrerelease)
64
        {
2✔
65
            const int perPage = 10;
66
            for (int page = 1; true; ++page)
3✔
67
            {
2✔
68
                var json = Call($"repos/{reference.Repository}/releases?per_page={perPage}&page={page}");
2✔
69
                if (json == null)
2!
70
                {
×
71
                    break;
×
72
                }
73
                Log.Debug("Parsing JSON...");
2✔
74
                var releases = JsonConvert.DeserializeObject<GithubRelease[]>(json)
2✔
75
                               ?? Array.Empty<GithubRelease>();
76
                if (releases.Length < 1)
2!
77
                {
×
78
                    // That's all folks!
79
                    break;
×
80
                }
81

82
                foreach (var ghRel in releases.Where(r => ReleaseTypeMatches(usePrerelease, r.PreRelease)
5!
83
                                                          // Skip releases without assets
84
                                                          && (reference.UseSourceArchive
85
                                                              || (r.Assets != null
86
                                                                  && r.Assets.Any(reference.FilterMatches))))
87
                                              // Insurance against GitHub returning them in the wrong order
88
                                              .OrderByDescending(r => r.PublishedAt))
2✔
89
                {
2✔
90
                    yield return ghRel;
2✔
91
                }
2✔
92
            }
×
93
        }
×
94

95
        public List<GithubUser> getOrgMembers(GithubUser organization)
96
            => (Call($"orgs/{organization.Login}/public_members") is string s
2!
97
                    ? JsonConvert.DeserializeObject<List<GithubUser>>(s)
98
                    : null)
99
               ?? new List<GithubUser>();
100

101
        /// <summary>
102
        /// Download a URL via the GitHubAPI.
103
        /// Will use a token if we have one.
104
        /// </summary>
105
        /// <param name="url">The URL to download</param>
106
        /// <returns>
107
        /// null if the URL isn't on GitHub, otherwise the contents of the download
108
        /// </returns>
109
        public string? DownloadText(Uri url)
110
        {
×
111
            if (TryGetGitHubPath(url, out string? ghOwner,
×
112
                                      out string? ghRepo,
113
                                      out string? ghBranch,
114
                                      out string? ghPath))
115
            {
×
116
                Log.InfoFormat("Found GitHub URL, retrieving with API: {0} {1} {2} {3}",
×
117
                               ghOwner, ghRepo, ghBranch, ghPath);
118
                return Call(
×
119
                    $"repos/{ghOwner}/{ghRepo}/contents/{ghPath}?ref={ghBranch}",
120
                    rawMediaType);
121
            }
122
            else
123
            {
×
124
                Log.DebugFormat("Not a GitHub URL: {0}", url.ToString());
×
125
                return null;
×
126
            }
127
        }
×
128

129
        private static bool TryGetGitHubPath(Uri url,
130
                                             [NotNullWhen(true)] out string? owner,
131
                                             [NotNullWhen(true)] out string? repo,
132
                                             [NotNullWhen(true)] out string? branch,
133
                                             [NotNullWhen(true)] out string? path)
134
        {
×
135
            switch (url.Host)
×
136
            {
137
                case "github.com":
138
                    Log.DebugFormat("Found standard GitHub host, checking format");
×
139
                    Match ghMatch = githubUrlRegex.Match(url.AbsolutePath);
×
140
                    if (ghMatch.Success && urlTypes.Contains(ghMatch.Groups["type"].Value))
×
141
                    {
×
142
                        Log.DebugFormat("Matched standard GitHub format");
×
143
                        owner  = ghMatch.Groups["owner"].Value;
×
144
                        repo   = ghMatch.Groups["repo"].Value;
×
145
                        branch = ghMatch.Groups["branch"].Value;
×
146
                        path   = ghMatch.Groups["path"].Value;
×
147
                        return true;
×
148
                    }
149
                    break;
×
150

151
                case "raw.githubusercontent.com":
152
                    Log.DebugFormat("Found raw GitHub host, checking format");
×
153
                    Match rawMatch = githubUserContentUrlRegex.Match(url.AbsolutePath);
×
154
                    if (rawMatch.Success)
×
155
                    {
×
156
                        Log.DebugFormat("Matched raw GitHub format");
×
157
                        owner  = rawMatch.Groups["owner"].Value;
×
158
                        repo   = rawMatch.Groups["repo"].Value;
×
159
                        branch = rawMatch.Groups["branch"].Value;
×
160
                        path   = rawMatch.Groups["path"].Value;
×
161
                        return true;
×
162
                    }
163
                    break;
×
164
            }
165
            owner = repo = branch = path = null;
×
166
            return false;
×
167
        }
×
168

169
        private string? Call(string path, string? mimeType = null)
170
        {
2✔
171
            var url = new Uri(ApiBase, path);
2✔
172
            Log.DebugFormat("Calling {0}", url);
2✔
173

174
            try
175
            {
2✔
176
                return _http.DownloadText(url, _oauthToken, mimeType);
2✔
177
            }
178
            catch (WebException k)
×
179
            {
×
180
                if (((HttpWebResponse?)k.Response)?.StatusCode is HttpStatusCode.Forbidden
×
181
                                                               #if NETFRAMEWORK
182
                                                               or (HttpStatusCode)429
183
                                                               #else
184
                                                               or HttpStatusCode.TooManyRequests
185
                                                               #endif
186
                                                               or HttpStatusCode.ServiceUnavailable
187
                    && k.Response.Headers["X-RateLimit-Remaining"] == "0"
188
                    && Net.ThrottledHosts.TryGetValue(url.Host, out Uri? infoUrl)
189
                    && infoUrl is not null)
190
                {
×
191
                    throw new RequestThrottledKraken(url, infoUrl, k,
×
192
                                                     $"GitHub API rate limit exceeded: {path}");
193
                }
194
                throw;
×
195
            }
196
        }
2✔
197
    }
198
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc