/
PullRequest.swift
215 lines (180 loc) · 6.72 KB
/
PullRequest.swift
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import Apollo
import Foundation
import SwiftUI
import UserNotifications
extension PullRequest.Nodes {
mutating func replaceAll(_ newNodes: PullRequest.Nodes) {
replaceSubrange(0 ..< count, with: newNodes)
}
}
func - (left: PullRequest.Nodes, right: PullRequest.Nodes) -> PullRequest.Nodes {
let rightIDs = right.map { $0.id }
return left.filter { !rightIDs.contains($0.id) }
}
enum ReviewResult {
case success
case failure
case pending
}
enum CheckResult {
case success
case failure
case pending
}
class PullRequest: ObservableObject {
struct Node: Identifiable {
let owner: String
let repo: String
let title: String
let url: String
let reviewDecision: String?
let mergeable: String
let state: String?
let commitUrl: String
let success: Bool
let comment: String?
let commentAuthor: String?
let draft: Bool
var id: String {
commitUrl
}
var ownerRepo: String {
"\(owner)/\(repo)"
}
var statusEmoji: String {
success ? "✅" : "❌"
}
}
typealias Nodes = [Node]
private var apollo: ApolloClient?
var githubQuery = Constants.defaultGithubQuery
@Published var nodes: [Node] = []
@Published var pendingNodes: [Node] = []
@Published var updatedAt = "-"
@Published var errorMessage: String = ""
private let dateFmt = {
let dtfmt = DateFormatter()
dtfmt.dateStyle = .none
dtfmt.timeStyle = .short
return dtfmt
}()
func configure(token: String, query: String) {
apollo = {
let cache = InMemoryNormalizedCache()
let store = ApolloStore(cache: cache)
let client = URLSessionClient()
let provider = DefaultInterceptorProvider(client: client, store: store)
let url = URL(string: "https://api.github.com/graphql")!
let transport = RequestChainNetworkTransport(
interceptorProvider: provider,
endpointURL: url,
additionalHeaders: ["Authorization": "Bearer \(token)"]
)
return ApolloClient(networkTransport: transport, store: store)
}()
githubQuery = query
}
private func unwrapError(_ err: Error) -> String {
var errmsg = err.localizedDescription
guard let err = err as? Apollo.ResponseCodeInterceptor.ResponseCodeError else {
return errmsg
}
switch err {
case let .invalidResponseCode(resp, _):
if let resp {
let statusCode = resp.statusCode
let statusMsg = HTTPURLResponse.localizedString(forStatusCode: statusCode)
errmsg = "GitHub API error: \(statusCode) \(statusMsg)"
}
}
return errmsg
}
private func updateNodes(_ value: GraphQLResult<Github.SearchPullRequestsQuery.Data>) {
var fetchedNodes: Nodes = []
var fetchedPendingNodes: Nodes = []
value.data?.search.nodes?.forEach { body in
if let pull = body?.asPullRequest {
let reviewDecision = pull.reviewDecision
let reviewResult: ReviewResult
if reviewDecision == nil || reviewDecision == .approved {
reviewResult = .success
} else if reviewDecision == .changesRequested {
reviewResult = .failure
} else {
reviewResult = .pending
}
guard let commit = pull.commits.nodes?.first??.commit else {
return
}
let state = commit.statusCheckRollup?.state
let checkResult: CheckResult
if state == .success {
checkResult = .success
} else if state == .failure || state == .error {
checkResult = .failure
} else {
checkResult = .pending
}
let node = Node(
owner: pull.repository.owner.login,
repo: pull.repository.name,
title: pull.title,
url: pull.url,
reviewDecision: reviewDecision?.rawValue,
mergeable: pull.mergeable.rawValue,
state: state?.rawValue,
commitUrl: commit.url,
success: reviewResult == .success && checkResult == .success,
comment: pull.comments.nodes?.first??.bodyText,
commentAuthor: pull.comments.nodes?.first??.author?.login,
draft: pull.isDraft
)
if pull.isDraft {
fetchedPendingNodes.append(node)
} else if reviewResult == .pending && checkResult == .pending {
fetchedPendingNodes.append(node)
} else if reviewResult == .success && checkResult == .pending || reviewResult == .pending && checkResult == .success {
fetchedPendingNodes.append(node)
} else {
fetchedNodes.append(node)
}
}
}
let newNodes = fetchedNodes - nodes
nodes.replaceAll(fetchedNodes)
pendingNodes.replaceAll(fetchedPendingNodes)
if newNodes.count > 0 {
notify(newNodes)
}
updatedAt = dateFmt.string(from: Date())
}
private func notify(_ newNodes: Nodes) {
Task {
let userNotificationCenter = UNUserNotificationCenter.current()
for node in newNodes {
let content = UNMutableNotificationContent()
content.title = node.ownerRepo
content.body = node.statusEmoji + node.title
content.userInfo = ["url": node.url]
content.sound = UNNotificationSound.default
let req = UNNotificationRequest(identifier: "jp.winebarrel.Succ.\(node.id)", content: content, trigger: nil)
try? await userNotificationCenter.add(req)
}
}
}
func update(showError: Bool = false) {
errorMessage = ""
let query = Github.SearchPullRequestsQuery(query: githubQuery)
apollo?.fetch(query: query, cachePolicy: .fetchIgnoringCacheCompletely) { result in
switch result {
case let .success(value):
self.updateNodes(value)
case let .failure(error):
AppLogger.shared.debug("failed to search GitHub: \(error)")
if showError {
self.errorMessage = self.unwrapError(error)
}
}
}
}
}