Skip to content

Commit 33746ee

Browse files
committed
Fix for #9.
1 parent 594f45f commit 33746ee

File tree

7 files changed

+160
-10
lines changed

7 files changed

+160
-10
lines changed

CCMenu.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
038FF7162BB62E6D0017CD4C /* GitHubReposByUserCCM2OnlyResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 038FF7152BB62E6D0017CD4C /* GitHubReposByUserCCM2OnlyResponse.json */; };
5656
038FF7182BB631CB0017CD4C /* URLComponentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038FF7172BB631CB0017CD4C /* URLComponentsExtension.swift */; };
5757
039B524629676D0700994910 /* Build.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039B524529676D0700994910 /* Build.swift */; };
58+
03A3EF932D3459D400407A6F /* GitHubUserResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 03A3EF922D3459D400407A6F /* GitHubUserResponse.json */; };
59+
03A3EF942D3459D400407A6F /* GitHubUserOrgResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 03A3EF912D3459D400407A6F /* GitHubUserOrgResponse.json */; };
5860
03B021252A5D9F9D00B889BE /* MenuExtraModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B021242A5D9F9D00B889BE /* MenuExtraModelTests.swift */; };
5961
03B1D75F2A6079CD007BCB8A /* MenuItemModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B1D75E2A6079CD007BCB8A /* MenuItemModelTests.swift */; };
6062
03B1D7612A607DD0007BCB8A /* PipelineRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B1D7602A607DD0007BCB8A /* PipelineRowViewModel.swift */; };
@@ -178,6 +180,8 @@
178180
038FF7152BB62E6D0017CD4C /* GitHubReposByUserCCM2OnlyResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = GitHubReposByUserCCM2OnlyResponse.json; sourceTree = "<group>"; };
179181
038FF7172BB631CB0017CD4C /* URLComponentsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponentsExtension.swift; sourceTree = "<group>"; };
180182
039B524529676D0700994910 /* Build.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Build.swift; sourceTree = "<group>"; };
183+
03A3EF912D3459D400407A6F /* GitHubUserOrgResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = GitHubUserOrgResponse.json; sourceTree = "<group>"; };
184+
03A3EF922D3459D400407A6F /* GitHubUserResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = GitHubUserResponse.json; sourceTree = "<group>"; };
181185
03B021242A5D9F9D00B889BE /* MenuExtraModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuExtraModelTests.swift; sourceTree = "<group>"; };
182186
03B1D75E2A6079CD007BCB8A /* MenuItemModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemModelTests.swift; sourceTree = "<group>"; };
183187
03B1D7602A607DD0007BCB8A /* PipelineRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PipelineRowViewModel.swift; sourceTree = "<group>"; };
@@ -294,6 +298,8 @@
294298
0322051F2B913BC900205DC6 /* Responses */ = {
295299
isa = PBXGroup;
296300
children = (
301+
03A3EF912D3459D400407A6F /* GitHubUserOrgResponse.json */,
302+
03A3EF922D3459D400407A6F /* GitHubUserResponse.json */,
297303
032205222B913CC000205DC6 /* GitHubWorkflowRunsResponse.json */,
298304
032205242B913D4000205DC6 /* GitHubReposByUserResponse.json */,
299305
038FF7152BB62E6D0017CD4C /* GitHubReposByUserCCM2OnlyResponse.json */,
@@ -711,6 +717,8 @@
711717
032205212B913C4E00205DC6 /* GitHubWorkflowsResponse.json in Resources */,
712718
032205292B96677200205DC6 /* GitHubUserReposResponse.json in Resources */,
713719
0322052B2B96708200205DC6 /* GitHubPipelineLocalhost.json in Resources */,
720+
03A3EF932D3459D400407A6F /* GitHubUserResponse.json in Resources */,
721+
03A3EF942D3459D400407A6F /* GitHubUserOrgResponse.json in Resources */,
714722
032205252B913D4000205DC6 /* GitHubReposByUserResponse.json in Resources */,
715723
038FF7162BB62E6D0017CD4C /* GitHubReposByUserCCM2OnlyResponse.json in Resources */,
716724
03825EA5259FFD1500DEB003 /* DefaultPipelines.json in Resources */,

CCMenu/Source/Pipeline Window/GitHub Sheets/GitHubRepositoryList.swift

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,25 @@ class GitHubRepositoryList: ObservableObject {
1717
}
1818

1919
private func fetchRepositories(owner: String, token: String?) async -> [GitHubRepository] {
20-
let ownerRepoRequest = GitHubAPI.requestForRepositories(owner: owner, token: token)
20+
let userRequest = GitHubAPI.requestForUser(user: owner, token: token)
21+
let (user, error) = await fetchUser(request: userRequest)
22+
guard let user else {
23+
return [GitHubRepository(message: error)]
24+
}
25+
26+
let ownerRepoRequest: URLRequest
27+
if user.isOrganization {
28+
ownerRepoRequest = GitHubAPI.requestForAllRepositories(org: owner, token: token)
29+
} else {
30+
ownerRepoRequest = GitHubAPI.requestForAllPublicRepositories(user: owner, token: token)
31+
}
2132
var allRepos = await fetchRepositories(request: ownerRepoRequest)
2233
if allRepos.count > 0 && !allRepos[0].isValid {
2334
return allRepos
2435
}
2536

26-
if let token, !token.isEmpty {
27-
let privateRepoRequest = GitHubAPI.requestForPrivateRepositories(token: token)
37+
if !user.isOrganization, let token, !token.isEmpty {
38+
let privateRepoRequest = GitHubAPI.requestForAllPrivateRepositories(token: token)
2839
let privateRepos = await fetchRepositories(request: privateRepoRequest)
2940
if privateRepos.count > 0 && !privateRepos[0].isValid {
3041
return privateRepos
@@ -41,6 +52,28 @@ class GitHubRepositoryList: ObservableObject {
4152
return allRepos
4253
}
4354

55+
private func fetchUser(request: URLRequest) async -> (GitHubUser?, String) {
56+
do {
57+
let (data, response) = try await URLSession.feedSession.data(for: request)
58+
guard let response = response as? HTTPURLResponse else { throw URLError(.unsupportedURL) }
59+
// TODO: Somehow refactor this to use the same code as fetchRepositories
60+
if response.statusCode == 403 || response.statusCode == 429 {
61+
if let v = response.value(forHTTPHeaderField: "x-ratelimit-remaining"), Int(v) == 0 {
62+
// HTTPURLResponse doesn't have a specific message for code 429
63+
return (nil, "too many requests")
64+
} else {
65+
return (nil, HTTPURLResponse.localizedString(forStatusCode: response.statusCode))
66+
}
67+
}
68+
if response.statusCode != 200 {
69+
return (nil, HTTPURLResponse.localizedString(forStatusCode: response.statusCode))
70+
}
71+
return (try JSONDecoder().decode(GitHubUser.self, from: data), "OK")
72+
} catch {
73+
return (nil, error.localizedDescription)
74+
}
75+
}
76+
4477
private func fetchRepositories(request: URLRequest) async -> [GitHubRepository] {
4578
do {
4679
let (data, response) = try await URLSession.feedSession.data(for: request)

CCMenu/Source/Pipeline Window/GitHub Sheets/GitHubSheetModel.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77

88
import Foundation
99

10+
struct GitHubUser: Identifiable, Decodable {
11+
var id: Int
12+
var type: String
13+
14+
var isOrganization: Bool {
15+
return type == "Organization"
16+
}
17+
}
18+
19+
1020
struct GitHubRepository: Identifiable, Hashable, Decodable {
1121

1222
var id: Int

CCMenu/Source/Server Monitor/GitHubAPI.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,25 @@ class GitHubAPI {
1818
return "4eafcf49451c588fbeac"
1919
}
2020

21-
// MARK: - repositories, workflows, and branches
21+
// MARK: - user, repositories, workflows, and branches
2222

23-
static func requestForRepositories(owner: String, token: String?) -> URLRequest {
24-
let path = String(format: "/users/%@/repos", owner)
23+
static func requestForUser(user: String, token: String?) -> URLRequest {
24+
let path = String(format: "/users/%@", user)
25+
return makeRequest(baseUrl: baseURL(forAPI: true), path: path, token: token)
26+
}
27+
28+
static func requestForAllPublicRepositories(user: String, token: String?) -> URLRequest {
29+
let path = String(format: "/users/%@/repos", user)
30+
let queryParams = [
31+
"type": "all",
32+
"sort": "pushed",
33+
"per_page": "100",
34+
];
35+
return makeRequest(baseUrl: baseURL(forAPI: true), path: path, params: queryParams, token: token)
36+
}
37+
38+
static func requestForAllRepositories(org: String, token: String?) -> URLRequest {
39+
let path = String(format: "/orgs/%@/repos", org)
2540
let queryParams = [
2641
"type": "all",
2742
"sort": "pushed",
@@ -30,7 +45,7 @@ class GitHubAPI {
3045
return makeRequest(baseUrl: baseURL(forAPI: true), path: path, params: queryParams, token: token)
3146
}
3247

33-
static func requestForPrivateRepositories(token: String) -> URLRequest {
48+
static func requestForAllPrivateRepositories(token: String) -> URLRequest {
3449
let path = String(format: "/user/repos")
3550
let queryParams = [
3651
"type": "private",
@@ -112,7 +127,7 @@ class GitHubAPI {
112127
return forAPI ? "https://api.github.com" : "https://github.com"
113128
}
114129

115-
private static func makeRequest(method: String = "GET", baseUrl: String, path: String, params: Dictionary<String, String>, token: String? = nil) -> URLRequest {
130+
private static func makeRequest(method: String = "GET", baseUrl: String, path: String, params: Dictionary<String, String> = [:], token: String? = nil) -> URLRequest {
116131
var components = URLComponents(string: baseUrl)!
117132
components.path = path
118133
components.queryItems = params.map({ URLQueryItem(name: $0.key, value: $0.value) })

CCMenuUITests/GitHubTests.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ class GitHubTests: XCTestCase {
8181
}
8282

8383
func testAddsGitHubPipeline() throws {
84+
webapp.router.get("/users/erikdoe") { _ in
85+
try TestHelper.contentsOfFile("GitHubUserResponse.json")
86+
}
8487
webapp.router.get("/users/erikdoe/repos") { _ in
8588
try TestHelper.contentsOfFile("GitHubReposByUserCCM2OnlyResponse.json")
8689
}
@@ -130,6 +133,9 @@ class GitHubTests: XCTestCase {
130133
}
131134

132135
func testAddsGitHubPipelineByIdIfNeccessary() throws {
136+
webapp.router.get("/users/erikdoe") { _ in
137+
try TestHelper.contentsOfFile("GitHubUserResponse.json")
138+
}
133139
webapp.router.get("/users/erikdoe/repos") { _ in
134140
try TestHelper.contentsOfFile("GitHubReposByUserCCM2OnlyResponse.json")
135141
}
@@ -177,6 +183,9 @@ class GitHubTests: XCTestCase {
177183

178184
func testAddsGitHubPipelineWithBranch() throws {
179185
var branchParam: String?
186+
webapp.router.get("/users/erikdoe") { _ in
187+
try TestHelper.contentsOfFile("GitHubUserResponse.json")
188+
}
180189
webapp.router.get("/users/erikdoe/repos") { _ in
181190
try TestHelper.contentsOfFile("GitHubReposByUserCCM2OnlyResponse.json")
182191
}
@@ -223,6 +232,9 @@ class GitHubTests: XCTestCase {
223232
}
224233

225234
func testAddGitHubPipelinePrivateRepos() throws {
235+
webapp.router.get("/users/erikdoe") { _ in
236+
try TestHelper.contentsOfFile("GitHubUserResponse.json")
237+
}
226238
webapp.router.get("/users/erikdoe/repos") { _ in
227239
try TestHelper.contentsOfFile("GitHubReposByUserResponse.json")
228240
}
@@ -263,7 +275,7 @@ class GitHubTests: XCTestCase {
263275
}
264276

265277
func testShowsRateLimitExceededForRepositories() throws {
266-
webapp.router.get("/users/erikdoe/repos", options: .editResponse) { r -> String in
278+
webapp.router.get("/users/erikdoe", options: .editResponse) { r -> String in
267279
r.response.status = .forbidden
268280
r.response.headers.replaceOrAdd(name: "x-ratelimit-remaining", value: "0")
269281
return "{ \"message\": \"API rate limit exceeded for ...\" } "
@@ -289,6 +301,9 @@ class GitHubTests: XCTestCase {
289301

290302
func testDoesntDoubleFetchRepositories() throws {
291303
var fetchCount = 0
304+
webapp.router.get("/users/erikdoe") { _ in
305+
try TestHelper.contentsOfFile("GitHubUserResponse.json")
306+
}
292307
webapp.router.get("/users/erikdoe/repos") { _ in
293308
fetchCount += 1
294309
return try TestHelper.contentsOfFile("GitHubReposByUserResponse.json")
@@ -318,7 +333,6 @@ class GitHubTests: XCTestCase {
318333

319334
// Assert that no further fetch occured
320335
XCTAssertEqual(1, fetchCount)
321-
322336
}
323337

324338
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"login": "thoughtworks",
3+
"id": 18878,
4+
"node_id": "MDEyOk9yZ2FuaXphdGlvbjE4ODc4",
5+
"avatar_url": "https://avatars.githubusercontent.com/u/18878?v=4",
6+
"gravatar_id": "",
7+
"url": "https://api.github.com/users/thoughtworks",
8+
"html_url": "https://github.com/thoughtworks",
9+
"followers_url": "https://api.github.com/users/thoughtworks/followers",
10+
"following_url": "https://api.github.com/users/thoughtworks/following{/other_user}",
11+
"gists_url": "https://api.github.com/users/thoughtworks/gists{/gist_id}",
12+
"starred_url": "https://api.github.com/users/thoughtworks/starred{/owner}{/repo}",
13+
"subscriptions_url": "https://api.github.com/users/thoughtworks/subscriptions",
14+
"organizations_url": "https://api.github.com/users/thoughtworks/orgs",
15+
"repos_url": "https://api.github.com/users/thoughtworks/repos",
16+
"events_url": "https://api.github.com/users/thoughtworks/events{/privacy}",
17+
"received_events_url": "https://api.github.com/users/thoughtworks/received_events",
18+
"type": "Organization",
19+
"user_view_type": "public",
20+
"site_admin": false,
21+
"name": "Thoughtworks",
22+
"company": null,
23+
"blog": "https://thoughtworks.com",
24+
"location": "Global",
25+
"email": null,
26+
"hireable": null,
27+
"bio": "We're a leading global technology consultancy that integrates strategy, design and software engineering to enable our clients to thrive. ",
28+
"twitter_username": "thoughtworks",
29+
"public_repos": 67,
30+
"public_gists": 0,
31+
"followers": 581,
32+
"following": 0,
33+
"created_at": "2008-07-29T21:54:09Z",
34+
"updated_at": "2024-11-25T18:40:42Z"
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"login": "erikdoe",
3+
"id": 954026,
4+
"node_id": "MDQ6VXNlcjk1NDAyNg==",
5+
"avatar_url": "https://avatars.githubusercontent.com/u/954026?v=4",
6+
"gravatar_id": "",
7+
"url": "https://api.github.com/users/erikdoe",
8+
"html_url": "https://github.com/erikdoe",
9+
"followers_url": "https://api.github.com/users/erikdoe/followers",
10+
"following_url": "https://api.github.com/users/erikdoe/following{/other_user}",
11+
"gists_url": "https://api.github.com/users/erikdoe/gists{/gist_id}",
12+
"starred_url": "https://api.github.com/users/erikdoe/starred{/owner}{/repo}",
13+
"subscriptions_url": "https://api.github.com/users/erikdoe/subscriptions",
14+
"organizations_url": "https://api.github.com/users/erikdoe/orgs",
15+
"repos_url": "https://api.github.com/users/erikdoe/repos",
16+
"events_url": "https://api.github.com/users/erikdoe/events{/privacy}",
17+
"received_events_url": "https://api.github.com/users/erikdoe/received_events",
18+
"type": "User",
19+
"user_view_type": "public",
20+
"site_admin": false,
21+
"name": "Erik Doernenburg",
22+
"company": "Thoughtworks",
23+
"blog": "http://erik.doernenburg.com",
24+
"location": "Hamburg, Germany",
25+
"email": null,
26+
"hireable": null,
27+
"bio": null,
28+
"twitter_username": null,
29+
"public_repos": 21,
30+
"public_gists": 6,
31+
"followers": 266,
32+
"following": 0,
33+
"created_at": "2011-08-02T12:34:47Z",
34+
"updated_at": "2025-01-10T21:12:36Z"
35+
}

0 commit comments

Comments
 (0)