├── .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 | 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 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 238 | 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 --------------------------------------------------------------------------------