@@ -17,7 +17,7 @@ public static async Task ExecuteAsync(string owner, string repo, int topN)
17
17
{
18
18
repo = GetUserInput ( "请输入仓库名称:" , ValidateOwnerOrRepo ) ;
19
19
}
20
-
20
+
21
21
if ( topN < 1 )
22
22
{
23
23
topN = GetTopNInput ( "请输入要获取前几个贡献者信息:" ) ;
@@ -26,7 +26,6 @@ public static async Task ExecuteAsync(string owner, string repo, int topN)
26
26
bool repositoryExists = await ValidateRepositoryExists ( owner , repo ) ;
27
27
if ( ! repositoryExists )
28
28
{
29
- AnsiConsole . Markup ( "[red]该仓库不存在,请检查输入的所有者和仓库名是否正确。[/]\n " ) ;
30
29
return ;
31
30
}
32
31
@@ -36,27 +35,160 @@ public static async Task ExecuteAsync(string owner, string repo, int topN)
36
35
private static async Task GetContributorsAsync ( string owner , string repo , int topN )
37
36
{
38
37
using HttpClient client = new ( ) ;
39
- string url = $ "https://api.github.com/repos/{ owner } /{ repo } /contributors?per_page={ topN } ";
40
38
client . DefaultRequestHeaders . Add ( "User-Agent" , "CSharpApp" ) ;
41
- HttpResponseMessage response = await client . GetAsync ( url ) ;
39
+ int pages = ( int ) Math . Ceiling ( ( double ) topN / 100 ) ; // 提前声明总页数,每页个数在后面计算
40
+ try
41
+ {
42
+ if ( topN <= 100 ) // 100个以内不需要分页
43
+ {
44
+ pages = 1 ; // 1 页就够 - 不用再请求总页数
45
+ }
46
+ else // 100个以上则先定义每页100个以减少页数,进而减少请求次数
47
+ {
48
+ HttpResponseMessage response = await client . GetAsync ( "https://api.github.com/repos/{owner}/{repo}/contributors" ) ; // 先请求一下获取总页数
49
+ if ( response . IsSuccessStatusCode )
50
+ {
51
+ string ? linkHeader = response . Headers . Contains ( "Link" ) ? response . Headers . GetValues ( "Link" ) . FirstOrDefault ( ) : null ;
52
+ if ( linkHeader == null )
53
+ {
54
+ // 解析失败
55
+ AnsiConsole . Markup ( "[yellow]无法解析总分页数: Link 头为 null[/]\n " ) ;
56
+ }
57
+ else
58
+ {
59
+ Regex regex = new ( @"<([^>]+)>;\s*rel=""last""" ) ;
60
+ Match match = regex . Match ( linkHeader ) ;
61
+ string allPagesString = match . Groups [ 1 ] . Value ; // 返回匹配的 URL 部分
42
62
43
- if ( response . IsSuccessStatusCode )
63
+ if ( match . Success )
64
+ {
65
+ if ( int . TryParse ( allPagesString , out int allPages ) )
66
+ {
67
+ // 成功解析,allPages 现在是整数
68
+ /*
69
+ 首先将 topN 强制转换为 double 类型,以确保执行除法时获得浮点数。
70
+ 然后通过 Math.Ceiling() 向上取整。
71
+ 最后将结果转换回 int 类型获得最终需要请求的总页数。
72
+ */
73
+ if ( pages > allPages )
74
+ {
75
+ pages = allPages ; // 再多也不能超出总页数呀
76
+ }
77
+ }
78
+ else
79
+ {
80
+ // 解析失败
81
+ AnsiConsole . Markup ( "[yellow]无法解析总分页数: 匹配失败[/]\n " ) ;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ catch ( HttpRequestException e )
44
89
{
45
- string content = await response . Content . ReadAsStringAsync ( ) ;
46
- JsonNode jsonNode = JsonNode . Parse ( content ) ;
47
- JsonArray contributors = jsonNode . AsArray ( ) ;
90
+ AnsiConsole . Markup ( $ "[yellow]无法解析总分页数: 请求失败[/]\n [yellow]信息:{ e . Message } [/]") ;
91
+ // 直接用最开始算的,后面还有错再报,这里用警告样式
92
+ }
93
+
94
+ #if DEBUG
95
+ AnsiConsole . Markup ( $ "[purple][[DEBUG]] 请求分页数: { pages } [/]\n ") ;
96
+ #endif
97
+
98
+ List < JsonNode > allContributors = [ ] ;
99
+ string url ;
100
+
101
+ for ( int i = 1 ; i <= pages ; i ++ )
102
+ {
103
+ // 计算此页个数
104
+ if ( ( i == pages ) && ( topN % 100 != 0 ) ) // 如果是最后一页
105
+ {
106
+ url = $ "https://api.github.com/repos/{ owner } /{ repo } /contributors?per_page={ topN % 100 } &page={ i } ";
107
+ }
108
+ else
109
+ {
110
+ url = $ "https://api.github.com/repos/{ owner } /{ repo } /contributors?per_page={ 100 } &page={ i } ";
111
+ }
112
+
113
+ #if DEBUG
114
+ AnsiConsole . Markup ( $ "[purple][[DEBUG]] 请求: { url } [/]\n ") ;
115
+ #endif
116
+
117
+ try
118
+ {
119
+ HttpResponseMessage response = await client . GetAsync ( url ) ;
120
+ if ( response . IsSuccessStatusCode )
121
+ {
122
+ string jsonResponse = await response . Content . ReadAsStringAsync ( ) ;
123
+ JsonNode ? jsonNode = JsonNode . Parse ( jsonResponse ) ;
124
+ if ( jsonNode == null || jsonNode . AsArray ( ) == null )
125
+ {
126
+ AnsiConsole . Markup ( "[red]无法解析贡献者数据: 请求成功,但返回 null[/]\n " ) ;
127
+ return ;
128
+ }
129
+
130
+ // 合并获取到的贡献者
131
+ allContributors . AddRange ( jsonNode . AsArray ( ) . Cast < JsonNode > ( ) ) ;
132
+ }
133
+ else if ( response . StatusCode == System . Net . HttpStatusCode . Forbidden )
134
+ {
135
+ string ? resetTime = response . Headers . Contains ( "X-RateLimit-Reset" )
136
+ ? response . Headers . GetValues ( "X-RateLimit-Reset" ) . FirstOrDefault ( ) // 获取第一个值
137
+ : "未知" ;
138
+
139
+ AnsiConsole . Markup ( $ "[yellow]你 太 快 了 ![/]\n [yellow]您的请求已达到 [link=https://docs.github.com/zh/rest/using-the-rest-api/rate-limits-for-the-rest-api]GitHub API 速率限制[/][/]\n ") ;
140
+ if ( ( resetTime != null ) && ( resetTime != "未知" ) )
141
+ {
142
+ // 等待速率限制重置
143
+ #if DEBUG
144
+ AnsiConsole . Markup ( $ "[purple][[DEBUG]] 速率重置时间戳: { Markup . Escape ( resetTime ) } [/]\n ") ;
145
+ #endif
146
+ long resetTimestamp = long . Parse ( resetTime ) ;
147
+ DateTime resetDateTimeUtc = DateTimeOffset . FromUnixTimeSeconds ( resetTimestamp ) . DateTime ;
148
+ // 将 UTC 时间转换为本地时间
149
+ DateTime resetDateTimeLocal = resetDateTimeUtc . ToLocalTime ( ) ;
150
+ #if DEBUG
151
+ AnsiConsole . Markup ( $ "[purple][[DEBUG]] 转化后速率重置时间: { resetDateTimeLocal } [/]\n ") ;
152
+ #endif
153
+ TimeSpan waitTime = resetDateTimeLocal - DateTime . Now ;
154
+ AnsiConsole . Markup ( $ "[yellow]重置时间: { resetDateTimeLocal } (需要等待 { waitTime } )[/]") ;
155
+ Thread . Sleep ( waitTime ) ; // 等待重置
156
+ i -- ; // 本次请求没成功,后面重试时再请求这页
157
+ }
158
+ else
159
+ {
160
+ AnsiConsole . Markup ( "[red]无法获取贡献数据: 请求达到速率限制,但无法获取重置时间戳: X-RateLimit-Reset 头为 null 或无法转为 String 类型[/]\n " ) ;
161
+ return ;
162
+ }
163
+ }
164
+ else
165
+ {
48
166
49
- var table = new Table ( ) ;
167
+ AnsiConsole . Markup ( "[red]无法获取贡献数据: 请求失败: 未捕获异常[/]\n " ) ;
168
+ return ;
169
+ }
170
+ }
171
+ catch ( Exception ex )
172
+ {
173
+ // 捕获网络或其他错误
174
+ AnsiConsole . Markup ( $ "[red]无法获取贡献数据: 请求发生异常: { Markup . Escape ( ex . Message ) } [/]\n ") ;
175
+ }
176
+
177
+ Thread . Sleep ( 1000 ) ; // 请求间间隔 1 秒 - https://docs.github.com/zh/rest/using-the-rest-api/best-practices-for-using-the-rest-api?apiVersion=2022-11-28#handle-rate-limit-errors-appropriately
178
+ }
179
+
180
+ if ( allContributors . Count > 0 )
181
+ {
182
+ Table table = new ( ) ;
50
183
table . AddColumn ( "排名" ) ;
51
184
table . AddColumn ( "贡献者" ) ;
52
185
table . AddColumn ( "贡献计数" ) ;
53
186
54
187
int rank = 1 ;
55
- foreach ( JsonNode contributor in contributors )
188
+ foreach ( JsonNode contributor in allContributors )
56
189
{
57
- JsonObject contributorObject = contributor . AsObject ( ) ;
58
- string login = Markup . Escape ( contributorObject [ "login" ] . GetValue < string > ( ) ) ; // 转义 [] 之类的特殊符号
59
- int contributions = contributorObject [ "contributions" ] . GetValue < int > ( ) ;
190
+ string login = Markup . Escape ( contributor [ "login" ] ? . ToString ( ) ?? string . Empty ) ;
191
+ int contributions = contributor [ "contributions" ] ? . GetValue < int > ( ) ?? 0 ;
60
192
table . AddRow ( rank . ToString ( ) , $ "[link=https://github.com/{ login } ]{ login } [/]", contributions . ToString ( ) ) ;
61
193
rank ++ ;
62
194
}
@@ -65,7 +197,7 @@ private static async Task GetContributorsAsync(string owner, string repo, int to
65
197
}
66
198
else
67
199
{
68
- AnsiConsole . Markup ( "[red]获取贡献者数据失败 。[/]" ) ;
200
+ AnsiConsole . Markup ( "[red]该仓库没有贡献者 (如果仓库为空则可能出现此情况) 。[/]" ) ;
69
201
}
70
202
}
71
203
@@ -95,12 +227,33 @@ private static int GetTopNInput(string prompt)
95
227
AnsiConsole . Markup ( "[cyan]? [/]" + prompt + " " ) ;
96
228
string input = ( Console . ReadLine ( ) ?? string . Empty ) . Trim ( ) ;
97
229
98
- if ( int . TryParse ( input , out topN ) && topN >= 1 )
230
+ // 尝试解析输入为 int 类型
231
+ if ( int . TryParse ( input , out topN ) )
99
232
{
100
- break ;
233
+ // 检查输入的数字是否在 int 类型的有效范围内
234
+ if ( topN >= 1 )
235
+ {
236
+ break ;
237
+ }
238
+ else
239
+ {
240
+ AnsiConsole . Markup ( "[red]请输入一个大于等于 1 的正整数。[/]\n " ) ;
241
+ }
242
+ }
243
+ else
244
+ {
245
+ // 如果 int.TryParse 失败,说明输入不符合 int 类型的格式
246
+ // 检查是否超出 int 范围
247
+ if ( input . Length > 10 || long . TryParse ( input , out long largeInput ) && largeInput > int . MaxValue )
248
+ {
249
+ AnsiConsole . Markup ( "[red]太多了!int 类型最大为 2,147,483,647[/]\n " ) ;
250
+ }
251
+ else
252
+ {
253
+ // 普通的格式错误提示
254
+ AnsiConsole . Markup ( "[red]请输入一个有效的正整数。[/]\n " ) ;
255
+ }
101
256
}
102
-
103
- AnsiConsole . Markup ( "[red]请输入一个大于等于 1 的正整数。[/]\n " ) ;
104
257
}
105
258
while ( true ) ;
106
259
@@ -116,11 +269,33 @@ private static bool ValidateOwnerOrRepo(string input)
116
269
117
270
private static async Task < bool > ValidateRepositoryExists ( string owner , string repo )
118
271
{
119
- using HttpClient client = new ( ) ;
120
- string url = $ "https://api.github.com/repos/{ owner } /{ repo } ";
121
- client . DefaultRequestHeaders . Add ( "User-Agent" , "CSharpApp" ) ;
122
- HttpResponseMessage response = await client . GetAsync ( url ) ;
123
- return response . IsSuccessStatusCode ;
272
+ try
273
+ {
274
+ using HttpClient client = new ( ) ;
275
+ string url = $ "https://api.github.com/repos/{ owner } /{ repo } ";
276
+ client . DefaultRequestHeaders . Add ( "User-Agent" , "CSharpApp" ) ;
277
+ HttpResponseMessage response = await client . GetAsync ( url ) ;
278
+
279
+ if ( ! response . IsSuccessStatusCode )
280
+ {
281
+ // 仓库不存在的提示
282
+ AnsiConsole . Markup ( "[red]仓库不存在(或没有访问权限),请检查输入的所有者和仓库名是否正确。[/]\n " ) ;
283
+ return false ;
284
+ }
285
+ return true ;
286
+ }
287
+ catch ( HttpRequestException e )
288
+ {
289
+ // 处理速率限制或其他访问错误
290
+ AnsiConsole . Markup ( $ "[red]无法验证仓库是否存在: 请求错误[/]\n [red]您的请求可能已达到 [link=https://docs.github.com/zh/rest/using-the-rest-api/rate-limits-for-the-rest-api]GitHub API 速率限制[/][/]\n [red]详细错误信息: { e } [/]\n ") ;
291
+ return false ;
292
+ }
293
+ catch ( Exception ex )
294
+ {
295
+ // 其他异常处理
296
+ AnsiConsole . Markup ( $ "[red]无法验证仓库是否存在: { ex . Message } [/]\n ") ;
297
+ return false ;
298
+ }
124
299
}
125
300
}
126
301
}
0 commit comments