├── .gitignore
├── CommandLineInput.swift
├── Engineer.swift
├── GraphAPIResponse.swift
├── MainMenu.xib
├── MainViewModel+ViewData.swift
├── NSImageView+URL.swift
├── README.md
├── TeamStatus.xcodeproj
├── project.pbxproj
└── project.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── TeamStatus
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ └── statusIcon.imageset
│ │ ├── Contents.json
│ │ ├── brainstorming (1).png
│ │ └── brainstorming.png
├── Info.plist
├── Logger.swift
├── ReviewerCellView.swift
├── ReviewerView.swift
├── SeparatorCellView.swift
└── StatusMenuController.swift
├── TeamStatusCommon
├── Configuration.swift
├── MainViewModel.swift
├── NetworkManager.swift
├── QueryManager.swift
├── Sequence+uniqueElements.swift
├── TeamStatusApp.swift
├── libraries
│ ├── AnyError.swift
│ ├── Result.swift
│ └── ResultProtocol.swift
└── main.swift
├── TeamStatusTests
├── Info.plist
└── TeamStatusTests.swift
├── doc
└── preview.png
└── release
└── TeamStatus.app.zip
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | # Package.pins
40 | .build/
41 |
42 | # CocoaPods
43 | #
44 | # We recommend against adding the Pods directory to your .gitignore. However
45 | # you should judge for yourself, the pros and cons are mentioned at:
46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
47 | #
48 | # Pods/
49 |
50 | # Carthage
51 | #
52 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
53 | # Carthage/Checkouts
54 |
55 | Carthage/Build
56 |
57 | # fastlane
58 | #
59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
60 | # screenshots whenever they are needed.
61 | # For more information about the recommended setup visit:
62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
63 |
64 | fastlane/report.xml
65 | fastlane/Preview.html
66 | fastlane/screenshots
67 | fastlane/test_output
--------------------------------------------------------------------------------
/CommandLineInput.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommandLineInput.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 04/11/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct CommandLineInput {
12 | let repositoryURL: URL
13 | let token: String
14 |
15 | init?() {
16 | guard CommandLine.arguments.count >= 3 else {
17 | Logger.log("Invalid input params.")
18 | return nil
19 | }
20 |
21 | guard
22 | let repositoryURL = URL(string: CommandLine.arguments[1])
23 | else {
24 | Logger.log("First argument needs to be the URL for the git repository.")
25 | return nil
26 | }
27 |
28 | self.repositoryURL = repositoryURL
29 | token = CommandLine.arguments[2]
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Engineer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Engineer.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 12/08/2018.
6 | // Copyright © 2018 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Engineer: Decodable, Hashable {
12 | let login: String
13 | let avatarURL: URL
14 | }
15 |
16 | extension Engineer {
17 | init(viewer: GraphAPIResponse.Data.Viewer) {
18 | self.login = viewer.login
19 | self.avatarURL = viewer.avatarURL
20 | }
21 | }
22 |
23 | extension Engineer {
24 | init(author: GraphAPIResponse.Data.Repository.PullRequests.Edge.Node.Author) {
25 | self.login = author.login
26 | self.avatarURL = author.avatarURL
27 | }
28 | }
29 |
30 | extension Engineer {
31 | init(author: GraphAPIResponse.Data.Repository.PullRequests.Edge.Node.Reviews.Edge.Node.Author) {
32 | self.login = author.login
33 | self.avatarURL = author.avatarURL
34 | }
35 | }
36 |
37 | extension Engineer {
38 | init(requestedReviewer: GraphAPIResponse.Data.Repository.PullRequests.Edge.Node.ReviewRequests.Edge.Node.RequestedReviewer) {
39 | self.login = requestedReviewer.login
40 | self.avatarURL = requestedReviewer.avatarURL
41 | }
42 | }
43 |
44 | extension Engineer {
45 | func PRsToReview(in pullRequests: [GraphAPIResponse.Data.Repository.PullRequests.Edge.Node]) -> [GraphAPIResponse.Data.Repository.PullRequests.Edge.Node] {
46 | return pullRequests.filter({
47 | $0.reviewRequests.edges.contains(where: { $0.node.requestedReviewer.login == login })
48 | })
49 | }
50 |
51 | func PRsReviewed(in pullRequests: [GraphAPIResponse.Data.Repository.PullRequests.Edge.Node]) -> [GraphAPIResponse.Data.Repository.PullRequests.Edge.Node] {
52 | return pullRequests.filter({
53 | $0.reviews.edges.contains(where: { $0.node.author.login == login })
54 | })
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/GraphAPIResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GraphAPIResponse.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 12/08/2018.
6 | // Copyright © 2018 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct GraphAPIResponse: Decodable {
12 | let data: Data
13 | }
14 |
15 | extension GraphAPIResponse {
16 | struct Data: Decodable {
17 | let viewer: Viewer
18 | let repository: Repository
19 | }
20 | }
21 |
22 | extension GraphAPIResponse.Data {
23 | struct Viewer: Decodable {
24 | private enum CodingKeys: String, CodingKey {
25 | case login
26 | case avatarURL = "avatarUrl"
27 | }
28 |
29 | let login: String
30 | let avatarURL: URL
31 | }
32 |
33 | struct Repository: Decodable {
34 | let pullRequests: PullRequests
35 | }
36 | }
37 |
38 | extension GraphAPIResponse.Data.Repository {
39 | struct PullRequests: Decodable {
40 | let edges: [Edge]
41 | }
42 | }
43 |
44 | extension GraphAPIResponse.Data.Repository.PullRequests {
45 | struct Edge: Decodable {
46 | let node: Node
47 | }
48 | }
49 |
50 | extension GraphAPIResponse.Data.Repository.PullRequests.Edge {
51 | struct Node: Decodable {
52 | let title: String
53 | let author: Author
54 | let mergeable: String
55 | let reviewRequests: ReviewRequests
56 | let reviews: Reviews
57 | }
58 | }
59 |
60 | extension GraphAPIResponse.Data.Repository.PullRequests.Edge.Node {
61 | struct Author: Decodable {
62 | private enum CodingKeys: String, CodingKey {
63 | case login
64 | case avatarURL = "avatarUrl"
65 | }
66 |
67 | let login: String
68 | let avatarURL: URL
69 | }
70 | }
71 |
72 | extension GraphAPIResponse.Data.Repository.PullRequests.Edge.Node {
73 | struct ReviewRequests: Decodable {
74 | let edges: [Edge]
75 | }
76 | }
77 |
78 | extension GraphAPIResponse.Data.Repository.PullRequests.Edge.Node.ReviewRequests {
79 | struct Edge: Decodable {
80 | let node: Node
81 | }
82 | }
83 |
84 | extension GraphAPIResponse.Data.Repository.PullRequests.Edge.Node.ReviewRequests.Edge {
85 | struct Node: Decodable {
86 | let requestedReviewer: RequestedReviewer
87 | }
88 | }
89 |
90 | extension GraphAPIResponse.Data.Repository.PullRequests.Edge.Node {
91 | struct Reviews: Decodable {
92 | let edges: [Edge]
93 | }
94 | }
95 |
96 | extension GraphAPIResponse.Data.Repository.PullRequests.Edge.Node.Reviews {
97 | struct Edge: Decodable {
98 | let node: Node
99 | }
100 | }
101 |
102 | extension GraphAPIResponse.Data.Repository.PullRequests.Edge.Node.Reviews.Edge {
103 | struct Node: Decodable {
104 | let author: Author
105 | }
106 | }
107 |
108 | extension GraphAPIResponse.Data.Repository.PullRequests.Edge.Node.Reviews.Edge.Node {
109 | struct Author: Decodable {
110 | private enum CodingKeys: String, CodingKey {
111 | case login
112 | case avatarURL = "avatarUrl"
113 | }
114 |
115 | let login: String
116 | let avatarURL: URL
117 | }
118 | }
119 |
120 | extension GraphAPIResponse.Data.Repository.PullRequests.Edge.Node.ReviewRequests.Edge.Node {
121 | struct RequestedReviewer: Decodable {
122 | private enum CodingKeys: String, CodingKey {
123 | case login
124 | case avatarURL = "avatarUrl"
125 | }
126 |
127 | let login: String
128 | let avatarURL: URL
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/MainMenu.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
282 |
292 |
302 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
--------------------------------------------------------------------------------
/MainViewModel+ViewData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewModel+ViewData.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 04/11/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
--------------------------------------------------------------------------------
/NSImageView+URL.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSImageView+URL.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 01/08/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | extension NSImageView {
12 | public func loadImageFromURL(urlString: String) {
13 | URLSession.shared.dataTask(with: NSURL(string: urlString)! as URL, completionHandler: { data, response, error in
14 | if let error = error {
15 | Logger.log(error.localizedDescription)
16 | return
17 | }
18 |
19 | DispatchQueue.main.async {
20 | self.image = NSImage(data: data!)
21 | }
22 | }).resume()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TeamStatus-for-GitHub
2 | A macOS status bar application for tracking code review process within the team.
3 |
4 |
5 |
6 | ## Download
7 | Prebuilt binary version of the app is available here:
8 | TeamStatus.app.zip.
9 |
10 | ## Configuration
11 | ### 1. Generate personal access token
12 | 1. Sign in to your GitHub account.
13 | 2. Go to Settings -> Developer settings -> Personal access tokens.
14 | 3. Click "Generate new token".
15 | 4. Give it some description and in the scopes select "repo".
16 | 5. Click "Generate token".
17 |
18 | That should create the token that looks like `2d28cf2d28cf2d28cf2d28cf2d28cf2d28cf2d28`.
19 |
20 | ### 2. Run the app
21 |
22 | To run the app execute the following command in terminal:
23 |
24 | ```
25 | open -a TeamStatus.app --args http://urlOfYourGitHubRepository accessToken
26 | ```
27 |
28 | Example:
29 | ```
30 | open -a TeamStatus.app --args https://github.com/yourteam/your-repository-name 2d28cf2d28cf2d28cf2d28cf2d28cf2d28cf2d28
31 | ```
32 |
--------------------------------------------------------------------------------
/TeamStatus.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | DA06BD021EE1C41100E9B261 /* ReviewerCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA06BD011EE1C41100E9B261 /* ReviewerCellView.swift */; };
11 | DA2C73AF2106766D00DCA1A4 /* SeparatorCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2C73AE2106766D00DCA1A4 /* SeparatorCellView.swift */; };
12 | DA46DE081FAE5C32002A202F /* CommandLineInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA46DE061FAE5C32002A202F /* CommandLineInput.swift */; };
13 | DA569E0C1F3127B400C9A8A7 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA569E0A1F31275C00C9A8A7 /* Logger.swift */; };
14 | DA569E0F1F31284300C9A8A7 /* NSImageView+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA569E0E1F31284300C9A8A7 /* NSImageView+URL.swift */; };
15 | DA589A661EDF5647006BFB72 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA589A651EDF5647006BFB72 /* AppDelegate.swift */; };
16 | DA589A6A1EDF5647006BFB72 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DA589A691EDF5647006BFB72 /* Assets.xcassets */; };
17 | DA589A781EDF5648006BFB72 /* TeamStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA589A771EDF5648006BFB72 /* TeamStatusTests.swift */; };
18 | DAB5B5022120AB9700FB53A9 /* Engineer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB5B5002120AB9700FB53A9 /* Engineer.swift */; };
19 | DAB5B5052120ABEF00FB53A9 /* GraphAPIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB5B5032120ABEF00FB53A9 /* GraphAPIResponse.swift */; };
20 | DAF5E8991EDF578D00CFAC81 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = DAF5E8981EDF578D00CFAC81 /* MainMenu.xib */; };
21 | DAF5E89B1EDF5B4B00CFAC81 /* StatusMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF5E89A1EDF5B4B00CFAC81 /* StatusMenuController.swift */; };
22 | DAF5E89D1EDF5D3100CFAC81 /* ReviewerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF5E89C1EDF5D3100CFAC81 /* ReviewerView.swift */; };
23 | DAF7BAE01EFB259500B71AF0 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF7BADF1EFB259500B71AF0 /* NetworkManager.swift */; };
24 | DAF7BAE31EFB25D100B71AF0 /* Sequence+uniqueElements.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF7BAE21EFB25D100B71AF0 /* Sequence+uniqueElements.swift */; };
25 | DAF7BAEE1EFB260000B71AF0 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF7BAEB1EFB260000B71AF0 /* Configuration.swift */; };
26 | DAF7BAF01EFB260000B71AF0 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF7BAEC1EFB260000B71AF0 /* MainViewModel.swift */; };
27 | DAF7BAF21EFB260000B71AF0 /* QueryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF7BAED1EFB260000B71AF0 /* QueryManager.swift */; };
28 | DAF7BAF71EFB266700B71AF0 /* AnyError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF7BAF41EFB266700B71AF0 /* AnyError.swift */; };
29 | DAF7BAF91EFB266700B71AF0 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF7BAF51EFB266700B71AF0 /* Result.swift */; };
30 | DAF7BAFB1EFB266700B71AF0 /* ResultProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF7BAF61EFB266700B71AF0 /* ResultProtocol.swift */; };
31 | /* End PBXBuildFile section */
32 |
33 | /* Begin PBXContainerItemProxy section */
34 | DA589A741EDF5648006BFB72 /* PBXContainerItemProxy */ = {
35 | isa = PBXContainerItemProxy;
36 | containerPortal = DA2394751ED5EB5900F8D325 /* Project object */;
37 | proxyType = 1;
38 | remoteGlobalIDString = DA589A621EDF5647006BFB72;
39 | remoteInfo = TeamStatus;
40 | };
41 | /* End PBXContainerItemProxy section */
42 |
43 | /* Begin PBXFileReference section */
44 | DA06BD011EE1C41100E9B261 /* ReviewerCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReviewerCellView.swift; sourceTree = ""; };
45 | DA2C73AE2106766D00DCA1A4 /* SeparatorCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorCellView.swift; sourceTree = ""; };
46 | DA46DE061FAE5C32002A202F /* CommandLineInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandLineInput.swift; sourceTree = ""; };
47 | DA569E0A1F31275C00C9A8A7 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Logger.swift; path = TeamStatus/Logger.swift; sourceTree = SOURCE_ROOT; };
48 | DA569E0E1F31284300C9A8A7 /* NSImageView+URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSImageView+URL.swift"; sourceTree = SOURCE_ROOT; };
49 | DA589A631EDF5647006BFB72 /* TeamStatus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TeamStatus.app; sourceTree = BUILT_PRODUCTS_DIR; };
50 | DA589A651EDF5647006BFB72 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
51 | DA589A691EDF5647006BFB72 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
52 | DA589A6E1EDF5647006BFB72 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
53 | DA589A731EDF5648006BFB72 /* TeamStatusTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TeamStatusTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
54 | DA589A771EDF5648006BFB72 /* TeamStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamStatusTests.swift; sourceTree = ""; };
55 | DA589A791EDF5648006BFB72 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
56 | DAB5B5002120AB9700FB53A9 /* Engineer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Engineer.swift; sourceTree = ""; };
57 | DAB5B5032120ABEF00FB53A9 /* GraphAPIResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphAPIResponse.swift; sourceTree = ""; };
58 | DAF5E8981EDF578D00CFAC81 /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = MainMenu.xib; path = ../MainMenu.xib; sourceTree = ""; };
59 | DAF5E89A1EDF5B4B00CFAC81 /* StatusMenuController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusMenuController.swift; sourceTree = ""; };
60 | DAF5E89C1EDF5D3100CFAC81 /* ReviewerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReviewerView.swift; sourceTree = ""; };
61 | DAF7BADF1EFB259500B71AF0 /* NetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NetworkManager.swift; path = TeamStatusCommon/NetworkManager.swift; sourceTree = SOURCE_ROOT; };
62 | DAF7BAE21EFB25D100B71AF0 /* Sequence+uniqueElements.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Sequence+uniqueElements.swift"; path = "TeamStatusCommon/Sequence+uniqueElements.swift"; sourceTree = SOURCE_ROOT; };
63 | DAF7BAEB1EFB260000B71AF0 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Configuration.swift; path = TeamStatusCommon/Configuration.swift; sourceTree = SOURCE_ROOT; };
64 | DAF7BAEC1EFB260000B71AF0 /* MainViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MainViewModel.swift; path = TeamStatusCommon/MainViewModel.swift; sourceTree = SOURCE_ROOT; };
65 | DAF7BAED1EFB260000B71AF0 /* QueryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = QueryManager.swift; path = TeamStatusCommon/QueryManager.swift; sourceTree = SOURCE_ROOT; };
66 | DAF7BAF41EFB266700B71AF0 /* AnyError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnyError.swift; path = TeamStatusCommon/libraries/AnyError.swift; sourceTree = SOURCE_ROOT; };
67 | DAF7BAF51EFB266700B71AF0 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Result.swift; path = TeamStatusCommon/libraries/Result.swift; sourceTree = SOURCE_ROOT; };
68 | DAF7BAF61EFB266700B71AF0 /* ResultProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ResultProtocol.swift; path = TeamStatusCommon/libraries/ResultProtocol.swift; sourceTree = SOURCE_ROOT; };
69 | DAF7BAFD1EFB267400B71AF0 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = main.swift; path = TeamStatusCommon/main.swift; sourceTree = SOURCE_ROOT; };
70 | DAF7BB001EFB26C300B71AF0 /* TeamStatusApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TeamStatusApp.swift; path = TeamStatusCommon/TeamStatusApp.swift; sourceTree = SOURCE_ROOT; };
71 | /* End PBXFileReference section */
72 |
73 | /* Begin PBXFrameworksBuildPhase section */
74 | DA589A601EDF5647006BFB72 /* Frameworks */ = {
75 | isa = PBXFrameworksBuildPhase;
76 | buildActionMask = 2147483647;
77 | files = (
78 | );
79 | runOnlyForDeploymentPostprocessing = 0;
80 | };
81 | DA589A701EDF5648006BFB72 /* Frameworks */ = {
82 | isa = PBXFrameworksBuildPhase;
83 | buildActionMask = 2147483647;
84 | files = (
85 | );
86 | runOnlyForDeploymentPostprocessing = 0;
87 | };
88 | /* End PBXFrameworksBuildPhase section */
89 |
90 | /* Begin PBXGroup section */
91 | DA2394741ED5EB5900F8D325 = {
92 | isa = PBXGroup;
93 | children = (
94 | DA23947F1ED5EB5900F8D325 /* TeamStatusCommon */,
95 | DA589A641EDF5647006BFB72 /* TeamStatus */,
96 | DA589A761EDF5648006BFB72 /* TeamStatusTests */,
97 | DA23947E1ED5EB5900F8D325 /* Products */,
98 | );
99 | sourceTree = "";
100 | };
101 | DA23947E1ED5EB5900F8D325 /* Products */ = {
102 | isa = PBXGroup;
103 | children = (
104 | DA589A631EDF5647006BFB72 /* TeamStatus.app */,
105 | DA589A731EDF5648006BFB72 /* TeamStatusTests.xctest */,
106 | );
107 | name = Products;
108 | sourceTree = "";
109 | };
110 | DA23947F1ED5EB5900F8D325 /* TeamStatusCommon */ = {
111 | isa = PBXGroup;
112 | children = (
113 | DA5898DC1EDE17FC006BFB72 /* Extensions */,
114 | DA5898D71EDE17C0006BFB72 /* Models */,
115 | DA23948B1ED5EEF100F8D325 /* Libraries */,
116 | DAF7BAFD1EFB267400B71AF0 /* main.swift */,
117 | DAF7BB001EFB26C300B71AF0 /* TeamStatusApp.swift */,
118 | DAF7BADF1EFB259500B71AF0 /* NetworkManager.swift */,
119 | DAF7BAEB1EFB260000B71AF0 /* Configuration.swift */,
120 | DAF7BAEC1EFB260000B71AF0 /* MainViewModel.swift */,
121 | DAF7BAED1EFB260000B71AF0 /* QueryManager.swift */,
122 | DA569E0A1F31275C00C9A8A7 /* Logger.swift */,
123 | DA46DE061FAE5C32002A202F /* CommandLineInput.swift */,
124 | );
125 | name = TeamStatusCommon;
126 | sourceTree = "";
127 | };
128 | DA23948B1ED5EEF100F8D325 /* Libraries */ = {
129 | isa = PBXGroup;
130 | children = (
131 | DA23948C1ED5EF0100F8D325 /* ResultType */,
132 | );
133 | name = Libraries;
134 | sourceTree = "";
135 | };
136 | DA23948C1ED5EF0100F8D325 /* ResultType */ = {
137 | isa = PBXGroup;
138 | children = (
139 | DAF7BAF41EFB266700B71AF0 /* AnyError.swift */,
140 | DAF7BAF51EFB266700B71AF0 /* Result.swift */,
141 | DAF7BAF61EFB266700B71AF0 /* ResultProtocol.swift */,
142 | );
143 | name = ResultType;
144 | sourceTree = "";
145 | };
146 | DA569E101F312F8C00C9A8A7 /* Cells */ = {
147 | isa = PBXGroup;
148 | children = (
149 | DA06BD011EE1C41100E9B261 /* ReviewerCellView.swift */,
150 | DA2C73AE2106766D00DCA1A4 /* SeparatorCellView.swift */,
151 | );
152 | name = Cells;
153 | sourceTree = "";
154 | };
155 | DA5898D71EDE17C0006BFB72 /* Models */ = {
156 | isa = PBXGroup;
157 | children = (
158 | DAB5B5002120AB9700FB53A9 /* Engineer.swift */,
159 | DAB5B5032120ABEF00FB53A9 /* GraphAPIResponse.swift */,
160 | );
161 | name = Models;
162 | sourceTree = "";
163 | };
164 | DA5898DC1EDE17FC006BFB72 /* Extensions */ = {
165 | isa = PBXGroup;
166 | children = (
167 | DAF7BAE21EFB25D100B71AF0 /* Sequence+uniqueElements.swift */,
168 | DA569E0E1F31284300C9A8A7 /* NSImageView+URL.swift */,
169 | );
170 | name = Extensions;
171 | sourceTree = "";
172 | };
173 | DA589A641EDF5647006BFB72 /* TeamStatus */ = {
174 | isa = PBXGroup;
175 | children = (
176 | DA569E101F312F8C00C9A8A7 /* Cells */,
177 | DA589A651EDF5647006BFB72 /* AppDelegate.swift */,
178 | DA589A691EDF5647006BFB72 /* Assets.xcassets */,
179 | DA589A6E1EDF5647006BFB72 /* Info.plist */,
180 | DAF5E8981EDF578D00CFAC81 /* MainMenu.xib */,
181 | DAF5E89A1EDF5B4B00CFAC81 /* StatusMenuController.swift */,
182 | DAF5E89C1EDF5D3100CFAC81 /* ReviewerView.swift */,
183 | );
184 | path = TeamStatus;
185 | sourceTree = "";
186 | };
187 | DA589A761EDF5648006BFB72 /* TeamStatusTests */ = {
188 | isa = PBXGroup;
189 | children = (
190 | DA589A771EDF5648006BFB72 /* TeamStatusTests.swift */,
191 | DA589A791EDF5648006BFB72 /* Info.plist */,
192 | );
193 | path = TeamStatusTests;
194 | sourceTree = "";
195 | };
196 | /* End PBXGroup section */
197 |
198 | /* Begin PBXNativeTarget section */
199 | DA589A621EDF5647006BFB72 /* TeamStatus */ = {
200 | isa = PBXNativeTarget;
201 | buildConfigurationList = DA589A7E1EDF5648006BFB72 /* Build configuration list for PBXNativeTarget "TeamStatus" */;
202 | buildPhases = (
203 | DA589A5F1EDF5647006BFB72 /* Sources */,
204 | DA589A601EDF5647006BFB72 /* Frameworks */,
205 | DA589A611EDF5647006BFB72 /* Resources */,
206 | );
207 | buildRules = (
208 | );
209 | dependencies = (
210 | );
211 | name = TeamStatus;
212 | productName = TeamStatus;
213 | productReference = DA589A631EDF5647006BFB72 /* TeamStatus.app */;
214 | productType = "com.apple.product-type.application";
215 | };
216 | DA589A721EDF5648006BFB72 /* TeamStatusTests */ = {
217 | isa = PBXNativeTarget;
218 | buildConfigurationList = DA589A7F1EDF5648006BFB72 /* Build configuration list for PBXNativeTarget "TeamStatusTests" */;
219 | buildPhases = (
220 | DA589A6F1EDF5648006BFB72 /* Sources */,
221 | DA589A701EDF5648006BFB72 /* Frameworks */,
222 | DA589A711EDF5648006BFB72 /* Resources */,
223 | );
224 | buildRules = (
225 | );
226 | dependencies = (
227 | DA589A751EDF5648006BFB72 /* PBXTargetDependency */,
228 | );
229 | name = TeamStatusTests;
230 | productName = TeamStatusTests;
231 | productReference = DA589A731EDF5648006BFB72 /* TeamStatusTests.xctest */;
232 | productType = "com.apple.product-type.bundle.unit-test";
233 | };
234 | /* End PBXNativeTarget section */
235 |
236 | /* Begin PBXProject section */
237 | DA2394751ED5EB5900F8D325 /* Project object */ = {
238 | isa = PBXProject;
239 | attributes = {
240 | LastSwiftUpdateCheck = 0830;
241 | LastUpgradeCheck = 0940;
242 | ORGANIZATIONNAME = "Marcin Religa";
243 | TargetAttributes = {
244 | DA589A621EDF5647006BFB72 = {
245 | CreatedOnToolsVersion = 8.3.2;
246 | LastSwiftMigration = 0910;
247 | ProvisioningStyle = Automatic;
248 | };
249 | DA589A721EDF5648006BFB72 = {
250 | CreatedOnToolsVersion = 8.3.2;
251 | LastSwiftMigration = 0910;
252 | ProvisioningStyle = Automatic;
253 | TestTargetID = DA589A621EDF5647006BFB72;
254 | };
255 | };
256 | };
257 | buildConfigurationList = DA2394781ED5EB5900F8D325 /* Build configuration list for PBXProject "TeamStatus" */;
258 | compatibilityVersion = "Xcode 3.2";
259 | developmentRegion = English;
260 | hasScannedForEncodings = 0;
261 | knownRegions = (
262 | en,
263 | Base,
264 | );
265 | mainGroup = DA2394741ED5EB5900F8D325;
266 | productRefGroup = DA23947E1ED5EB5900F8D325 /* Products */;
267 | projectDirPath = "";
268 | projectRoot = "";
269 | targets = (
270 | DA589A621EDF5647006BFB72 /* TeamStatus */,
271 | DA589A721EDF5648006BFB72 /* TeamStatusTests */,
272 | );
273 | };
274 | /* End PBXProject section */
275 |
276 | /* Begin PBXResourcesBuildPhase section */
277 | DA589A611EDF5647006BFB72 /* Resources */ = {
278 | isa = PBXResourcesBuildPhase;
279 | buildActionMask = 2147483647;
280 | files = (
281 | DA589A6A1EDF5647006BFB72 /* Assets.xcassets in Resources */,
282 | DAF5E8991EDF578D00CFAC81 /* MainMenu.xib in Resources */,
283 | );
284 | runOnlyForDeploymentPostprocessing = 0;
285 | };
286 | DA589A711EDF5648006BFB72 /* Resources */ = {
287 | isa = PBXResourcesBuildPhase;
288 | buildActionMask = 2147483647;
289 | files = (
290 | );
291 | runOnlyForDeploymentPostprocessing = 0;
292 | };
293 | /* End PBXResourcesBuildPhase section */
294 |
295 | /* Begin PBXSourcesBuildPhase section */
296 | DA589A5F1EDF5647006BFB72 /* Sources */ = {
297 | isa = PBXSourcesBuildPhase;
298 | buildActionMask = 2147483647;
299 | files = (
300 | DAF7BAF71EFB266700B71AF0 /* AnyError.swift in Sources */,
301 | DAF7BAE31EFB25D100B71AF0 /* Sequence+uniqueElements.swift in Sources */,
302 | DAF7BAFB1EFB266700B71AF0 /* ResultProtocol.swift in Sources */,
303 | DAF7BAF21EFB260000B71AF0 /* QueryManager.swift in Sources */,
304 | DAF7BAF91EFB266700B71AF0 /* Result.swift in Sources */,
305 | DAF7BAEE1EFB260000B71AF0 /* Configuration.swift in Sources */,
306 | DAF5E89B1EDF5B4B00CFAC81 /* StatusMenuController.swift in Sources */,
307 | DA589A661EDF5647006BFB72 /* AppDelegate.swift in Sources */,
308 | DA46DE081FAE5C32002A202F /* CommandLineInput.swift in Sources */,
309 | DAB5B5022120AB9700FB53A9 /* Engineer.swift in Sources */,
310 | DAF5E89D1EDF5D3100CFAC81 /* ReviewerView.swift in Sources */,
311 | DAB5B5052120ABEF00FB53A9 /* GraphAPIResponse.swift in Sources */,
312 | DA569E0C1F3127B400C9A8A7 /* Logger.swift in Sources */,
313 | DA06BD021EE1C41100E9B261 /* ReviewerCellView.swift in Sources */,
314 | DA569E0F1F31284300C9A8A7 /* NSImageView+URL.swift in Sources */,
315 | DAF7BAE01EFB259500B71AF0 /* NetworkManager.swift in Sources */,
316 | DA2C73AF2106766D00DCA1A4 /* SeparatorCellView.swift in Sources */,
317 | DAF7BAF01EFB260000B71AF0 /* MainViewModel.swift in Sources */,
318 | );
319 | runOnlyForDeploymentPostprocessing = 0;
320 | };
321 | DA589A6F1EDF5648006BFB72 /* Sources */ = {
322 | isa = PBXSourcesBuildPhase;
323 | buildActionMask = 2147483647;
324 | files = (
325 | DA589A781EDF5648006BFB72 /* TeamStatusTests.swift in Sources */,
326 | );
327 | runOnlyForDeploymentPostprocessing = 0;
328 | };
329 | /* End PBXSourcesBuildPhase section */
330 |
331 | /* Begin PBXTargetDependency section */
332 | DA589A751EDF5648006BFB72 /* PBXTargetDependency */ = {
333 | isa = PBXTargetDependency;
334 | target = DA589A621EDF5647006BFB72 /* TeamStatus */;
335 | targetProxy = DA589A741EDF5648006BFB72 /* PBXContainerItemProxy */;
336 | };
337 | /* End PBXTargetDependency section */
338 |
339 | /* Begin XCBuildConfiguration section */
340 | DA2394821ED5EB5900F8D325 /* Debug */ = {
341 | isa = XCBuildConfiguration;
342 | buildSettings = {
343 | ALWAYS_SEARCH_USER_PATHS = NO;
344 | CLANG_ANALYZER_NONNULL = YES;
345 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
346 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
347 | CLANG_CXX_LIBRARY = "libc++";
348 | CLANG_ENABLE_MODULES = YES;
349 | CLANG_ENABLE_OBJC_ARC = YES;
350 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
351 | CLANG_WARN_BOOL_CONVERSION = YES;
352 | CLANG_WARN_COMMA = YES;
353 | CLANG_WARN_CONSTANT_CONVERSION = YES;
354 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
355 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
356 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
357 | CLANG_WARN_EMPTY_BODY = YES;
358 | CLANG_WARN_ENUM_CONVERSION = YES;
359 | CLANG_WARN_INFINITE_RECURSION = YES;
360 | CLANG_WARN_INT_CONVERSION = YES;
361 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
362 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
363 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
364 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
365 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
366 | CLANG_WARN_STRICT_PROTOTYPES = YES;
367 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
368 | CLANG_WARN_UNREACHABLE_CODE = YES;
369 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
370 | CODE_SIGN_IDENTITY = "-";
371 | COPY_PHASE_STRIP = NO;
372 | DEBUG_INFORMATION_FORMAT = dwarf;
373 | ENABLE_STRICT_OBJC_MSGSEND = YES;
374 | ENABLE_TESTABILITY = YES;
375 | GCC_C_LANGUAGE_STANDARD = gnu99;
376 | GCC_DYNAMIC_NO_PIC = NO;
377 | GCC_NO_COMMON_BLOCKS = YES;
378 | GCC_OPTIMIZATION_LEVEL = 0;
379 | GCC_PREPROCESSOR_DEFINITIONS = (
380 | "DEBUG=1",
381 | "$(inherited)",
382 | );
383 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
384 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
385 | GCC_WARN_UNDECLARED_SELECTOR = YES;
386 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
387 | GCC_WARN_UNUSED_FUNCTION = YES;
388 | GCC_WARN_UNUSED_VARIABLE = YES;
389 | MACOSX_DEPLOYMENT_TARGET = 10.12;
390 | MTL_ENABLE_DEBUG_INFO = YES;
391 | ONLY_ACTIVE_ARCH = YES;
392 | SDKROOT = macosx;
393 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
394 | SWIFT_VERSION = 4.0;
395 | };
396 | name = Debug;
397 | };
398 | DA2394831ED5EB5900F8D325 /* Release */ = {
399 | isa = XCBuildConfiguration;
400 | buildSettings = {
401 | ALWAYS_SEARCH_USER_PATHS = NO;
402 | CLANG_ANALYZER_NONNULL = YES;
403 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
404 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
405 | CLANG_CXX_LIBRARY = "libc++";
406 | CLANG_ENABLE_MODULES = YES;
407 | CLANG_ENABLE_OBJC_ARC = YES;
408 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
409 | CLANG_WARN_BOOL_CONVERSION = YES;
410 | CLANG_WARN_COMMA = YES;
411 | CLANG_WARN_CONSTANT_CONVERSION = YES;
412 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
413 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
414 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
415 | CLANG_WARN_EMPTY_BODY = YES;
416 | CLANG_WARN_ENUM_CONVERSION = YES;
417 | CLANG_WARN_INFINITE_RECURSION = YES;
418 | CLANG_WARN_INT_CONVERSION = YES;
419 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
420 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
421 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
422 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
423 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
424 | CLANG_WARN_STRICT_PROTOTYPES = YES;
425 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
426 | CLANG_WARN_UNREACHABLE_CODE = YES;
427 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
428 | CODE_SIGN_IDENTITY = "-";
429 | COPY_PHASE_STRIP = NO;
430 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
431 | ENABLE_NS_ASSERTIONS = NO;
432 | ENABLE_STRICT_OBJC_MSGSEND = YES;
433 | GCC_C_LANGUAGE_STANDARD = gnu99;
434 | GCC_NO_COMMON_BLOCKS = YES;
435 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
436 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
437 | GCC_WARN_UNDECLARED_SELECTOR = YES;
438 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
439 | GCC_WARN_UNUSED_FUNCTION = YES;
440 | GCC_WARN_UNUSED_VARIABLE = YES;
441 | MACOSX_DEPLOYMENT_TARGET = 10.12;
442 | MTL_ENABLE_DEBUG_INFO = NO;
443 | SDKROOT = macosx;
444 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
445 | SWIFT_VERSION = 4.0;
446 | };
447 | name = Release;
448 | };
449 | DA589A7A1EDF5648006BFB72 /* Debug */ = {
450 | isa = XCBuildConfiguration;
451 | buildSettings = {
452 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
453 | COMBINE_HIDPI_IMAGES = YES;
454 | INFOPLIST_FILE = TeamStatus/Info.plist;
455 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
456 | PRODUCT_BUNDLE_IDENTIFIER = net.religa.TeamStatus;
457 | PRODUCT_NAME = "$(TARGET_NAME)";
458 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
459 | SWIFT_SWIFT3_OBJC_INFERENCE = Default;
460 | SWIFT_VERSION = 4.0;
461 | };
462 | name = Debug;
463 | };
464 | DA589A7B1EDF5648006BFB72 /* Release */ = {
465 | isa = XCBuildConfiguration;
466 | buildSettings = {
467 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
468 | COMBINE_HIDPI_IMAGES = YES;
469 | INFOPLIST_FILE = TeamStatus/Info.plist;
470 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
471 | PRODUCT_BUNDLE_IDENTIFIER = net.religa.TeamStatus;
472 | PRODUCT_NAME = "$(TARGET_NAME)";
473 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
474 | SWIFT_SWIFT3_OBJC_INFERENCE = Default;
475 | SWIFT_VERSION = 4.0;
476 | };
477 | name = Release;
478 | };
479 | DA589A7C1EDF5648006BFB72 /* Debug */ = {
480 | isa = XCBuildConfiguration;
481 | buildSettings = {
482 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
483 | BUNDLE_LOADER = "$(TEST_HOST)";
484 | COMBINE_HIDPI_IMAGES = YES;
485 | INFOPLIST_FILE = TeamStatusTests/Info.plist;
486 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
487 | PRODUCT_BUNDLE_IDENTIFIER = net.religa.TeamStatusTests;
488 | PRODUCT_NAME = "$(TARGET_NAME)";
489 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
490 | SWIFT_SWIFT3_OBJC_INFERENCE = On;
491 | SWIFT_VERSION = 4.0;
492 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TeamStatus.app/Contents/MacOS/TeamStatus";
493 | };
494 | name = Debug;
495 | };
496 | DA589A7D1EDF5648006BFB72 /* Release */ = {
497 | isa = XCBuildConfiguration;
498 | buildSettings = {
499 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
500 | BUNDLE_LOADER = "$(TEST_HOST)";
501 | COMBINE_HIDPI_IMAGES = YES;
502 | INFOPLIST_FILE = TeamStatusTests/Info.plist;
503 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
504 | PRODUCT_BUNDLE_IDENTIFIER = net.religa.TeamStatusTests;
505 | PRODUCT_NAME = "$(TARGET_NAME)";
506 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
507 | SWIFT_SWIFT3_OBJC_INFERENCE = On;
508 | SWIFT_VERSION = 4.0;
509 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TeamStatus.app/Contents/MacOS/TeamStatus";
510 | };
511 | name = Release;
512 | };
513 | /* End XCBuildConfiguration section */
514 |
515 | /* Begin XCConfigurationList section */
516 | DA2394781ED5EB5900F8D325 /* Build configuration list for PBXProject "TeamStatus" */ = {
517 | isa = XCConfigurationList;
518 | buildConfigurations = (
519 | DA2394821ED5EB5900F8D325 /* Debug */,
520 | DA2394831ED5EB5900F8D325 /* Release */,
521 | );
522 | defaultConfigurationIsVisible = 0;
523 | defaultConfigurationName = Release;
524 | };
525 | DA589A7E1EDF5648006BFB72 /* Build configuration list for PBXNativeTarget "TeamStatus" */ = {
526 | isa = XCConfigurationList;
527 | buildConfigurations = (
528 | DA589A7A1EDF5648006BFB72 /* Debug */,
529 | DA589A7B1EDF5648006BFB72 /* Release */,
530 | );
531 | defaultConfigurationIsVisible = 0;
532 | defaultConfigurationName = Release;
533 | };
534 | DA589A7F1EDF5648006BFB72 /* Build configuration list for PBXNativeTarget "TeamStatusTests" */ = {
535 | isa = XCConfigurationList;
536 | buildConfigurations = (
537 | DA589A7C1EDF5648006BFB72 /* Debug */,
538 | DA589A7D1EDF5648006BFB72 /* Release */,
539 | );
540 | defaultConfigurationIsVisible = 0;
541 | defaultConfigurationName = Release;
542 | };
543 | /* End XCConfigurationList section */
544 | };
545 | rootObject = DA2394751ED5EB5900F8D325 /* Project object */;
546 | }
547 |
--------------------------------------------------------------------------------
/TeamStatus.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/TeamStatus/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 31/05/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | @NSApplicationMain
12 | class AppDelegate: NSObject, NSApplicationDelegate {
13 | func applicationDidFinishLaunching(_ aNotification: Notification) {
14 |
15 | }
16 |
17 | func applicationWillTerminate(_ aNotification: Notification) {
18 | // Insert code here to tear down your application
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/TeamStatus/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "size" : "16x16",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "size" : "16x16",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "size" : "32x32",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "size" : "32x32",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "size" : "128x128",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "size" : "128x128",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "size" : "256x256",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "size" : "256x256",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "size" : "512x512",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "size" : "512x512",
51 | "scale" : "2x"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
--------------------------------------------------------------------------------
/TeamStatus/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/TeamStatus/Assets.xcassets/statusIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "brainstorming (1).png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "brainstorming.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/TeamStatus/Assets.xcassets/statusIcon.imageset/brainstorming (1).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcinreliga/TeamStatus-for-GitHub/6ab1dc08a4612b21c349d29538a065d974adbe2b/TeamStatus/Assets.xcassets/statusIcon.imageset/brainstorming (1).png
--------------------------------------------------------------------------------
/TeamStatus/Assets.xcassets/statusIcon.imageset/brainstorming.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcinreliga/TeamStatus-for-GitHub/6ab1dc08a4612b21c349d29538a065d974adbe2b/TeamStatus/Assets.xcassets/statusIcon.imageset/brainstorming.png
--------------------------------------------------------------------------------
/TeamStatus/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | LSUIElement
26 |
27 | NSHumanReadableCopyright
28 | Copyright © 2017 Marcin Religa. All rights reserved.
29 | NSMainNibFile
30 | MainMenu
31 | NSPrincipalClass
32 | NSApplication
33 |
34 |
35 |
--------------------------------------------------------------------------------
/TeamStatus/Logger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Logger.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 01/08/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class Logger {
12 | static func log(_ message: String) {
13 | print(message)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/TeamStatus/ReviewerCellView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReviewerCellView.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 02/06/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Cocoa
11 |
12 | final class ReviewerCellView: NSTableCellView {
13 | @IBOutlet private var backgroundContainerView: NSView!
14 | @IBOutlet private var containerView: NSView!
15 | @IBOutlet private var imageContainerView: NSView!
16 | @IBOutlet private var loginLabel: NSTextField!
17 | @IBOutlet var pullRequestsReviewedLabel: NSTextField!
18 | @IBOutlet var levelIndicatorContainerView: NSView!
19 | @IBOutlet var levelIndicatorLevelView: NSView!
20 | @IBOutlet var levelIndicatorEmptyView: NSView!
21 | @IBOutlet var levelIndicatorLevelViewWidthConstraint: NSLayoutConstraint!
22 | }
23 |
24 | extension ReviewerCellView {
25 | struct ViewData {
26 | let login: String
27 | let levelIndicator: LevelIndicator
28 | let numberOfReviewedPRs: Int
29 | let totalNumberOfPRs: Int
30 | let avatarURL: URL?
31 |
32 | struct LevelIndicator {
33 | let integerValue: Int
34 | let maxValue: Double
35 | }
36 | }
37 |
38 | func configure(with viewData: ViewData) {
39 | loginLabel.stringValue = viewData.login
40 | containerView.wantsLayer = true
41 |
42 | levelIndicatorContainerView.isHidden = false
43 | levelIndicatorContainerView.wantsLayer = true
44 |
45 | levelIndicatorContainerView.layer?.backgroundColor = NSColor.lightGray.cgColor
46 |
47 | levelIndicatorLevelView.wantsLayer = true
48 | levelIndicatorLevelView.isHidden = false
49 |
50 | levelIndicatorLevelView.layer?.backgroundColor = NSColor(calibratedRed: 126/255.0, green: 200/255.0, blue: 107/255.0, alpha: 1).cgColor
51 |
52 | levelIndicatorEmptyView.wantsLayer = true
53 | levelIndicatorEmptyView.isHidden = false
54 |
55 | levelIndicatorEmptyView.layer?.backgroundColor = NSColor.gray.cgColor
56 |
57 | // levelIndicatorContainerView.layer?.masksToBounds = true
58 | // levelIndicatorContainerView.layer?.cornerRadius = 4
59 |
60 | let range = Double(levelIndicatorEmptyView.frame.width)
61 | let level = range * (Double(viewData.levelIndicator.integerValue) / viewData.levelIndicator.maxValue)
62 |
63 | levelIndicatorLevelViewWidthConstraint.constant = CGFloat(level)
64 |
65 | pullRequestsReviewedLabel.stringValue = "\(viewData.numberOfReviewedPRs) of \(viewData.totalNumberOfPRs)"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/TeamStatus/ReviewerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReviewerView.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 31/05/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class ReviewerView: NSView {
12 | @IBOutlet weak var reviewerLabel: NSTextField!
13 |
14 | override func draw(_ dirtyRect: NSRect) {
15 | super.draw(dirtyRect)
16 |
17 | // self.wantsLayer = true
18 | // self.layer?.backgroundColor = NSColor.red.cgColor
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/TeamStatus/SeparatorCellView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SeparatorCellView.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 23/07/2018.
6 | // Copyright © 2018 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | final class SeparatorCellView: NSTableCellView {
12 | @IBOutlet var titleLabel: NSTextField!
13 | }
14 |
15 | extension SeparatorCellView {
16 | struct ViewData {
17 | let title: String
18 | }
19 |
20 | func configure(with viewData: ViewData) {
21 | titleLabel.stringValue = viewData.title
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/TeamStatus/StatusMenuController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatusMenuController.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 31/05/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class StatusMenuController: NSObject {
12 | @IBOutlet var statusMenu: NSMenu!
13 | @IBOutlet var reviewerView: ReviewerView!
14 | @IBOutlet var tableView: NSTableView!
15 | @IBOutlet var viewerImageView: NSImageView!
16 | @IBOutlet var viewerLogin: NSTextField!
17 | @IBOutlet var myPullRequestsButton: NSButton!
18 | @IBOutlet var awaitingReviewButton: NSButton!
19 | @IBOutlet var reviewedButton: NSButton!
20 |
21 | fileprivate var viewModel: MainViewModel!
22 |
23 | let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
24 |
25 | // TODO: How to do it properly?
26 | var viewDidLoad = false
27 |
28 | fileprivate var viewer: GraphAPIResponse.Data.Viewer?
29 |
30 | override func awakeFromNib() {
31 | guard viewDidLoad == false else {
32 | return
33 | }
34 |
35 | updateStatusIcon()
36 | updateMainView()
37 |
38 | guard let input = CommandLineInput() else {
39 | return
40 | }
41 |
42 | scheduleRefreshing()
43 |
44 | viewModel = MainViewModel(view: self, repositoryURL: input.repositoryURL, token: input.token)
45 | viewModel.run()
46 |
47 | viewDidLoad = true
48 | }
49 |
50 | private func scheduleRefreshing() {
51 | let timer = Timer.scheduledTimer(timeInterval: 60.0, target: self, selector: #selector(self.refresh), userInfo: nil, repeats: true)
52 | RunLoop.main.add(timer, forMode: .commonModes)
53 | }
54 |
55 | private func updateMainView() {
56 | if let reviewerMenuItem = self.statusMenu.item(withTitle: "Reviewer") {
57 | reviewerMenuItem.view = reviewerView
58 | }
59 | }
60 |
61 | private func updateStatusIcon() {
62 | let icon = NSImage(named: NSImage.Name(rawValue: "statusIcon"))
63 | icon?.isTemplate = true // best for dark mode
64 | statusItem.image = icon
65 | statusItem.menu = statusMenu
66 | }
67 |
68 | @objc private func refresh() {
69 | viewModel.run()
70 | }
71 |
72 | @IBAction func quitClicked(sender: NSMenuItem) {
73 | NSApplication.shared.terminate(self)
74 | }
75 |
76 | @IBAction func refreshClicked(sender: NSButton) {
77 | print("refresh")
78 | viewModel.run()
79 | }
80 |
81 | @IBAction func openMyPullRequestsClicked(sender: NSButton) {
82 | viewModel.openMyPullRequests()
83 | statusMenu.cancelTracking()
84 | }
85 |
86 | @IBAction func openAwaitingReviewPullRequestsClicked(sender: NSButton) {
87 | viewModel.openAwaitingReviewPullRequests()
88 | statusMenu.cancelTracking()
89 | }
90 |
91 | @IBAction func openReviewedPullRequestsClicked(sender: NSButton) {
92 | viewModel.openReviewedPullRequests()
93 | statusMenu.cancelTracking()
94 | }
95 |
96 | @IBAction func openAllPullRequestsClicked(sender: NSButton) {
97 | viewModel.openAllPullRequests()
98 | statusMenu.cancelTracking()
99 | }
100 | }
101 |
102 | extension StatusMenuController: MainViewProtocol {
103 | func didFinishRunning(reviewers: [Engineer], viewer: GraphAPIResponse.Data.Viewer?) {
104 | self.viewer = viewer
105 |
106 | self.tableView.reloadData()
107 | }
108 |
109 | func didFailToRun() {
110 | Logger.log("Did fail to run")
111 | }
112 |
113 | func updateStatusItem(title: String, isAttentionNeeded: Bool) {
114 | let titleToSet: String
115 | if isAttentionNeeded {
116 | titleToSet = "\(title) ⚠️"
117 | } else {
118 | titleToSet = title
119 | }
120 |
121 | statusItem.title = titleToSet
122 | }
123 |
124 | func updateViewerView(with engineer: Engineer, ownPullRequestsCount: Int, pullRequestsToReviewCount: Int, pullRequestsReviewed: Int) {
125 | viewerLogin.stringValue = engineer.login
126 | viewerImageView?.loadImageFromURL(urlString: engineer.avatarURL.absoluteString)
127 |
128 | myPullRequestsButton.title = "my (\(ownPullRequestsCount))"
129 | awaitingReviewButton.title = "awaiting (\(pullRequestsToReviewCount))"
130 | reviewedButton.title = "reviewed (\(pullRequestsReviewed))"
131 | }
132 | }
133 |
134 | extension StatusMenuController: NSTableViewDataSource {
135 | func numberOfRows(in tableView: NSTableView) -> Int {
136 | // Because of separators.
137 | // TODO: define sections (row types) properly.
138 | return viewModel.reviewersSorted.count + 2
139 | }
140 | }
141 |
142 | extension StatusMenuController: NSTableViewDelegate {
143 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
144 | guard let tableColumn = tableColumn else {
145 | fatalError()
146 | }
147 |
148 | switch tableColumn.identifier {
149 | case NSUserInterfaceItemIdentifier("UserLoginTableColumn"):
150 | guard viewModel.isSeparator(at: row) == false else {
151 | guard let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: String(describing: SeparatorCellView.self)), owner: self) as? SeparatorCellView else {
152 | fatalError()
153 | }
154 |
155 | let viewData = viewModel.viewDataForSeparator(at: row)
156 | cell.configure(with: viewData)
157 | return cell
158 | }
159 |
160 | guard let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: String(describing: ReviewerCellView.self)), owner: self) as? ReviewerCellView else {
161 | fatalError()
162 | }
163 |
164 | let viewData = viewModel.viewDataForUserLoginCell(at: row)
165 | cell.configure(with: viewData)
166 |
167 | if let imageURL = viewData.avatarURL {
168 | cell.imageView?.loadImageFromURL(urlString: imageURL.absoluteString)
169 | }
170 | return cell
171 | default:
172 | fatalError()
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/TeamStatusCommon/Configuration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Configuration.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 31/05/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum Configuration {
12 | static let apiBaseURL = URL(string: "https://api.github.com/graphql")!
13 | }
14 |
--------------------------------------------------------------------------------
/TeamStatusCommon/MainViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewModel.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 31/05/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol MainViewProtocol {
12 | func didFinishRunning(reviewers: [Engineer], viewer: GraphAPIResponse.Data.Viewer?)
13 | func didFailToRun()
14 | func updateStatusItem(title: String, isAttentionNeeded: Bool)
15 | func updateViewerView(with engineer: Engineer, ownPullRequestsCount: Int, pullRequestsToReviewCount: Int, pullRequestsReviewed: Int)
16 | }
17 |
18 | final class MainViewModel {
19 | private let view: MainViewProtocol
20 | private let queryManager: QueryManager = QueryManager()
21 | private let networkManager: NetworkManager
22 |
23 | var reviewersSorted: [Engineer] = []
24 | private var pullRequests: [GraphAPIResponse.Data.Repository.PullRequests.Edge.Node] = []
25 | private var viewer: GraphAPIResponse.Data.Viewer?
26 | private var repositoryURL: URL
27 |
28 | init(view: MainViewProtocol, repositoryURL: URL, token: String) {
29 | self.view = view
30 | self.repositoryURL = repositoryURL
31 | self.networkManager = NetworkManager(apiBaseURL: Configuration.apiBaseURL, token: token)
32 | }
33 |
34 | func run() {
35 | guard let query = queryManager.allPullRequestsQuery else {
36 | return Logger.log("Query is empty.")
37 | }
38 |
39 | networkManager.query(query) { [weak self] result in
40 | guard let _self = self else {
41 | return
42 | }
43 |
44 | switch result {
45 | case .success(let data):
46 | do {
47 | let graphAPIResponse = try JSONDecoder().decode(GraphAPIResponse.self, from: data)
48 |
49 | let reviewersRequested = graphAPIResponse.data.repository.pullRequests.edges.map({
50 | $0.node.reviewRequests.edges.map({ $0.node.requestedReviewer })
51 | }).flatMap({ $0 })
52 |
53 | let reviewersReviewed = graphAPIResponse.data.repository.pullRequests.edges.map({
54 | $0.node.reviews.edges.map({ $0.node.author })
55 | }).flatMap({ $0 })
56 |
57 | let allEngineers = reviewersRequested.map({ Engineer(requestedReviewer: $0) }) + reviewersReviewed.map({ Engineer(author: $0) })
58 |
59 | _self.queryOpenPullRequests(involving: allEngineers.uniqueElements)
60 | } catch {
61 | print("JSON parsing error: \(error)")
62 | }
63 | case .failure:
64 | print("Failed to get all pull requests data.")
65 | }
66 | }
67 | }
68 |
69 | private func queryOpenPullRequests(involving engineers: [Engineer]) {
70 | guard let query = queryManager.openPullRequestsQuery else {
71 | return Logger.log("Query is empty.")
72 | }
73 |
74 | networkManager.query(query) { [weak self] result in
75 | guard let _self = self else {
76 | return
77 | }
78 | switch result {
79 | case .success(let data):
80 | do {
81 | let graphAPIResponse = try JSONDecoder().decode(GraphAPIResponse.self, from: data)
82 |
83 | _self.viewer = graphAPIResponse.data.viewer
84 | _self.pullRequests = graphAPIResponse.data.repository.pullRequests.edges.map({ $0.node })
85 |
86 | _self.reviewersSorted = engineers.sorted(by: { a, b in
87 | a.PRsToReview(in: _self.pullRequests).count < b.PRsToReview(in: _self.pullRequests).count
88 | })
89 |
90 | if let viewer = _self.viewer {
91 | let engineer = Engineer(viewer: viewer)
92 | let pullRequestsCount = _self.pullRequestsToReviewCount(for: engineer, in: _self.pullRequests)
93 | let isAttentionNeeded = _self.hasAnyConflicts(for: viewer, in: _self.pullRequests)
94 | let ownPullRequestsCount = _self.numberOfPullRequests(for: viewer, in: _self.pullRequests)
95 | let pullRequestsReviewedCount = _self.numberOfPullRequestsReviewed(by: viewer, in: _self.pullRequests)
96 |
97 | DispatchQueue.main.async {
98 | // TODO: This can be merged into single call.
99 | _self.view.updateStatusItem(title: "\(pullRequestsCount)", isAttentionNeeded: isAttentionNeeded)
100 | _self.view.updateViewerView(
101 | with: engineer,
102 | ownPullRequestsCount: ownPullRequestsCount,
103 | pullRequestsToReviewCount: pullRequestsCount,
104 | pullRequestsReviewed: pullRequestsReviewedCount
105 | )
106 | }
107 | }
108 |
109 | DispatchQueue.main.async {
110 | _self.view.didFinishRunning(reviewers: _self.reviewersSorted, viewer: _self.viewer)
111 | }
112 | } catch {
113 | print("JSON parsing error: \(error)")
114 | }
115 | case .failure:
116 | DispatchQueue.main.async {
117 | _self.view.didFailToRun()
118 | }
119 | }
120 | }
121 | }
122 |
123 | func pullRequestsToReviewCount(for engineer: Engineer, in pullRequests: [GraphAPIResponse.Data.Repository.PullRequests.Edge.Node]) -> Int {
124 | return engineer.PRsToReview(in: pullRequests).count
125 | }
126 |
127 | func hasAnyConflicts(for viewer: GraphAPIResponse.Data.Viewer, in pullRequests: [GraphAPIResponse.Data.Repository.PullRequests.Edge.Node]) -> Bool {
128 | return pullRequests.first(where: { $0.mergeable == "CONFLICTING" && $0.author.login == viewer.login }) != nil
129 | }
130 |
131 | func numberOfPullRequests(for viewer: GraphAPIResponse.Data.Viewer, in pullRequests: [GraphAPIResponse.Data.Repository.PullRequests.Edge.Node]) -> Int {
132 | return pullRequests.filter({ $0.author.login == viewer.login }).count
133 | }
134 |
135 | func numberOfPullRequestsReviewed(by viewer: GraphAPIResponse.Data.Viewer, in pullRequests: [GraphAPIResponse.Data.Repository.PullRequests.Edge.Node]) -> Int {
136 | return pullRequests.filter({ $0.reviews.edges.contains(where: { $0.node.author.login == viewer.login }) }).count
137 | }
138 |
139 | private var viewDataForReviewer: [ReviewerCellView.ViewData] {
140 | return reviewersSorted.map({
141 | viewData(for: $0)
142 | })
143 | }
144 |
145 | func viewData(for engineer: Engineer) -> ReviewerCellView.ViewData {
146 | let prsToReview = engineer.PRsToReview(in: pullRequests).count
147 | let prsReviewed = engineer.PRsReviewed(in: pullRequests).count
148 | let totalPRs = prsToReview + prsReviewed
149 |
150 | // If total is 0 then set both integer and max to 1 so the bar is full green.
151 | let levelIndicatorViewData = ReviewerCellView.ViewData.LevelIndicator(
152 | integerValue: totalPRs == 0 ? 1 : prsReviewed,
153 | maxValue: totalPRs == 0 ? 1 : Double(totalPRs)
154 | )
155 |
156 | return ReviewerCellView.ViewData(
157 | login: engineer.login,
158 | levelIndicator: levelIndicatorViewData,
159 | numberOfReviewedPRs: prsReviewed,
160 | totalNumberOfPRs: totalPRs,
161 | avatarURL: engineer.avatarURL
162 | )
163 | }
164 |
165 | // FIXME: Should not use UIKit subclasses.
166 | func viewDataForUserLoginCell(at rowIndex: Int) -> ReviewerCellView.ViewData {
167 | return viewDataForReviewer[reviewerIndexFor(row: rowIndex)]
168 | }
169 |
170 | func viewDataForSeparator(at rowIndex: Int) -> SeparatorCellView.ViewData {
171 | if rowIndex == 0 {
172 | return SeparatorCellView.ViewData(title: "available for review")
173 | } else if rowIndex == numberOfAvailableReviewers + 1 {
174 | return SeparatorCellView.ViewData(title: "others")
175 | } else {
176 | fatalError()
177 | }
178 | }
179 |
180 | private func reviewerIndexFor(row: Int) -> Int {
181 | if row <= numberOfAvailableReviewers {
182 | return row - 1
183 | } else {
184 | return row - 2
185 | }
186 | }
187 |
188 | func isSeparator(at rowIndex: Int) -> Bool {
189 | return rowIndex == 0 || rowIndex == numberOfAvailableReviewers + 1
190 | }
191 |
192 | private var numberOfAvailableReviewers: Int {
193 | return viewDataForReviewer.filter({ $0.numberOfReviewedPRs == $0.totalNumberOfPRs }).count
194 | }
195 |
196 | func openMyPullRequests() {
197 | guard
198 | let viewer = viewer,
199 | let url = URL(string: "\(repositoryURL.absoluteString)/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc+author%3A\(viewer.login)")
200 | else {
201 | return
202 | }
203 |
204 | openBrowser(with: url)
205 | }
206 |
207 | func openAwaitingReviewPullRequests() {
208 | guard
209 | let viewer = viewer,
210 | let url = URL(string: "\(repositoryURL.absoluteString)/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc+review-requested%3A\(viewer.login)")
211 | else {
212 | return
213 | }
214 |
215 | openBrowser(with: url)
216 | }
217 |
218 | func openReviewedPullRequests() {
219 | guard
220 | let viewer = viewer,
221 | let url = URL(string: "\(repositoryURL.absoluteString)/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc+reviewed-by%3A\(viewer.login)")
222 | else {
223 | return
224 | }
225 |
226 | openBrowser(with: url)
227 | }
228 |
229 | func openAllPullRequests() {
230 | openMyPullRequests()
231 | openAwaitingReviewPullRequests()
232 | openReviewedPullRequests()
233 | }
234 |
235 | private func openBrowser(with url: URL) {
236 | Process.launchedProcess(launchPath: "/usr/bin/open", arguments: [url.absoluteString])
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/TeamStatusCommon/NetworkManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkManager.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 24/05/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum NetworkError: Error {
12 | case didFailToFetchData
13 | }
14 |
15 | final class NetworkManager {
16 | private let apiBaseURL: URL
17 | private let token: String
18 |
19 | init(apiBaseURL: URL, token: String) {
20 | self.apiBaseURL = apiBaseURL
21 | self.token = token
22 | }
23 |
24 | func query(_ query: String, completion: @escaping (Result) -> Void) {
25 | var request = URLRequest(url: apiBaseURL)
26 | request.httpMethod = "POST"
27 | request.addValue("bearer \(token)", forHTTPHeaderField: "Authorization")
28 | request.httpBody = query.data(using: .utf8)
29 |
30 | // FIXME: This is workaround for GitHub API issues.
31 | // https://platform.github.community/t/executing-a-request-again-results-in-412-precondition-failed/1456/9
32 | let config = URLSessionConfiguration.default
33 | config.requestCachePolicy = .reloadIgnoringLocalCacheData
34 | config.urlCache = nil
35 | let session = URLSession.init(configuration: config)
36 |
37 | let task = session.dataTask(with: request) { data, response, error in
38 | guard let data = data, error == nil else {
39 | completion(.failure(NetworkError.didFailToFetchData))
40 | return
41 | }
42 |
43 | completion(.success(data))
44 | }
45 | task.resume()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/TeamStatusCommon/QueryManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QueryManager.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 31/05/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class QueryManager {
12 | private var repositoryPathComponents: [String]? {
13 | guard
14 | let input = CommandLineInput(),
15 | let path = URLComponents(url: input.repositoryURL, resolvingAgainstBaseURL: false)?.path
16 | else {
17 | return nil
18 | }
19 |
20 | return path.split(separator: "/").map({ String($0) })
21 | }
22 |
23 | private var repositoryName: String? {
24 | guard
25 | let pathComponents = repositoryPathComponents,
26 | let repositoryName = pathComponents.last
27 | else {
28 | return nil
29 | }
30 |
31 | return String(repositoryName)
32 | }
33 |
34 | private var teamName: String? {
35 | guard
36 | let pathComponents = repositoryPathComponents,
37 | let teamName = pathComponents.first
38 | else {
39 | return nil
40 | }
41 |
42 | return String(teamName)
43 | }
44 |
45 | var openPullRequestsQuery: String? {
46 | guard
47 | let repositoryName = repositoryName,
48 | let teamName = teamName
49 | else {
50 | return nil
51 | }
52 |
53 | return "{\"query\": \"query { rateLimit { cost limit remaining resetAt } viewer { login avatarUrl } repository(owner: \\\"\(teamName)\\\", name: \\\"\(repositoryName)\\\") { url pullRequests(last: 30, states: OPEN) { edges { node { title author { login avatarUrl } updatedAt mergeable reviews(first: 100) { edges { node { author { login avatarUrl } } } }, reviewRequests(first: 100) { edges { node { requestedReviewer { ... on User { login avatarUrl } } } } } } } } }}\" }"
54 | }
55 |
56 | var allPullRequestsQuery: String? {
57 | guard
58 | let repositoryName = repositoryName,
59 | let teamName = teamName
60 | else {
61 | return nil
62 | }
63 |
64 | return "{\"query\": \"query { rateLimit { cost limit remaining resetAt } viewer { login avatarUrl } repository(owner: \\\"\(teamName)\\\", name: \\\"\(repositoryName)\\\") { url pullRequests(last: 100, states: [OPEN, MERGED]) { edges { node { title author { login avatarUrl } updatedAt mergeable reviews(first: 100) { edges { node { author { login avatarUrl } } } }, reviewRequests(first: 100) { edges { node { requestedReviewer { ... on User { login avatarUrl } } } } } } } } }}\" }"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/TeamStatusCommon/Sequence+uniqueElements.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sequence.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 30/05/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension Sequence where Iterator.Element: Hashable {
12 | var uniqueElements: [Iterator.Element] {
13 | return Array(Set(self))
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/TeamStatusCommon/TeamStatusApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamStatusApp.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 24/05/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | final class TeamStatusApp {
12 | private var viewModel: MainViewModel!
13 | fileprivate var semaphore: DispatchSemaphore?
14 |
15 | func run() {
16 | switch CommandLine.arguments.count {
17 | case 1:
18 | print("Error: Authorization token is required.")
19 | default:
20 | guard let input = CommandLineInput() else {
21 | return
22 | }
23 | viewModel = MainViewModel(view: self, repositoryURL: input.repositoryURL, token: input.token)
24 | semaphore = DispatchSemaphore(value: 0)
25 | viewModel.run()
26 | semaphore?.wait()
27 | }
28 | }
29 |
30 | fileprivate func finish() {
31 | semaphore?.signal()
32 | }
33 | }
34 |
35 | extension TeamStatusApp: MainViewProtocol {
36 | func didFinishRunning(reviewers: [Engineer], pullRequests: [PullRequest], viewer: GraphAPIResponse.Data.Viewer?) {
37 | for reviewer in reviewers {
38 | let pullRequestsReviewRequested = reviewer.PRsToReview(in: pullRequests)
39 | let pullRequestsReviewed = reviewer.PRsReviewed(in: pullRequests)
40 | let login = reviewer.login.padding(toLength: 20, withPad: " ", startingAt: 0)
41 | print("\(login) requested in: \(pullRequestsReviewRequested.count), reviewed: \(pullRequestsReviewed.count)")
42 | }
43 | print("")
44 |
45 | finish()
46 | }
47 |
48 | func didFailToRun() {
49 | finish()
50 | }
51 |
52 | // TODO: This needs to be decoupled with view model used by Mac UI app.
53 | func updateStatusItem(title: String, isAttentionNeeded: Bool) {}
54 |
55 | func updateViewerView(with engineer: Engineer, ownPullRequestsCount: Int, pullRequestsToReviewCount: Int, pullRequestsReviewed: Int) {}
56 | }
57 |
--------------------------------------------------------------------------------
/TeamStatusCommon/libraries/AnyError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A type-erased error which wraps an arbitrary error instance. This should be
4 | /// useful for generic contexts.
5 | public struct AnyError: Swift.Error {
6 | /// The underlying error.
7 | public let error: Swift.Error
8 |
9 | public init(_ error: Swift.Error) {
10 | if let anyError = error as? AnyError {
11 | self = anyError
12 | } else {
13 | self.error = error
14 | }
15 | }
16 | }
17 |
18 | extension AnyError: ErrorProtocolConvertible {
19 | public static func error(from error: Error) -> AnyError {
20 | return AnyError(error)
21 | }
22 | }
23 |
24 | extension AnyError: CustomStringConvertible {
25 | public var description: String {
26 | return String(describing: error)
27 | }
28 | }
29 |
30 | extension AnyError: LocalizedError {
31 | public var errorDescription: String? {
32 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
33 | return error.localizedDescription
34 | #else
35 | #if swift(>=4.0)
36 | // The workaround below is not needed for Swift 4.0 thanks to
37 | // https://github.com/apple/swift-corelibs-foundation/pull/967.
38 | #else
39 | if let nsError = error as? NSError {
40 | return nsError.localizedDescription
41 | }
42 | #endif
43 | return error.localizedDescription
44 | #endif
45 | }
46 |
47 | public var failureReason: String? {
48 | return (error as? LocalizedError)?.failureReason
49 | }
50 |
51 | public var helpAnchor: String? {
52 | return (error as? LocalizedError)?.helpAnchor
53 | }
54 |
55 | public var recoverySuggestion: String? {
56 | return (error as? LocalizedError)?.recoverySuggestion
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/TeamStatusCommon/libraries/Result.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015 Rob Rix. All rights reserved.
2 |
3 | /// An enum representing either a failure with an explanatory error, or a success with a result value.
4 | public enum Result: ResultProtocol, CustomStringConvertible, CustomDebugStringConvertible {
5 | case success(T)
6 | case failure(Error)
7 |
8 | // MARK: Constructors
9 |
10 | /// Constructs a success wrapping a `value`.
11 | public init(value: T) {
12 | self = .success(value)
13 | }
14 |
15 | /// Constructs a failure wrapping an `error`.
16 | public init(error: Error) {
17 | self = .failure(error)
18 | }
19 |
20 | /// Constructs a result from an `Optional`, failing with `Error` if `nil`.
21 | public init(_ value: T?, failWith: @autoclosure () -> Error) {
22 | self = value.map(Result.success) ?? .failure(failWith())
23 | }
24 |
25 | /// Constructs a result from a function that uses `throw`, failing with `Error` if throws.
26 | public init(_ f: @autoclosure () throws -> T) {
27 | self.init(attempt: f)
28 | }
29 |
30 | /// Constructs a result from a function that uses `throw`, failing with `Error` if throws.
31 | public init(attempt f: () throws -> T) {
32 | do {
33 | self = .success(try f())
34 | } catch var error {
35 | if Error.self == AnyError.self {
36 | error = AnyError(error)
37 | }
38 | self = .failure(error as! Error)
39 | }
40 | }
41 |
42 | // MARK: Deconstruction
43 |
44 | /// Returns the value from `success` Results or `throw`s the error.
45 | public func dematerialize() throws -> T {
46 | switch self {
47 | case let .success(value):
48 | return value
49 | case let .failure(error):
50 | throw error
51 | }
52 | }
53 |
54 | /// Case analysis for Result.
55 | ///
56 | /// Returns the value produced by applying `ifFailure` to `failure` Results, or `ifSuccess` to `success` Results.
57 | public func analysis(ifSuccess: (T) -> Result, ifFailure: (Error) -> Result) -> Result {
58 | switch self {
59 | case let .success(value):
60 | return ifSuccess(value)
61 | case let .failure(value):
62 | return ifFailure(value)
63 | }
64 | }
65 |
66 | // MARK: Errors
67 |
68 | /// The domain for errors constructed by Result.
69 | public static var errorDomain: String { return "com.antitypical.Result" }
70 |
71 | /// The userInfo key for source functions in errors constructed by Result.
72 | public static var functionKey: String { return "\(errorDomain).function" }
73 |
74 | /// The userInfo key for source file paths in errors constructed by Result.
75 | public static var fileKey: String { return "\(errorDomain).file" }
76 |
77 | /// The userInfo key for source file line numbers in errors constructed by Result.
78 | public static var lineKey: String { return "\(errorDomain).line" }
79 |
80 | /// Constructs an error.
81 | public static func error(_ message: String? = nil, function: String = #function, file: String = #file, line: Int = #line) -> NSError {
82 | var userInfo: [String: Any] = [
83 | functionKey: function,
84 | fileKey: file,
85 | lineKey: line,
86 | ]
87 |
88 | if let message = message {
89 | userInfo[NSLocalizedDescriptionKey] = message
90 | }
91 |
92 | return NSError(domain: errorDomain, code: 0, userInfo: userInfo)
93 | }
94 |
95 |
96 | // MARK: CustomStringConvertible
97 |
98 | public var description: String {
99 | return analysis(
100 | ifSuccess: { ".success(\($0))" },
101 | ifFailure: { ".failure(\($0))" })
102 | }
103 |
104 |
105 | // MARK: CustomDebugStringConvertible
106 |
107 | public var debugDescription: String {
108 | return description
109 | }
110 | }
111 |
112 | // MARK: - Derive result from failable closure
113 |
114 | public func materialize(_ f: () throws -> T) -> Result {
115 | return materialize(try f())
116 | }
117 |
118 | public func materialize(_ f: @autoclosure () throws -> T) -> Result {
119 | do {
120 | return .success(try f())
121 | } catch {
122 | return .failure(AnyError(error))
123 | }
124 | }
125 |
126 | // MARK: - Cocoa API conveniences
127 |
128 | #if !os(Linux)
129 |
130 | /// Constructs a `Result` with the result of calling `try` with an error pointer.
131 | ///
132 | /// This is convenient for wrapping Cocoa API which returns an object or `nil` + an error, by reference. e.g.:
133 | ///
134 | /// Result.try { NSData(contentsOfURL: URL, options: .dataReadingMapped, error: $0) }
135 | public func `try`(_ function: String = #function, file: String = #file, line: Int = #line, `try`: (NSErrorPointer) -> T?) -> Result {
136 | var error: NSError?
137 | return `try`(&error).map(Result.success) ?? .failure(error ?? Result.error(function: function, file: file, line: line))
138 | }
139 |
140 | /// Constructs a `Result` with the result of calling `try` with an error pointer.
141 | ///
142 | /// This is convenient for wrapping Cocoa API which returns a `Bool` + an error, by reference. e.g.:
143 | ///
144 | /// Result.try { NSFileManager.defaultManager().removeItemAtURL(URL, error: $0) }
145 | public func `try`(_ function: String = #function, file: String = #file, line: Int = #line, `try`: (NSErrorPointer) -> Bool) -> Result<(), NSError> {
146 | var error: NSError?
147 | return `try`(&error) ?
148 | .success(())
149 | : .failure(error ?? Result<(), NSError>.error(function: function, file: file, line: line))
150 | }
151 |
152 | #endif
153 |
154 | // MARK: - ErrorProtocolConvertible conformance
155 |
156 | extension NSError: ErrorProtocolConvertible {
157 | public static func error(from error: Swift.Error) -> Self {
158 | func cast(_ error: Swift.Error) -> T {
159 | return error as! T
160 | }
161 |
162 | return cast(error)
163 | }
164 | }
165 |
166 | // MARK: - migration support
167 |
168 | @available(*, unavailable, message: "Use the overload which returns `Result` instead")
169 | public func materialize(_ f: () throws -> T) -> Result {
170 | fatalError()
171 | }
172 |
173 | @available(*, unavailable, message: "Use the overload which returns `Result` instead")
174 | public func materialize(_ f: @autoclosure () throws -> T) -> Result {
175 | fatalError()
176 | }
177 |
178 | // MARK: -
179 |
180 | import Foundation
181 |
--------------------------------------------------------------------------------
/TeamStatusCommon/libraries/ResultProtocol.swift:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2015 Rob Rix. All rights reserved.
2 |
3 | /// A type that can represent either failure with an error or success with a result value.
4 | public protocol ResultProtocol {
5 | associatedtype Value
6 | associatedtype Error: Swift.Error
7 |
8 | /// Constructs a successful result wrapping a `value`.
9 | init(value: Value)
10 |
11 | /// Constructs a failed result wrapping an `error`.
12 | init(error: Error)
13 |
14 | /// Case analysis for ResultProtocol.
15 | ///
16 | /// Returns the value produced by appliying `ifFailure` to the error if self represents a failure, or `ifSuccess` to the result value if self represents a success.
17 | func analysis(ifSuccess: (Value) -> U, ifFailure: (Error) -> U) -> U
18 |
19 | /// Returns the value if self represents a success, `nil` otherwise.
20 | ///
21 | /// A default implementation is provided by a protocol extension. Conforming types may specialize it.
22 | var value: Value? { get }
23 |
24 | /// Returns the error if self represents a failure, `nil` otherwise.
25 | ///
26 | /// A default implementation is provided by a protocol extension. Conforming types may specialize it.
27 | var error: Error? { get }
28 | }
29 |
30 | public extension ResultProtocol {
31 |
32 | /// Returns the value if self represents a success, `nil` otherwise.
33 | public var value: Value? {
34 | return analysis(ifSuccess: { $0 }, ifFailure: { _ in nil })
35 | }
36 |
37 | /// Returns the error if self represents a failure, `nil` otherwise.
38 | public var error: Error? {
39 | return analysis(ifSuccess: { _ in nil }, ifFailure: { $0 })
40 | }
41 |
42 | /// Returns a new Result by mapping `Success`es’ values using `transform`, or re-wrapping `Failure`s’ errors.
43 | public func map(_ transform: (Value) -> U) -> Result {
44 | return flatMap { .success(transform($0)) }
45 | }
46 |
47 | /// Returns the result of applying `transform` to `Success`es’ values, or re-wrapping `Failure`’s errors.
48 | public func flatMap(_ transform: (Value) -> Result) -> Result {
49 | return analysis(
50 | ifSuccess: transform,
51 | ifFailure: Result.failure)
52 | }
53 |
54 | /// Returns a Result with a tuple of the receiver and `other` values if both
55 | /// are `Success`es, or re-wrapping the error of the earlier `Failure`.
56 | public func fanout(_ other: @autoclosure () -> R) -> Result<(Value, R.Value), Error>
57 | where Error == R.Error
58 | {
59 | return self.flatMap { left in other().map { right in (left, right) } }
60 | }
61 |
62 | /// Returns a new Result by mapping `Failure`'s values using `transform`, or re-wrapping `Success`es’ values.
63 | public func mapError(_ transform: (Error) -> Error2) -> Result {
64 | return flatMapError { .failure(transform($0)) }
65 | }
66 |
67 | /// Returns the result of applying `transform` to `Failure`’s errors, or re-wrapping `Success`es’ values.
68 | public func flatMapError(_ transform: (Error) -> Result) -> Result {
69 | return analysis(
70 | ifSuccess: Result.success,
71 | ifFailure: transform)
72 | }
73 |
74 | /// Returns a new Result by mapping `Success`es’ values using `success`, and by mapping `Failure`'s values using `failure`.
75 | public func bimap(success: (Value) -> U, failure: (Error) -> Error2) -> Result {
76 | return analysis(
77 | ifSuccess: { .success(success($0)) },
78 | ifFailure: { .failure(failure($0)) }
79 | )
80 | }
81 | }
82 |
83 | public extension ResultProtocol {
84 |
85 | // MARK: Higher-order functions
86 |
87 | /// Returns `self.value` if this result is a .Success, or the given value otherwise. Equivalent with `??`
88 | public func recover(_ value: @autoclosure () -> Value) -> Value {
89 | return self.value ?? value()
90 | }
91 |
92 | /// Returns this result if it is a .Success, or the given result otherwise. Equivalent with `??`
93 | public func recover(with result: @autoclosure () -> Self) -> Self {
94 | return analysis(
95 | ifSuccess: { _ in self },
96 | ifFailure: { _ in result() })
97 | }
98 | }
99 |
100 | /// Protocol used to constrain `tryMap` to `Result`s with compatible `Error`s.
101 | public protocol ErrorProtocolConvertible: Swift.Error {
102 | static func error(from error: Swift.Error) -> Self
103 | }
104 |
105 | public extension ResultProtocol where Error: ErrorProtocolConvertible {
106 |
107 | /// Returns the result of applying `transform` to `Success`es’ values, or wrapping thrown errors.
108 | public func tryMap(_ transform: (Value) throws -> U) -> Result {
109 | return flatMap { value in
110 | do {
111 | return .success(try transform(value))
112 | }
113 | catch {
114 | let convertedError = Error.error(from: error)
115 | // Revisit this in a future version of Swift. https://twitter.com/jckarter/status/672931114944696321
116 | return .failure(convertedError)
117 | }
118 | }
119 | }
120 | }
121 |
122 | // MARK: - Operators
123 |
124 | extension ResultProtocol where Value: Equatable, Error: Equatable {
125 | /// Returns `true` if `left` and `right` are both `Success`es and their values are equal, or if `left` and `right` are both `Failure`s and their errors are equal.
126 | public static func ==(left: Self, right: Self) -> Bool {
127 | if let left = left.value, let right = right.value {
128 | return left == right
129 | } else if let left = left.error, let right = right.error {
130 | return left == right
131 | }
132 | return false
133 | }
134 |
135 | /// Returns `true` if `left` and `right` represent different cases, or if they represent the same case but different values.
136 | public static func !=(left: Self, right: Self) -> Bool {
137 | return !(left == right)
138 | }
139 | }
140 |
141 | extension ResultProtocol {
142 | /// Returns the value of `left` if it is a `Success`, or `right` otherwise. Short-circuits.
143 | public static func ??(left: Self, right: @autoclosure () -> Value) -> Value {
144 | return left.recover(right())
145 | }
146 |
147 | /// Returns `left` if it is a `Success`es, or `right` otherwise. Short-circuits.
148 | public static func ??(left: Self, right: @autoclosure () -> Self) -> Self {
149 | return left.recover(with: right())
150 | }
151 | }
152 |
153 | // MARK: - migration support
154 |
--------------------------------------------------------------------------------
/TeamStatusCommon/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // main.swift
3 | // TeamStatus
4 | //
5 | // Created by Marcin Religa on 24/05/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | let app = TeamStatusApp()
12 | app.run()
13 |
--------------------------------------------------------------------------------
/TeamStatusTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/TeamStatusTests/TeamStatusTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TeamStatusTests.swift
3 | // TeamStatusTests
4 | //
5 | // Created by Marcin Religa on 31/05/2017.
6 | // Copyright © 2017 Marcin Religa. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import TeamStatus
11 |
12 | class TeamStatusTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | // Use XCTAssert and related functions to verify your tests produce the correct results.
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measure {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/doc/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcinreliga/TeamStatus-for-GitHub/6ab1dc08a4612b21c349d29538a065d974adbe2b/doc/preview.png
--------------------------------------------------------------------------------
/release/TeamStatus.app.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcinreliga/TeamStatus-for-GitHub/6ab1dc08a4612b21c349d29538a065d974adbe2b/release/TeamStatus.app.zip
--------------------------------------------------------------------------------