diff --git a/BuildaCIServer/XcodeServer.swift b/BuildaCIServer/XcodeServer.swift index 042fb85..01c04cb 100644 --- a/BuildaCIServer/XcodeServer.swift +++ b/BuildaCIServer/XcodeServer.swift @@ -147,7 +147,7 @@ public extension XcodeServer { self.http.sendRequest(request, completion: { (response, body, error) -> () in if response == nil { - let e = error ?? Errors.errorWithInfo("Nil response") + let e = error ?? Error.withInfo("Nil response") completion(response: nil, body: body, error: e) return } @@ -156,7 +156,7 @@ public extension XcodeServer { }) } else { - completion(response: nil, body: nil, error: Errors.errorWithInfo("Couldn't create Request")) + completion(response: nil, body: nil, error: Error.withInfo("Couldn't create Request")) } } @@ -173,11 +173,11 @@ public extension XcodeServer { if response.statusCode == 204 { completion(success: true, error: nil) } else { - completion(success: false, error: Errors.errorWithInfo("Wrong status code: \(response.statusCode)")) + completion(success: false, error: Error.withInfo("Wrong status code: \(response.statusCode)")) } return } - completion(success: false, error: Errors.errorWithInfo("Nil response")) + completion(success: false, error: Error.withInfo("Nil response")) } } @@ -194,11 +194,11 @@ public extension XcodeServer { if response.statusCode == 204 { completion(success: true, error: nil) } else { - completion(success: false, error: Errors.errorWithInfo("Wrong status code: \(response.statusCode)")) + completion(success: false, error: Error.withInfo("Wrong status code: \(response.statusCode)")) } return } - completion(success: false, error: Errors.errorWithInfo("Nil response")) + completion(success: false, error: Error.withInfo("Nil response")) } } @@ -217,7 +217,7 @@ public extension XcodeServer { let bot = Bot(json: body as NSDictionary) completion(bot: bot, error: nil) } else { - completion(bot: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(bot: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -239,7 +239,7 @@ public extension XcodeServer { let bot = Bot(json: body) completion(bot: bot, error: nil) } else { - completion(bot: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(bot: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -262,10 +262,10 @@ public extension XcodeServer { if response.statusCode == 204 { completion(success: true, error: nil) } else { - completion(success: false, error: Errors.errorWithInfo("Wrong status code: \(response.statusCode)")) + completion(success: false, error: Error.withInfo("Wrong status code: \(response.statusCode)")) } } else { - completion(success: false, error: Errors.errorWithInfo("Nil response")) + completion(success: false, error: Error.withInfo("Nil response")) } } } @@ -283,7 +283,7 @@ public extension XcodeServer { let bots: [Bot] = XcodeServerArray(body) completion(bots: bots, error: nil) } else { - completion(bots: nil, error: Errors.errorWithInfo("Wrong data returned: \(body)")) + completion(bots: nil, error: Error.withInfo("Wrong data returned: \(body)")) } } } @@ -305,7 +305,7 @@ public extension XcodeServer { let integrations: [Integration] = XcodeServerArray(body) completion(integrations: integrations, error: nil) } else { - completion(integrations: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(integrations: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -332,7 +332,7 @@ public extension XcodeServer { let integration = Integration(json: body) completion(integration: integration, error: nil) } else { - completion(integration: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(integration: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -367,7 +367,7 @@ public extension XcodeServer { let devices: [Device] = XcodeServerArray(array) completion(devices: devices, error: error) } else { - completion(devices: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(devices: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -385,10 +385,10 @@ public extension XcodeServer { if let canCreateBots = body["result"] as? Bool where canCreateBots == true { completion(canCreateBots: true, error: nil) } else { - completion(canCreateBots: false, error: Errors.errorWithInfo("Specified user cannot create bots")) + completion(canCreateBots: false, error: Error.withInfo("Specified user cannot create bots")) } } else { - completion(canCreateBots: false, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(canCreateBots: false, error: Error.withInfo("Wrong body \(body)")) } } } diff --git a/BuildaGitServer/GitHubEndpoints.swift b/BuildaGitServer/GitHubEndpoints.swift index 0b79231..8bd1878 100644 --- a/BuildaGitServer/GitHubEndpoints.swift +++ b/BuildaGitServer/GitHubEndpoints.swift @@ -33,7 +33,7 @@ public class GitHubEndpoints { private let baseURL: String private let token: String? - init(baseURL: String, token: String?) { + public init(baseURL: String, token: String?) { self.baseURL = baseURL self.token = token } diff --git a/BuildaGitServer/GitHubServer.swift b/BuildaGitServer/GitHubServer.swift index 198b256..190e8f8 100644 --- a/BuildaGitServer/GitHubServer.swift +++ b/BuildaGitServer/GitHubServer.swift @@ -125,7 +125,7 @@ extension GitHubServer { } if response == nil { - completion(response: nil, body: body, error: Errors.errorWithInfo("Nil response")) + completion(response: nil, body: body, error: Error.withInfo("Nil response")) return } @@ -151,7 +151,7 @@ extension GitHubServer { where message == "Not Found" { let url = request.URL ?? "" - completion(response: nil, body: nil, error: Errors.errorWithInfo("Not found: \(url)")) + completion(response: nil, body: nil, error: Error.withInfo("Not found: \(url)")) return } @@ -162,7 +162,7 @@ extension GitHubServer { case 400 ... 500: let message = (body as? NSDictionary)?["message"] as? String ?? "Unknown error" let resultString = "\(statusCode): \(message)" - completion(response: response, body: body, error: Errors.errorWithInfo(resultString, internalError: error)) + completion(response: response, body: body, error: Error.withInfo(resultString, internalError: error)) return default: break @@ -189,7 +189,7 @@ extension GitHubServer { self.sendRequestWithPossiblePagination(request, accumulatedResponseBody: NSArray(), completion: completion) } else { - completion(response: nil, body: nil, error: Errors.errorWithInfo("Couldn't create Request")) + completion(response: nil, body: nil, error: Error.withInfo("Couldn't create Request")) } } @@ -212,7 +212,7 @@ extension GitHubServer { let prs: [PullRequest] = GitHubArray(body) completion(prs: prs, error: nil) } else { - completion(prs: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(prs: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -238,7 +238,7 @@ extension GitHubServer { let pr = PullRequest(json: body) completion(pr: pr, error: nil) } else { - completion(pr: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(pr: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -266,7 +266,7 @@ extension GitHubServer { let mostRecentStatus = statuses.sorted({ return $0.created! > $1.created! }).first completion(status: mostRecentStatus, error: nil) } else { - completion(status: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(status: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -293,7 +293,7 @@ extension GitHubServer { let status = Status(json: body) completion(status: status, error: nil) } else { - completion(status: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(status: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -321,7 +321,7 @@ extension GitHubServer { let comments: [Comment] = GitHubArray(body) completion(comments: comments, error: nil) } else { - completion(comments: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(comments: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -351,7 +351,7 @@ extension GitHubServer { let comment = Comment(json: body) completion(comment: comment, error: nil) } else { - completion(comment: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(comment: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -381,7 +381,7 @@ extension GitHubServer { let comment = Comment(json: body) completion(comment: comment, error: nil) } else { - completion(comment: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(comment: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -433,7 +433,7 @@ extension GitHubServer { completion(result: nil, error: error) } } else { - completion(result: nil, error: Errors.errorWithInfo("Nil response")) + completion(result: nil, error: Error.withInfo("Nil response")) } } } @@ -459,7 +459,7 @@ extension GitHubServer { let branches: [Branch] = GitHubArray(body) completion(branches: branches, error: nil) } else { - completion(branches: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(branches: nil, error: Error.withInfo("Wrong body \(body)")) } } } @@ -485,7 +485,7 @@ extension GitHubServer { let repository: Repo = Repo(json: body) completion(repo: repository, error: nil) } else { - completion(repo: nil, error: Errors.errorWithInfo("Wrong body \(body)")) + completion(repo: nil, error: Error.withInfo("Wrong body \(body)")) } } } diff --git a/BuildaGitServer/GitHubServerExtensions.swift b/BuildaGitServer/GitHubServerExtensions.swift index 0333682..466654f 100644 --- a/BuildaGitServer/GitHubServerExtensions.swift +++ b/BuildaGitServer/GitHubServerExtensions.swift @@ -29,7 +29,7 @@ public extension GitHubServer { let sha = pr.head.sha self.getStatusOfCommit(sha, repo: repo, completion: completion) } else { - completion(status: nil, error: Errors.errorWithInfo("PR is nil and error is nil")) + completion(status: nil, error: Error.withInfo("PR is nil and error is nil")) } } } @@ -58,7 +58,7 @@ public extension GitHubServer { } completion(foundComments: filtered, error: nil) } else { - completion(foundComments: nil, error: Errors.errorWithInfo("Nil comments and nil error. Wat?")) + completion(foundComments: nil, error: Error.withInfo("Nil comments and nil error. Wat?")) } } } diff --git a/BuildaUtils/Errors.swift b/BuildaUtils/Errors.swift index 32637f1..bf7e258 100644 --- a/BuildaUtils/Errors.swift +++ b/BuildaUtils/Errors.swift @@ -12,14 +12,14 @@ public enum BuildaError: String { case UnknownError = "Unknown error" } -public class Errors { +public class Error { - public class func errorFromType(type: BuildaError) -> NSError { + public class func fromType(type: BuildaError) -> NSError { - return self.errorWithInfo(type.rawValue) + return self.withInfo(type.rawValue) } - public class func errorWithInfo(info: String?, internalError: NSError? = nil, userInfo: NSDictionary? = nil) -> NSError { + public class func withInfo(info: String?, internalError: NSError? = nil, userInfo: NSDictionary? = nil) -> NSError { var finalInfo = NSMutableDictionary() if let info = info { diff --git a/BuildaUtils/HTTPUtils.swift b/BuildaUtils/HTTPUtils.swift index 762700c..00e2e19 100644 --- a/BuildaUtils/HTTPUtils.swift +++ b/BuildaUtils/HTTPUtils.swift @@ -74,7 +74,7 @@ public class HTTP { let commonError: NSError? = { if let userInfo = userInfo { - return Errors.errorWithInfo(nil, internalError: nil, userInfo: userInfo) + return Error.withInfo(nil, internalError: nil, userInfo: userInfo) } return nil }() @@ -87,7 +87,7 @@ public class HTTP { completion(response: httpResponse, body: code, error: error) } } else { - let e = error ?? Errors.errorWithInfo("Response is nil") + let e = error ?? Error.withInfo("Response is nil") completion(response: nil, body: nil, error: e) } diff --git a/BuildaUtils/XcodeProjectParser.swift b/BuildaUtils/XcodeProjectParser.swift index f8d8f30..3468284 100644 --- a/BuildaUtils/XcodeProjectParser.swift +++ b/BuildaUtils/XcodeProjectParser.swift @@ -81,12 +81,12 @@ public class XcodeProjectParser { if let parsed = self.parseCheckoutFile(checkoutUrl) { return (parsed, nil) } else { - let error = Errors.errorWithInfo("Cannot parse the checkout file at path \(checkoutUrl)") + let error = Error.withInfo("Cannot parse the checkout file at path \(checkoutUrl)") return (nil, error) } } //no checkout, what to do? - let error = Errors.errorWithInfo("Cannot find the Checkout file, please make sure to open this project in Xcode at least once (it will generate the required Checkout file). Then try again.") + let error = Error.withInfo("Cannot find the Checkout file, please make sure to open this project in Xcode at least once (it will generate the required Checkout file). Then try again.") return (nil, error) } diff --git a/Buildasaur.xcodeproj/project.pbxproj b/Buildasaur.xcodeproj/project.pbxproj index 2e7297f..97a84d9 100644 --- a/Buildasaur.xcodeproj/project.pbxproj +++ b/Buildasaur.xcodeproj/project.pbxproj @@ -19,6 +19,15 @@ 3A331C411A940C8D0005AA98 /* CommonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A331C401A940C8D0005AA98 /* CommonExtensions.swift */; }; 3A3BDC1D1AF6C51900D2CD99 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3BDC1C1AF6C51900D2CD99 /* Extensions.swift */; }; 3A3BDC1F1AF6D34900D2CD99 /* GitHubRateLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3BDC1E1AF6D34900D2CD99 /* GitHubRateLimit.swift */; }; + 3A46A2C11B07D8D800F0FA4B /* SyncerBotNaming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46A2C01B07D8D800F0FA4B /* SyncerBotNaming.swift */; }; + 3A46A2C31B07DCCA00F0FA4B /* SyncerBotManipulation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46A2C21B07DCCA00F0FA4B /* SyncerBotManipulation.swift */; }; + 3A46A2C61B07DEAE00F0FA4B /* SyncerGitHubUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46A2C51B07DEAE00F0FA4B /* SyncerGitHubUtils.swift */; }; + 3A46A2C81B07DF8F00F0FA4B /* SyncerBotUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46A2C71B07DF8F00F0FA4B /* SyncerBotUtils.swift */; }; + 3A46A2CA1B07E28700F0FA4B /* SyncPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46A2C91B07E28700F0FA4B /* SyncPair.swift */; }; + 3A46A2CD1B07E38E00F0FA4B /* SyncPair_PR_NoBot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46A2CC1B07E38E00F0FA4B /* SyncPair_PR_NoBot.swift */; }; + 3A46A2CF1B07E60200F0FA4B /* SyncPair_PR_Bot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46A2CE1B07E60200F0FA4B /* SyncPair_PR_Bot.swift */; }; + 3A46A2D11B07E64F00F0FA4B /* SyncPair_NoPR_Bot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46A2D01B07E64F00F0FA4B /* SyncPair_NoPR_Bot.swift */; }; + 3A46A2E31B081F2100F0FA4B /* SyncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46A2E21B081F2100F0FA4B /* SyncerTests.swift */; }; 3A4770A21A745F470016E170 /* StorageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4770A11A745F470016E170 /* StorageUtils.swift */; }; 3A4770A41A745FFA0016E170 /* XcodeProjectParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4770A31A745FFA0016E170 /* XcodeProjectParser.swift */; }; 3A5591561A913C0A00FB19F2 /* XcodeLocalSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5591551A913C0A00FB19F2 /* XcodeLocalSource.swift */; }; @@ -95,6 +104,13 @@ remoteGlobalIDString = 3AAF6E731A3CE4CB00C657FB; remoteInfo = BuildaUtils; }; + 3A46A2DC1B081EF400F0FA4B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3A5687681A3B93BD0066DB2B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3A56876F1A3B93BD0066DB2B; + remoteInfo = Buildasaur; + }; 3AAF6E891A3CE4CC00C657FB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 3A5687681A3B93BD0066DB2B /* Project object */; @@ -177,6 +193,17 @@ 3A38CF951A3CE94C00F41AFA /* Status.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Status.swift; path = BuildaGitServer/Status.swift; sourceTree = SOURCE_ROOT; }; 3A3BDC1C1AF6C51900D2CD99 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 3A3BDC1E1AF6D34900D2CD99 /* GitHubRateLimit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubRateLimit.swift; sourceTree = ""; }; + 3A46A2C01B07D8D800F0FA4B /* SyncerBotNaming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncerBotNaming.swift; sourceTree = ""; }; + 3A46A2C21B07DCCA00F0FA4B /* SyncerBotManipulation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncerBotManipulation.swift; sourceTree = ""; }; + 3A46A2C51B07DEAE00F0FA4B /* SyncerGitHubUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncerGitHubUtils.swift; sourceTree = ""; }; + 3A46A2C71B07DF8F00F0FA4B /* SyncerBotUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncerBotUtils.swift; sourceTree = ""; }; + 3A46A2C91B07E28700F0FA4B /* SyncPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncPair.swift; sourceTree = ""; }; + 3A46A2CC1B07E38E00F0FA4B /* SyncPair_PR_NoBot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncPair_PR_NoBot.swift; sourceTree = ""; }; + 3A46A2CE1B07E60200F0FA4B /* SyncPair_PR_Bot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncPair_PR_Bot.swift; sourceTree = ""; }; + 3A46A2D01B07E64F00F0FA4B /* SyncPair_NoPR_Bot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncPair_NoPR_Bot.swift; sourceTree = ""; }; + 3A46A2D61B081EF400F0FA4B /* BuildasaurTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BuildasaurTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A46A2D91B081EF400F0FA4B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3A46A2E21B081F2100F0FA4B /* SyncerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncerTests.swift; sourceTree = ""; }; 3A4770A11A745F470016E170 /* StorageUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageUtils.swift; sourceTree = ""; }; 3A4770A31A745FFA0016E170 /* XcodeProjectParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XcodeProjectParser.swift; sourceTree = ""; }; 3A4770A51A746BC00016E170 /* Buildasaur.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Buildasaur.entitlements; sourceTree = ""; }; @@ -255,6 +282,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3A46A2D31B081EF400F0FA4B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3A56876D1A3B93BD0066DB2B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -320,19 +354,61 @@ isa = PBXGroup; children = ( 3A612F001AADA28D00183BCB /* BuildTemplate.swift */, - 3A2F9D811A8FE5D700B0DB68 /* StorageManager.swift */, 3A2F9D841A8FE64900B0DB68 /* LocalSource.swift */, - 3A5591551A913C0A00FB19F2 /* XcodeLocalSource.swift */, - 3A4770A11A745F470016E170 /* StorageUtils.swift */, 3AAA1B711AABBBB200FA1598 /* NetworkUtils.swift */, + 3A2F9D811A8FE5D700B0DB68 /* StorageManager.swift */, + 3A4770A11A745F470016E170 /* StorageUtils.swift */, + 3A5591551A913C0A00FB19F2 /* XcodeLocalSource.swift */, + 3A5B04B11AB5C42A00F60536 /* XcodeServerSyncerUtils.swift */, ); name = Model; sourceTree = ""; }; + 3A46A2C41B07DE1200F0FA4B /* HDGitHubXCBotSyncer */ = { + isa = PBXGroup; + children = ( + 3A90C4BF1A90220F0048C040 /* HDGitHubXCBotSyncer.swift */, + 3A46A2C21B07DCCA00F0FA4B /* SyncerBotManipulation.swift */, + 3A46A2C01B07D8D800F0FA4B /* SyncerBotNaming.swift */, + 3A46A2C51B07DEAE00F0FA4B /* SyncerGitHubUtils.swift */, + 3A46A2C71B07DF8F00F0FA4B /* SyncerBotUtils.swift */, + ); + name = HDGitHubXCBotSyncer; + sourceTree = ""; + }; + 3A46A2CB1B07E33400F0FA4B /* SyncPairs */ = { + isa = PBXGroup; + children = ( + 3A46A2C91B07E28700F0FA4B /* SyncPair.swift */, + 3A46A2CC1B07E38E00F0FA4B /* SyncPair_PR_NoBot.swift */, + 3A46A2CE1B07E60200F0FA4B /* SyncPair_PR_Bot.swift */, + 3A46A2D01B07E64F00F0FA4B /* SyncPair_NoPR_Bot.swift */, + ); + name = SyncPairs; + sourceTree = ""; + }; + 3A46A2D71B081EF400F0FA4B /* BuildasaurTests */ = { + isa = PBXGroup; + children = ( + 3A46A2E21B081F2100F0FA4B /* SyncerTests.swift */, + 3A46A2D81B081EF400F0FA4B /* Supporting Files */, + ); + path = BuildasaurTests; + sourceTree = ""; + }; + 3A46A2D81B081EF400F0FA4B /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 3A46A2D91B081EF400F0FA4B /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; 3A5687671A3B93BD0066DB2B = { isa = PBXGroup; children = ( 3A5687721A3B93BD0066DB2B /* Buildasaur */, + 3A46A2D71B081EF400F0FA4B /* BuildasaurTests */, 3AAF6EBF1A3CE57100C657FB /* BuildaCIServer */, 3AAF6EE51A3CE5BA00C657FB /* BuildaGitServer */, 3AAF6EF41A3CE5BA00C657FB /* BuildaGitServerTests */, @@ -351,6 +427,7 @@ 3AAF6EE41A3CE5BA00C657FB /* BuildaGitServer.framework */, 3AAF6EEE1A3CE5BA00C657FB /* BuildaGitServerTests.xctest */, 3A15464B1B02A32C001EEB45 /* BuildaUtilsTests.xctest */, + 3A46A2D61B081EF400F0FA4B /* BuildasaurTests.xctest */, ); name = Products; sourceTree = ""; @@ -465,20 +542,20 @@ 3AAF6E751A3CE4CC00C657FB /* BuildaUtils */ = { isa = PBXGroup; children = ( - 3A808A1C1ADB063F0073145D /* Persistence.swift */, - 3A808A1A1ADB03640073145D /* Logging.swift */, - 3AF1B1211AAC621800917EF3 /* UIUtils.swift */, - 3A4770A31A745FFA0016E170 /* XcodeProjectParser.swift */, - 3A6355DC1A3BC19800545BF9 /* HTTPUtils.swift */, - 3ACB831F1A3BB64F00E285FD /* JSON.swift */, - 3A7B91351A3E41980060A21A /* Server.swift */, 3AAF6E781A3CE4CC00C657FB /* BuildaUtils.h */, - 3AAF6E761A3CE4CC00C657FB /* Supporting Files */, 3AAA1B731AABBD3A00FA1598 /* Errors.swift */, 3A3BDC1C1AF6C51900D2CD99 /* Extensions.swift */, + 3A6355DC1A3BC19800545BF9 /* HTTPUtils.swift */, + 3ACB831F1A3BB64F00E285FD /* JSON.swift */, + 3A808A1A1ADB03640073145D /* Logging.swift */, + 3A808A1C1ADB063F0073145D /* Persistence.swift */, 3A1546451B02A0C6001EEB45 /* Script.swift */, + 3A7B91351A3E41980060A21A /* Server.swift */, 3AF2B03A1B02C5D700DCF2D6 /* SSHKeyVerification.swift */, 3AB3FDC81B05616A00021197 /* TimeUtils.swift */, + 3AF1B1211AAC621800917EF3 /* UIUtils.swift */, + 3A4770A31A745FFA0016E170 /* XcodeProjectParser.swift */, + 3AAF6E761A3CE4CC00C657FB /* Supporting Files */, ); path = BuildaUtils; sourceTree = ""; @@ -550,8 +627,8 @@ isa = PBXGroup; children = ( 3A2F9D851A8FE64900B0DB68 /* Syncer.swift */, - 3A90C4BF1A90220F0048C040 /* HDGitHubXCBotSyncer.swift */, - 3A5B04B11AB5C42A00F60536 /* XcodeServerSyncerUtils.swift */, + 3A46A2CB1B07E33400F0FA4B /* SyncPairs */, + 3A46A2C41B07DE1200F0FA4B /* HDGitHubXCBotSyncer */, ); name = Syncers; sourceTree = ""; @@ -604,6 +681,24 @@ productReference = 3A15464B1B02A32C001EEB45 /* BuildaUtilsTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 3A46A2D51B081EF400F0FA4B /* BuildasaurTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3A46A2DE1B081EF400F0FA4B /* Build configuration list for PBXNativeTarget "BuildasaurTests" */; + buildPhases = ( + 3A46A2D21B081EF400F0FA4B /* Sources */, + 3A46A2D31B081EF400F0FA4B /* Frameworks */, + 3A46A2D41B081EF400F0FA4B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3A46A2DD1B081EF400F0FA4B /* PBXTargetDependency */, + ); + name = BuildasaurTests; + productName = BuildasaurTests; + productReference = 3A46A2D61B081EF400F0FA4B /* BuildasaurTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 3A56876F1A3B93BD0066DB2B /* Buildasaur */ = { isa = PBXNativeTarget; buildConfigurationList = 3A56878C1A3B93BD0066DB2B /* Build configuration list for PBXNativeTarget "Buildasaur" */; @@ -712,6 +807,10 @@ 3A15464A1B02A32C001EEB45 = { CreatedOnToolsVersion = 6.3.1; }; + 3A46A2D51B081EF400F0FA4B = { + CreatedOnToolsVersion = 6.4; + TestTargetID = 3A56876F1A3B93BD0066DB2B; + }; 3A56876F1A3B93BD0066DB2B = { CreatedOnToolsVersion = 6.1.1; DevelopmentTeam = 7BJ2984YDK; @@ -755,6 +854,7 @@ 3AAF6EE31A3CE5BA00C657FB /* BuildaGitServer */, 3AAF6EED1A3CE5BA00C657FB /* BuildaGitServerTests */, 3A15464A1B02A32C001EEB45 /* BuildaUtilsTests */, + 3A46A2D51B081EF400F0FA4B /* BuildasaurTests */, ); }; /* End PBXProject section */ @@ -767,6 +867,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3A46A2D41B081EF400F0FA4B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3A56876E1A3B93BD0066DB2B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -815,18 +922,32 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 3A46A2D21B081EF400F0FA4B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A46A2E31B081F2100F0FA4B /* SyncerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3A56876C1A3B93BD0066DB2B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 3AAA1B761AAC504700FA1598 /* StatusServerViewController.swift in Sources */, + 3A46A2C81B07DF8F00F0FA4B /* SyncerBotUtils.swift in Sources */, + 3A46A2C61B07DEAE00F0FA4B /* SyncerGitHubUtils.swift in Sources */, 3A5591561A913C0A00FB19F2 /* XcodeLocalSource.swift in Sources */, 3A5687781A3B93BD0066DB2B /* MainViewController.swift in Sources */, + 3A46A2CA1B07E28700F0FA4B /* SyncPair.swift in Sources */, 3A2F9D861A8FE64900B0DB68 /* LocalSource.swift in Sources */, + 3A46A2C11B07D8D800F0FA4B /* SyncerBotNaming.swift in Sources */, + 3A46A2C31B07DCCA00F0FA4B /* SyncerBotManipulation.swift in Sources */, 3A5B04AC1AB4AC0F00F60536 /* SetupViewController.swift in Sources */, 3A5B04AA1AB4ABEB00F60536 /* TriggerViewController.swift in Sources */, 3AF1B1241AAC7CA500917EF3 /* StatusSyncerViewController.swift in Sources */, 3A5B04B21AB5C42A00F60536 /* XcodeServerSyncerUtils.swift in Sources */, + 3A46A2CD1B07E38E00F0FA4B /* SyncPair_PR_NoBot.swift in Sources */, 3A5B04B01AB5144700F60536 /* ManualBotManagementViewController.swift in Sources */, 3A5687761A3B93BD0066DB2B /* AppDelegate.swift in Sources */, 3A2F9D821A8FE5D700B0DB68 /* StorageManager.swift in Sources */, @@ -835,10 +956,12 @@ 3AAA1B721AABBBB200FA1598 /* NetworkUtils.swift in Sources */, 3AAA1B661AAB6E9800FA1598 /* SeparatorView.swift in Sources */, 3A2F9D871A8FE64900B0DB68 /* Syncer.swift in Sources */, + 3A46A2CF1B07E60200F0FA4B /* SyncPair_PR_Bot.swift in Sources */, 3A612F011AADA28D00183BCB /* BuildTemplate.swift in Sources */, 3A331C411A940C8D0005AA98 /* CommonExtensions.swift in Sources */, 3AD338B01AAE31D500ECD0F2 /* BuildTemplateViewController.swift in Sources */, 3A90C4C01A90220F0048C040 /* HDGitHubXCBotSyncer.swift in Sources */, + 3A46A2D11B07E64F00F0FA4B /* SyncPair_NoPR_Bot.swift in Sources */, 3AAA1B681AAB722600FA1598 /* StatusProjectViewController.swift in Sources */, 3AB3FDCB1B05644300021197 /* MenuItemManager.swift in Sources */, ); @@ -920,6 +1043,11 @@ target = 3AAF6E731A3CE4CB00C657FB /* BuildaUtils */; targetProxy = 3A1546521B02A32C001EEB45 /* PBXContainerItemProxy */; }; + 3A46A2DD1B081EF400F0FA4B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3A56876F1A3B93BD0066DB2B /* Buildasaur */; + targetProxy = 3A46A2DC1B081EF400F0FA4B /* PBXContainerItemProxy */; + }; 3AAF6E8A1A3CE4CC00C657FB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 3AAF6E731A3CE4CB00C657FB /* BuildaUtils */; @@ -1022,6 +1150,65 @@ }; name = Release; }; + 3A46A2DF1B081EF400F0FA4B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = BuildasaurTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Buildasaur.app/Contents/MacOS/Buildasaur"; + }; + name = Debug; + }; + 3A46A2E01B081EF400F0FA4B /* Testing */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + ENABLE_NS_ASSERTIONS = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = BuildasaurTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Buildasaur.app/Contents/MacOS/Buildasaur"; + }; + name = Testing; + }; + 3A46A2E11B081EF400F0FA4B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + ENABLE_NS_ASSERTIONS = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = BuildasaurTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Buildasaur.app/Contents/MacOS/Buildasaur"; + }; + name = Release; + }; 3A56878A1A3B93BD0066DB2B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1460,6 +1647,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 3A46A2DE1B081EF400F0FA4B /* Build configuration list for PBXNativeTarget "BuildasaurTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3A46A2DF1B081EF400F0FA4B /* Debug */, + 3A46A2E01B081EF400F0FA4B /* Testing */, + 3A46A2E11B081EF400F0FA4B /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; 3A56876B1A3B93BD0066DB2B /* Build configuration list for PBXProject "Buildasaur" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Buildasaur/CommonExtensions.swift b/Buildasaur/CommonExtensions.swift index f8d3c96..1a9d6ec 100644 --- a/Buildasaur/CommonExtensions.swift +++ b/Buildasaur/CommonExtensions.swift @@ -26,5 +26,39 @@ extension Array { } return nil } +} + +extension Array { + func mapVoidAsync(transformAsync: (item: T, itemCompletion: () -> ()) -> (), completion: () -> ()) { + self.mapAsync(transformAsync, completion: { (_) -> () in + completion() + }) + } + + func mapAsync(transformAsync: (item: T, itemCompletion: (U) -> ()) -> (), completion: ([U]) -> ()) { + + let group = dispatch_group_create() + var returnedValueMap = [Int: U]() + + for (index, element) in enumerate(self) { + dispatch_group_enter(group) + transformAsync(item: element, itemCompletion: { + (returned: U) -> () in + returnedValueMap[index] = returned + dispatch_group_leave(group) + }) + } + + dispatch_group_notify(group, dispatch_get_main_queue()) { + + //we have all the returned values in a map, put it back into an array of Us + var returnedValues = [U]() + for i in 0 ..< returnedValueMap.count { + returnedValues.append(returnedValueMap[i]!) + } + completion(returnedValues) + } + } } + diff --git a/Buildasaur/HDGitHubXCBotSyncer.swift b/Buildasaur/HDGitHubXCBotSyncer.swift index 90fe5ad..1576adc 100644 --- a/Buildasaur/HDGitHubXCBotSyncer.swift +++ b/Buildasaur/HDGitHubXCBotSyncer.swift @@ -21,7 +21,7 @@ public class HDGitHubXCBotSyncer : Syncer { typealias GitHubStatusAndComment = (status: Status, comment: String?) - init(integrationServer: XcodeServer, sourceServer: GitHubServer, localSource: LocalSource, + public init(integrationServer: XcodeServer, sourceServer: GitHubServer, localSource: LocalSource, syncInterval: NSTimeInterval, waitForLttm: Bool, postStatusComments: Bool) { self.github = sourceServer @@ -71,7 +71,7 @@ public class HDGitHubXCBotSyncer : Syncer { return dict } - private func repoName() -> String? { + func repoName() -> String? { return self.localSource.githubRepoName() } @@ -107,8 +107,10 @@ public class HDGitHubXCBotSyncer : Syncer { self.reports["All Bots"] = "\(bots.count)" - self.resolvePRsAndBots(repoName: repoName, prs: prs, bots: bots, completion: { + //we have both PRs and Bots, resolve + self.syncPRsAndBots(repoName: repoName, prs: prs, bots: bots, completion: { + //everything is done, report the damage of GitHub rate limit if let rateLimitInfo = self.github.latestRateLimitInfo { let report = rateLimitInfo.getReport() @@ -119,787 +121,122 @@ public class HDGitHubXCBotSyncer : Syncer { completion() }) } else { - self.notifyError(Errors.errorWithInfo("Nil bots even when error was nil"), context: "Fetching Bots") + self.notifyErrorString("Nil bots even when error was nil", context: "Fetching Bots") completion() } }) } else { - self.notifyError(Errors.errorWithInfo("PRs are nil and error is nil"), context: "Fetching PRs") + self.notifyErrorString("PRs are nil and error is nil", context: "Fetching PRs") completion() } }) } else { - self.notifyError(nil, context: "No repo name for GitHub found in URL") + self.notifyErrorString("Nil repo name", context: "Syncing") completion() } } - private func resolvePRsAndBots(#repoName: String, prs: [PullRequest], bots: [Bot], completion: () -> ()) { + public func syncPRsAndBots(#repoName: String, prs: [PullRequest], bots: [Bot], completion: () -> ()) { let prsDescription = prs.map({ "\n\tPR \($0.number): \($0.title) [\($0.head.ref) -> \($0.base.ref))]" }) + ["\n"] let botsDescription = bots.map({ "\n\t\($0.name)" }) + ["\n"] - Log.verbose("Resolving prs:\n\(prsDescription) \nand bots:\n\(botsDescription)") - if let repoName = self.repoName() { - - //first filter only builda's bots, don't manipulate manually created bots - //also filter only bots that belong to this project - let buildaBots = bots.filter { self.isBuildaBotBelongingToRepoWithName($0, repoName: repoName) } - - //create a map of name -> bot for fast manipulation - var mappedBots = [String: Bot]() - for bot in buildaBots { - mappedBots[bot.name] = bot - } - - //keep track of the ones that have a PR - var toSync: [(pr: PullRequest, bot: Bot)] = [] - var toCreate: [PullRequest] = [] - for pr in prs { - - let botName = self.nameForBotWithPR(pr, repoName: repoName) - - if let bot = mappedBots[botName] { - //we found a corresponding bot to this PR, add to toSync - toSync.append((pr: pr, bot: bot)) - - //and remove from bots mappedBots, because we handled it - mappedBots.removeValueForKey(botName) - } else { - //no bot found for this PR, we'll have to create one - toCreate.append(pr) - } - } - - //bots that we haven't found a corresponding PR for we delete - let toDelete = mappedBots.values.array - - //apply changes - self.applyResolvedChanges(toSync: toSync, toCreate: toCreate, toDelete: toDelete, completion: completion) - } else { - self.notifyError(Errors.errorWithInfo("Nil repo name"), context: "Resolving PRs and Bots") - completion() - } - } - - private func applyResolvedChanges(#toSync: [(pr: PullRequest, bot: Bot)], toCreate: [PullRequest], toDelete: [Bot], completion: () -> ()) { - - let group = dispatch_group_create() - - //first delete outdated bots - dispatch_group_enter(group) - self.deleteBots(toDelete, completion: { () -> () in - dispatch_group_leave(group) - }) - - //create new bots with prs - dispatch_group_enter(group) - self.createBotsFromPRs(toCreate, completion: { () -> () in - dispatch_group_leave(group) - }) + //create the changes necessary + let (toSync, toCreate, toDelete) = self.resolvePRsAndBots(repoName: repoName, prs: prs, bots: bots) - //and sync PR + Bot pairs - dispatch_group_enter(group) - self.syncPRBotPairs(toSync, completion: { () -> () in - dispatch_group_leave(group) - }) - - //when both, finish this method as well - dispatch_group_notify(group, dispatch_get_main_queue()) { () -> Void in - - if toCreate.count > 0 { - self.reports["Created bots"] = "\(toCreate.count)" - } - if toDelete.count > 0 { - self.reports["Deleted bots"] = "\(toDelete.count)" - } - if toSync.count > 0 { - self.reports["Synced bots"] = "\(toSync.count)" - } - - completion() - } - } - - private func syncPRBotPairs(pairs: [(pr: PullRequest, bot: Bot)], completion: () -> ()) { + //create actions from changes, so called "SyncPairs" + let syncPairs = self.createSyncPairsFrom(toSync: toSync, toCreate: toCreate, toDelete: toDelete) - pairs.mapVoidAsync({ (pair, itemCompletion) -> () in - self.tryToSyncPRWithBot(pair.pr, bot: pair.bot, completion: { () -> () in - Log.verbose("Synced up PR #\(pair.pr.number) with bot \(pair.bot.name)") - itemCompletion() - }) - }, completion: completion) + //start these actions + self.applyResolvedSyncPairs(syncPairs, completion: completion) } - private func isBotEnabled(pr: PullRequest, integrations: [Integration], completion: (isEnabled: Bool) -> ()) { + public func resolvePRsAndBots(#repoName: String, prs: [PullRequest], bots: [Bot]) -> (toSync: [(pr: PullRequest, bot: Bot)], toCreate: [PullRequest], toDelete: [Bot]) { - //bot is enabled if (there are any integrations) OR (there is a recent comment with a keyword to enable the bot in the pull request's conversation) - //which means that there are two ways of enabling a bot. - //a) manually start an integration through Xcode, API call or in Builda's GUI (TBB) - //b) (optional) comment an agreed keyword in the Pull Request, e.g. "lttm" - 'looks testable to me' is a frequent one - - if integrations.count > 0 || !self.waitForLttm { - completion(isEnabled: true) - return - } + //first filter only builda's bots, don't manipulate manually created bots + //also filter only bots that belong to this project + let buildaBots = bots.filter { BotNaming.isBuildaBotBelongingToRepoWithName($0, repoName: repoName) } - let keyword = ["lttm"] + //create a map of name -> bot for fast manipulation + var mappedBots = [String: Bot]() + for bot in buildaBots { mappedBots[bot.name] = bot } - if let repoName = self.repoName() { - - self.github.findMatchingCommentInIssue(keyword, issue: pr.number, repo: repoName) { - (foundComments, error) -> () in - - if error != nil { - self.notifyError(error, context: "Fetching comments") - completion(isEnabled: false) - return - } - - if let foundComments = foundComments { - completion(isEnabled: foundComments.count > 0) - } else { - completion(isEnabled: false) - } - } - - } else { - Log.error("No repo name, cannot find the GitHub repo!") - completion(isEnabled: false) - } - } - - private func tryToSyncPRWithBot(pr: PullRequest, bot: Bot, completion: () -> ()) { + //PRs that also have a bot, toSync + var toSync: [(pr: PullRequest, bot: Bot)] = [] - /* - TODO: we should establish some reliable and reasonable plan for how many integrations to fetch. - currently it's always 20, but some setups might have a crazy workflow with very frequent commits - on active bots etc. - */ - let query = [ - "last": "20" - ] - self.xcodeServer.getIntegrations(bot.id, query: query, completion: { (integrations, error) -> () in + //PRs that don't have a bot yet, to create + var toCreate: [PullRequest] = [] + for pr in prs { - if let error = error { - self.notifyError(error, context: "Bot \(bot.name) failed return integrations") - completion() - return - } + let botName = BotNaming.nameForBotWithPR(pr, repoName: repoName) - if let integrations = integrations { + if let bot = mappedBots[botName] { + //we found a corresponding bot to this PR, add to toSync + toSync.append((pr: pr, bot: bot)) - //first check whether the bot is even enabled - self.isBotEnabled(pr, integrations: integrations, completion: { (isEnabled) -> () in - - if isEnabled { - - self.syncPRWithBotIntegrations(pr, bot: bot, integrations: integrations, completion: completion) - - } else { - - //not enabled, make sure the PR reflects that and the instructions are clear - Log.verbose("Bot \(bot.name) is not yet enabled, ignoring...") - - let status = self.createStatusFromState(.Pending, description: "Waiting for \"lttm\" to start testing") - let notYetEnabled = GitHubStatusAndComment(status: status, comment: nil) - self.updatePRStatusIfNecessary(notYetEnabled, prNumber: pr.number, completion: completion) - } - }) + //and remove from bots mappedBots, because we handled it + mappedBots.removeValueForKey(botName) } else { - self.notifyError(Errors.errorWithInfo("Nil integrations even after returning nil error!"), context: "Getting integrations") + //no bot found for this PR, we'll have to create one + toCreate.append(pr) } - }) - } - - private func updatePRStatusIfNecessary(newStatus: GitHubStatusAndComment, prNumber: Int, completion: () -> ()) { - - let repoName = self.repoName()! - - self.github.getPullRequest(prNumber, repo: repoName) { (pr, error) -> () in - - if error != nil { - self.notifyError(error, context: "PR \(prNumber) failed to return data") - completion() - return - } - - if let pr = pr { - - let latestCommit = pr.head.sha - - self.github.getStatusOfCommit(latestCommit, repo: repoName, completion: { (status, error) -> () in - - if error != nil { - self.notifyError(error, context: "PR \(prNumber) failed to return status") - completion() - return - } - - if status == nil || newStatus.status != status! { - - self.postStatusWithComment(newStatus, commit: latestCommit, repo: repoName, pr: pr, completion: completion) - - } else { - completion() - } - }) - - } else { - self.notifyError(Errors.errorWithInfo("PR is nil and error is nil"), context: "Fetching a PR") - completion() - } - } - } - - private func syncPRWithBotIntegrations(pr: PullRequest, bot: Bot, integrations: [Integration], completion: () -> ()) { - - let group = dispatch_group_create() - - let uniqueIntegrations = Set(integrations) - - //------------ - // Split integrations into two groups: 1) for this SHA, 2) the rest - //------------ - - let headCommit: String = pr.head.sha - - //1) for this SHA - let headCommitIntegrations = uniqueIntegrations.filterSet { - (integration: Integration) -> Bool in - - //if it's not pending, we need to take a look at the blueprint and inspect the SHA. - if let blueprint = integration.blueprint, let sha = blueprint.commitSHA { - return sha == headCommit - } - - //when an integration is Pending, Preparing or Checking out, it doesn't have a blueprint, but it is, by definition, a headCommit - //integration (because it will check out the latest commit on the branch when it starts running) - if - integration.currentStep == .Pending || - integration.currentStep == .Preparing || - integration.currentStep == .Checkout - { - return true - } - - if integration.currentStep == .Completed { - - if let result = integration.result { - - //if the result doesn't have a SHA yet and isn't pending - take a look at the result - //if it's a checkout-error, assume that it is a malformed SSH key bot, so don't keep - //restarting integrations - at least until someone fixes it (by closing the PR and fixing - //their SSH keys in Buildasaur so that when the next bot gets created, it does so with the right - //SSH keys. - if result == .CheckoutError { - Log.error("Integration #\(integration.number) finished with a checkout error - please check that your SSH keys setup in Buildasaur are correct! If you need to fix them, please do so and then you need to recreate the bot - e.g. by closing the Pull Request, waiting for a sync (bot will disappear) and then reopening the Pull Request - should do the job!") - return true - } - - if result == .Canceled { - - //another case is when the integration gets doesn't yet have a blueprint AND was cancelled - - //we should assume it belongs to the latest commit, because we can't tell otherwise. - return true - } - } - } - - return false - } - - //2) the rest - let otherCommitIntegrations = uniqueIntegrations.subtract(headCommitIntegrations) - let noncompletedOtherCommitIntegrations: Set = otherCommitIntegrations.filterSet { - return $0.currentStep != .Completed - } - - //2.1) Ok, now first cancel all unfinished integrations of the non-current commits - dispatch_group_enter(group) - self.cancelIntegrations(Array(noncompletedOtherCommitIntegrations), completion: { () -> () in - dispatch_group_leave(group) - }) - - //------------ - // Now we're resolving Integrations for the current commit only - //------------ - /* - The resolving logic goes like this now. We have an array of integrations I for the latest commits. - A. is array empty? - A1. true -> there are no integrations for this commit. kick one off! we're done. - A2. false -> keep resolving (all references to "integrations" below mean only integrations of the current commit - B. take all pending integrations, keep the most recent one, if it's there, cancel all the other ones. - C. take the running integration, if it's there - D. take all completed integrations - - resolve the status of the PR as follows - - E. is there a latest pending integration? - E1. true -> status is ["Pending": "Waiting on the queue"]. also, if there's a running integration, cancel it. - E2. false -> - F. is there a running integration? - F1. true -> status is ["Pending": "Integration in progress..."]. update status and do nothing else. - F2. false -> - G. are there any completed integrations? - G1. true -> based on the result of the integrations create the PR status - G2. false -> this shouldn't happen, print a very angry message. - */ - - //A. is this array empty? - if headCommitIntegrations.count == 0 { - - //A1. - it's empty, kick off an integration for the latest commit - dispatch_group_enter(group) - self.xcodeServer.postIntegration(bot.id, completion: { (integration, error) -> () in - - if let integration = integration where error == nil { - Log.info("Bot \(bot.name) successfully enqueued Integration #\(integration.number)") - } else { - self.notifyError(error, context: "Bot \(bot.name) failed to enqueue an integration") - } - - dispatch_group_leave(group) - }) - //nothing else to do - - } else { - - //A2. not empty, keep resolving - - //B. get pending Integrations - let pending = headCommitIntegrations.filterSet { - $0.currentStep == .Pending - } - - var latestPendingIntegration: Integration? - if pending.count > 0 { - - //we should cancel all but the most recent one - //turn the pending set into an array and sort by integration number in ascending order - var pendingSortedArray: Array = Array(pending).sorted({ (integrationA, integrationB) -> Bool in - return integrationA.number < integrationB.number - }) - - //keep the latest, which will be the last in the array - //let this one run, it might have been a force rebuild. - latestPendingIntegration = pendingSortedArray.removeLast() - - //however, cancel the rest of the pending integrations - dispatch_group_enter(group) - self.cancelIntegrations(pendingSortedArray) { - dispatch_group_leave(group) - } - } - - //Get the running integration, if it's there - let runningIntegration = headCommitIntegrations.filterSet { - $0.currentStep != .Completed && $0.currentStep != .Pending - }.first - - //Get all completed integrations for this commit - let completedIntegrations = headCommitIntegrations.filterSet { - $0.currentStep == .Completed - } - - //resolve - dispatch_group_enter(group) - self.resolvePRStatusFromLatestIntegrations(pending: latestPendingIntegration, running: runningIntegration, completed: completedIntegrations, completion: { (statusWithComment) -> () in - - //we now have the status and an optional comment to add. - //in order to know what to do, we need to fetch the current status of this commit first. - let repoName = self.repoName()! - self.github.getStatusOfCommit(headCommit, repo: repoName, completion: { (status, error) -> () in - - if error != nil { - self.notifyError(error, context: "Failed to fetch status of commit \(headCommit) in repo \(repoName)") - dispatch_group_leave(group) - return - } - - let updateStatus: Bool - if let currentStatus = status { - //we have the current status! - updateStatus = (statusWithComment.status != currentStatus) - } else { - //doesn't have a status yet, update - updateStatus = true - } - - if updateStatus { - - let oldStatus = status?.description ?? "[no status]" - let newStatus = statusWithComment - let comment = newStatus.comment ?? "[no comment]" - Log.info("Updating status of commit \(headCommit) in PR #\(pr.number) from \(oldStatus) to \(newStatus), will add comment \(comment)") - - //we need to update status - self.postStatusWithComment(statusWithComment, commit: headCommit, repo: repoName, pr: pr, completion: { () -> () in - dispatch_group_leave(group) - }) - - } else { - //everything is how it's supposed to be - dispatch_group_leave(group) - } - }) - }) - } - //when all actions finished, complete - dispatch_group_notify(group, dispatch_get_main_queue(), completion) - } - - private func postStatusWithComment(statusWithComment: GitHubStatusAndComment, commit: String, repo: String, pr: PullRequest, completion: () -> ()) { + //bots that don't have a PR, to delete + let toDelete = mappedBots.values.array - self.github.postStatusOfCommit(statusWithComment.status, sha: commit, repo: repo) { (status, error) -> () in - - if error != nil { - self.notifyError(error, context: "Failed to post a status on commit \(commit) of repo \(repo)") - completion() - return - } - - //have a chance to NOT post a status comment... - let postStatusComments = self.postStatusComments - - //optional there can be a comment to be posted as well - if let comment = statusWithComment.comment where postStatusComments { - - //we have a comment, post it - self.github.postCommentOnIssue(comment, issueNumber: pr.number, repo: repo, completion: { (comment, error) -> () in - - if error != nil { - self.notifyError(error, context: "Failed to post a comment \"\(comment)\" on PR \(pr.number) of repo \(repo)") - } - completion() - }) - - } else { - completion() - } - } + return (toSync, toCreate, toDelete) } - private func resolvePRStatusFromLatestIntegrations(#pending: Integration?, running: Integration?, completed: Set, completion: (GitHubStatusAndComment) -> ()) { - - let group = dispatch_group_create() - let statusWithComment: GitHubStatusAndComment - - //if there's any pending integration, we're ["Pending" - Waiting in the queue] - if let pending = pending { - - //TODO: show how many builds are ahead in the queue and estimate when it will be - //started and when finished? (there is an average running time on each bot, it should be easy) - let status = self.createStatusFromState(.Pending, description: "Build waiting in the queue...") - statusWithComment = (status: status, comment: nil) - - //also, cancel the running integration, if it's there any - if let running = running { - dispatch_group_enter(group) - self.cancelIntegrations([running], completion: { () -> () in - dispatch_group_leave(group) - }) - } - } else { - - //there's no pending integration, it's down to running and completed - if let running = running { - - //there is a running integration. - //TODO: estimate, based on the average running time of this bot and on the started timestamp, when it will finish. add that to the description. - let currentStepString = running.currentStep.rawValue - let status = self.createStatusFromState(.Pending, description: "Integration step: \(currentStepString)...") - statusWithComment = (status: status, comment: nil) - - } else { - - //there no running integration, we're down to completed integration. - if completed.count > 0 { - - //we have some completed integrations - statusWithComment = self.resolveStatusFromCompletedIntegrations(completed) - - } else { - //this shouldn't happen. - Log.error("LOGIC ERROR! This shouldn't happen, there are no completed integrations!") - let status = self.createStatusFromState(.Error, description: "* UNKNOWN STATE, Builda ERROR *") - statusWithComment = (status: status, "Builda error, unknown state!") - } - } - } + public func createSyncPairsFrom(#toSync: [(pr: PullRequest, bot: Bot)], toCreate: [PullRequest], toDelete: [Bot]) -> [SyncPair] { - dispatch_group_notify(group, dispatch_get_main_queue()) { () -> Void in - completion(statusWithComment) - } - } - - private func formattedDurationOfIntegration(integration: Integration) -> String? { + //create sync pairs for each action needed + let deleteBotSyncPairs = toDelete.map({ SyncPair_NoPR_Bot(bot: $0) as SyncPair }) + let createBotSyncPairs = toCreate.map({ SyncPair_PR_NoBot(pr: $0) as SyncPair }) + let syncPRBotSyncPairs = toSync.map({ SyncPair_PR_Bot(pr: $0.pr, bot: $0.bot) as SyncPair }) - if let seconds = integration.duration { - - var result = TimeUtils.secondsToNaturalTime(Int(seconds)) - return result - - } else { - Log.error("No duration provided in integration \(integration)") - return "[NOT PROVIDED]" - } - } - - private func baseCommentFromIntegration(integration: Integration) -> String { - - var comment = "Result of integration \(integration.number)\n" - if let duration = self.formattedDurationOfIntegration(integration) { - comment += "Integration took " + duration + ".\n" - } - return comment - } - - private func resolveStatusFromCompletedIntegrations(integrations: Set) -> GitHubStatusAndComment { + //here feel free to inject more things to be done during a sync - //get integrations sorted by number - let sortedDesc = Array(integrations).sorted { $0.number > $1.number } + //put them all into one array + let syncPairsRaw: [SyncPair] = deleteBotSyncPairs + createBotSyncPairs + syncPRBotSyncPairs - //if there are any succeeded, it wins - iterating from the end - if let passingIntegration = sortedDesc.filter({ - (integration: Integration) -> Bool in - switch integration.result! { - case Integration.Result.Succeeded, Integration.Result.Warnings, Integration.Result.AnalyzerWarnings: - return true - default: - return false - } - }).first { - - let baseComment = self.baseCommentFromIntegration(passingIntegration) - let comment: String - let status = self.createStatusFromState(.Success, description: "Build passed!") - let summary = passingIntegration.buildResultSummary! - if passingIntegration.result == .Succeeded { - comment = baseComment + "Perfect build! All \(summary.testsCount) tests passed. :+1:" - } else if passingIntegration.result == .Warnings { - comment = baseComment + "All \(summary.testsCount) tests passed, but please fix \(summary.warningCount) warnings." - } else { - comment = baseComment + "All \(summary.testsCount) tests passed, but please fix \(summary.analyzerWarningCount) analyzer warnings." - } - return (status: status, comment: comment) - } - - //ok, no succeeded, warnings or analyzer warnings, get down to test failures - if let testFailingIntegration = sortedDesc.filter({ - $0.result! == Integration.Result.TestFailures - }).first { - - let baseComment = self.baseCommentFromIntegration(testFailingIntegration) - let status = self.createStatusFromState(.Failure, description: "Build failed tests!") - let summary = testFailingIntegration.buildResultSummary! - let comment = baseComment + "Build failed \(summary.testFailureCount) tests out of \(summary.testsCount)" - return (status: status, comment: comment) - } - - //ok, the build didn't even run then. it either got cancelled or failed - if let erroredIntegration = sortedDesc.filter({ - $0.result! != Integration.Result.Canceled - }).first { - - let baseComment = self.baseCommentFromIntegration(erroredIntegration) - let errorCount: String - if let summary = erroredIntegration.buildResultSummary { - errorCount = "\(summary.errorCount)" - } else { - errorCount = "?" - } - let status = self.createStatusFromState(.Error, description: "Build error!") - let comment = baseComment + "\(errorCount) build errors: \(erroredIntegration.result!.rawValue)" - return (status: status, comment: comment) - } - - //cool, not even build error. it must be just canceled ones then. - if let canceledIntegration = sortedDesc.filter({ - $0.result! == Integration.Result.Canceled - }).first { - - let baseComment = self.baseCommentFromIntegration(canceledIntegration) - let status = self.createStatusFromState(.Error, description: "Build canceled!") - let comment = baseComment + "Build was manually canceled." - return (status: status, comment: comment) - } - - //hmm no idea, if we got all the way here. just leave it with no state. - let status = self.createStatusFromState(.NoState, description: nil) - return (status: status, comment: nil) - } - - private func createStatusFromState(state: Status.State, description: String?) -> Status { + //prepared sync pair + let syncPairs = syncPairsRaw.map({ + (syncPair: SyncPair) -> SyncPair in + syncPair.syncer = self + return syncPair + }) - //TODO: add useful targetUrl and potentially have multiple contexts to show multiple stats on the PR - let context = "Buildasaur" - let newDescription: String? - if let description = description { - newDescription = "\(context): \(description)" - } else { - newDescription = nil + if toCreate.count > 0 { + self.reports["Created bots"] = "\(toCreate.count)" } - return Status(state: state, description: newDescription, targetUrl: nil, context: context) - } - - //probably make these a bit more generic, something like an async reduce which calls completion when all finish - private func cancelIntegrations(integrations: [Integration], completion: () -> ()) { - - integrations.mapVoidAsync({ (integration, itemCompletion) -> () in - - self.xcodeServer.cancelIntegration(integration.id, completion: { (success, error) -> () in - if error != nil { - self.notifyError(error, context: "Failed to cancel integration \(integration.number)") - } else { - Log.info("Successfully cancelled integration \(integration.number)") - } - itemCompletion() - }) - - }, completion: completion) - } - - private func deleteBots(bots: [Bot], completion: () -> ()) { - - bots.mapVoidAsync({ (bot, itemCompletion) -> () in - - self.xcodeServer.deleteBot(bot.id, revision: bot.rev, completion: { (success, error) -> () in - - if error != nil { - self.notifyError(error, context: "Failed to delete bot with name \(bot.name)") - } else { - Log.info("Successfully deleted bot \(bot.name)") - } - itemCompletion() - }) - - }, completion: completion) - } - - private func createBotsFromPRs(prs: [PullRequest], completion: () -> ()) { - - prs.mapVoidAsync({ (item, itemCompletion) -> () in - self.createBotFromPR(item, completion: itemCompletion) - }, completion: completion) - } - - private func createBotFromPR(pr: PullRequest, completion: () -> ()) { - - /* - synced bots must have a manual schedule, Builda tells the bot to reintegrate in case of a new commit. - this has the advantage in cases when someone pushes 10 commits. if we were using Xcode Server's "On Commit" - schedule, it'd schedule 10 integrations, which could take ages. Builda's logic instead only schedules one - integration for the latest commit's SHA. - - even though this is desired behavior in this syncer, technically different syncers can have completely different - logic. here I'm just explaining why "On Commit" schedule isn't generally a good idea for when managed by Builda. - */ - let schedule = BotSchedule.manualBotSchedule() - let botName = self.nameForBotWithPR(pr, repoName: self.repoName()!) - let template = self.currentBuildTemplate() - - //to handle forks - let headOriginUrl = pr.head.repo.repoUrlSSH - let localProjectOriginUrl = self.localSource.projectURL!.absoluteString - - let project: LocalSource - if headOriginUrl != localProjectOriginUrl { - - //we have a fork, duplicate the metadata with the fork's origin - if let source = self.localSource.duplicateForForkAtOriginURL(headOriginUrl) { - project = source - } else { - self.notifyError(Errors.errorWithInfo("Couldn't create a LocalSource for fork with origin at url \(headOriginUrl)"), context: "Creating a bot from a PR") - completion() - return - } - } else { - //a normal PR in the same repo, no need to duplicate, just use the existing localSource - project = self.localSource + if toDelete.count > 0 { + self.reports["Deleted bots"] = "\(toDelete.count)" } - - let xcodeServer = self.xcodeServer - let branch = pr.head.ref - - XcodeServerSyncerUtils.createBotFromBuildTemplate(botName, template: template, project: project, branch: branch, scheduleOverride: schedule, xcodeServer: xcodeServer) { (bot, error) -> () in - - if error != nil { - self.notifyError(error, context: "Failed to create bot with name \(botName)") - } - completion() + if toSync.count > 0 { + self.reports["Synced bots"] = "\(toSync.count)" } - } - - private func currentBuildTemplate() -> BuildTemplate! { - if - let preferredTemplateId = self.localSource.preferredTemplateId, - let template = StorageManager.sharedInstance.buildTemplates.filter({ $0.uniqueId == preferredTemplateId }).first { - return template - } - - assertionFailure("Couldn't get the current build template, this syncer should NOT be running!") - return nil - } - - private func isBuildaBot(bot: Bot) -> Bool { - return bot.name.hasPrefix(self.prefixForBuildaBot()) - } - - private func isBuildaBotBelongingToRepoWithName(bot: Bot, repoName: String) -> Bool { - return bot.name.hasPrefix(self.prefixForBuildaBotInRepoWithName(repoName)) + return syncPairs } - private func nameForBotWithPR(pr: PullRequest, repoName: String) -> String { - return "\(self.prefixForBuildaBotInRepoWithName(repoName)) PR #\(pr.number)" - } - - private func prefixForBuildaBotInRepoWithName(repoName: String) -> String { - return "\(self.prefixForBuildaBot()) [\(repoName)]" - } - - private func prefixForBuildaBot() -> String { - return "BuildaBot" - } -} - -extension Array { - - func mapVoidAsync(transformAsync: (item: T, itemCompletion: () -> ()) -> (), completion: () -> ()) { - self.mapAsync(transformAsync, completion: { (_) -> () in - completion() - }) - } - - func mapAsync(transformAsync: (item: T, itemCompletion: (U) -> ()) -> (), completion: ([U]) -> ()) { + private func applyResolvedSyncPairs(syncPairs: [SyncPair], completion: () -> ()) { + //actually kick the sync pairs off let group = dispatch_group_create() - var returnedValueMap = [Int: U]() - - for (index, element) in enumerate(self) { + for i in syncPairs { dispatch_group_enter(group) - transformAsync(item: element, itemCompletion: { - (returned: U) -> () in - returnedValueMap[index] = returned + i.start({ (error) -> () in + if let error = error { + self.notifyError(error, context: "SyncPair: \(i.syncPairName())") + } dispatch_group_leave(group) }) } - dispatch_group_notify(group, dispatch_get_main_queue()) { - - //we have all the returned values in a map, put it back into an array of Us - var returnedValues = [U]() - for i in 0 ..< returnedValueMap.count { - returnedValues.append(returnedValueMap[i]!) - } - completion(returnedValues) - } + dispatch_group_notify(group, dispatch_get_main_queue(), completion) } - } diff --git a/Buildasaur/LocalSource.swift b/Buildasaur/LocalSource.swift index f9b2036..3412127 100644 --- a/Buildasaur/LocalSource.swift +++ b/Buildasaur/LocalSource.swift @@ -141,7 +141,7 @@ public class LocalSource : JSONSerializable { if self.parseCheckoutType(meta!) == nil { //disallowed let allowedString = ", ".join([AllowedCheckoutTypes.SSH].map({ $0.rawValue })) - let error = Errors.errorWithInfo("Disallowed checkout type, the project must be checked out over one of the supported schemes: \(allowedString)") + let error = Error.withInfo("Disallowed checkout type, the project must be checked out over one of the supported schemes: \(allowedString)") return (false, nil, error) } @@ -235,6 +235,17 @@ public class LocalSource : JSONSerializable { } } + public init() { + self.forkOriginURL = nil + self.availabilityState = .Unchecked + self.url = NSURL() + self.preferredTemplateId = nil + self.githubToken = nil + self.publicSSHKeyUrl = nil + self.privateSSHKeyUrl = nil + self.sshPassphrase = nil + } + public func jsonify() -> NSDictionary { var json = NSMutableDictionary() diff --git a/Buildasaur/NetworkUtils.swift b/Buildasaur/NetworkUtils.swift index e079fe1..814af23 100644 --- a/Buildasaur/NetworkUtils.swift +++ b/Buildasaur/NetworkUtils.swift @@ -38,9 +38,9 @@ class NetworkUtils { //look at the permissions in the PR metadata if !readPermission { - completion(success: false, error: Errors.errorWithInfo("Missing read permission for repo")) + completion(success: false, error: Error.withInfo("Missing read permission for repo")) } else if !writePermission { - completion(success: false, error: Errors.errorWithInfo("Missing write permission for repo")) + completion(success: false, error: Error.withInfo("Missing write permission for repo")) } else { //now test ssh keys self.checkValidityOfSSHKeys(credentialValidationBlueprint, completion: { (success, error) -> () in @@ -52,12 +52,12 @@ class NetworkUtils { }) } } else { - completion(success: false, error: Errors.errorWithInfo("Couldn't find repo permissions in GitHub response")) + completion(success: false, error: Error.withInfo("Couldn't find repo permissions in GitHub response")) } }) } else { - completion(success: false, error: Errors.errorWithInfo("Invalid repo name")) + completion(success: false, error: Error.withInfo("Invalid repo name")) } } @@ -95,7 +95,7 @@ class NetworkUtils { if r.terminationStatus == 0 { completion(success: true, error: nil) } else { - completion(success: false, error: Errors.errorWithInfo(r.standardError)) + completion(success: false, error: Error.withInfo(r.standardError)) } } } diff --git a/Buildasaur/SyncPair.swift b/Buildasaur/SyncPair.swift new file mode 100644 index 0000000..7d9980d --- /dev/null +++ b/Buildasaur/SyncPair.swift @@ -0,0 +1,58 @@ +// +// SyncPair.swift +// Buildasaur +// +// Created by Honza Dvorsky on 16/05/2015. +// Copyright (c) 2015 Honza Dvorsky. All rights reserved. +// + +import Foundation +import BuildaUtils + +/* +* this class describes the basic sync element: e.g. a PR + Bot, a branch + Bot, a branch + no bot, a bot + no PR +* each sync pair has its own behaviors (a branch + no bot creates a bot, a bot + no PR deletes the bot, +* a PR + Bot figures out what to do next, ...) +* this is simpler than trying to catch all cases in one giant syncer class (at least I think) +*/ +public class SyncPair { + + var syncer: HDGitHubXCBotSyncer! + + init() { + // + } + + typealias Completion = (error: NSError?) -> () + + /** + * Call to perform sync. + */ + final func start(completion: Completion) { + + let start = NSDate() + Log.verbose("SyncPair \(self.syncPairName()) started sync") + + self.sync { (error) -> () in + + let duration = -1 * start.timeIntervalSinceNow.clipTo(3) + Log.verbose("SyncPair \(self.syncPairName()) finished sync after \(duration) seconds.") + completion(error: error) + } + } + + /** + * To be overriden by subclasses. + */ + func sync(completion: Completion) { + assertionFailure("Must be overriden by subclasses") + } + + /** + * To be overriden by subclasses. + */ + func syncPairName() -> String { + assertionFailure("Must be overriden by subclasses") + return "" + } +} diff --git a/Buildasaur/SyncPair_NoPR_Bot.swift b/Buildasaur/SyncPair_NoPR_Bot.swift new file mode 100644 index 0000000..1f21496 --- /dev/null +++ b/Buildasaur/SyncPair_NoPR_Bot.swift @@ -0,0 +1,41 @@ +// +// SyncPair_NoPR_Bot.swift +// Buildasaur +// +// Created by Honza Dvorsky on 16/05/2015. +// Copyright (c) 2015 Honza Dvorsky. All rights reserved. +// + +import Foundation +import BuildaCIServer +import BuildaGitServer + +class SyncPair_NoPR_Bot: SyncPair { + + let bot: Bot + + init(bot: Bot) { + self.bot = bot + super.init() + } + + override func sync(completion: Completion) { + + //delete the bot + let syncer = self.syncer + let bot = self.bot + + SyncPair_NoPR_Bot.deleteBot(syncer: syncer, bot: bot, completion: completion) + } + + override func syncPairName() -> String { + return "No PR + Bot (\(self.bot.name))" + } + + private class func deleteBot(#syncer: HDGitHubXCBotSyncer, bot: Bot, completion: Completion) { + + syncer.deleteBot(bot, completion: { () -> () in + completion(error: nil) + }) + } +} diff --git a/Buildasaur/SyncPair_PR_Bot.swift b/Buildasaur/SyncPair_PR_Bot.swift new file mode 100644 index 0000000..8527afc --- /dev/null +++ b/Buildasaur/SyncPair_PR_Bot.swift @@ -0,0 +1,464 @@ +// +// SyncPair_PR_Bot.swift +// Buildasaur +// +// Created by Honza Dvorsky on 16/05/2015. +// Copyright (c) 2015 Honza Dvorsky. All rights reserved. +// + +import Foundation +import BuildaCIServer +import BuildaGitServer +import BuildaUtils + +class SyncPair_PR_Bot: SyncPair { + + let pr: PullRequest + let bot: Bot + + init(pr: PullRequest, bot: Bot) { + self.pr = pr + self.bot = bot + super.init() + } + + override func sync(completion: Completion) { + + //sync the PR and the Bot + let pr = self.pr + let bot = self.bot + let syncer = self.syncer + SyncPair_PR_Bot.syncPRWithBot(syncer: syncer, pr: pr, bot: bot) { (error) -> () in + + completion(error: error) + } + } + + override func syncPairName() -> String { + return "PR (\(self.pr.number):\(self.pr.head.ref)) + Bot (\(self.bot.name))" + } + + //MARK: Internal + + private class func syncPRWithBot(#syncer: HDGitHubXCBotSyncer, pr: PullRequest, bot: Bot, completion: Completion) { + + /* + TODO: we should establish some reliable and reasonable plan for how many integrations to fetch. + currently it's always 20, but some setups might have a crazy workflow with very frequent commits + on active bots etc. + */ + let query = [ + "last": "20" + ] + syncer.xcodeServer.getIntegrations(bot.id, query: query, completion: { (integrations, error) -> () in + + if let error = error { + let e = Error.withInfo("Bot \(bot.name) failed return integrations", internalError: error) + completion(error: e) + return + } + + if let integrations = integrations { + + //first check whether the bot is even enabled + self.isBotEnabled(syncer, pr: pr, integrations: integrations, completion: { (isEnabled, error) -> () in + + if let error = error { + completion(error: error) + return + } + + if isEnabled { + + self.syncPRWithBotIntegrations(syncer: syncer, pr: pr, bot: bot, integrations: integrations, completion: completion) + + } else { + + //not enabled, make sure the PR reflects that and the instructions are clear + Log.verbose("Bot \(bot.name) is not yet enabled, ignoring...") + + let status = syncer.createStatusFromState(.Pending, description: "Waiting for \"lttm\" to start testing") + let notYetEnabled: HDGitHubXCBotSyncer.GitHubStatusAndComment = (status: status, comment: nil) + syncer.updatePRStatusIfNecessary(notYetEnabled, prNumber: pr.number, completion: completion) + } + }) + } else { + let e = Error.withInfo("Getting integrations", internalError: Error.withInfo("Nil integrations even after returning nil error!")) + completion(error: e) + } + }) + } + + private class func isBotEnabled(syncer: HDGitHubXCBotSyncer, pr: PullRequest, integrations: [Integration], completion: (isEnabled: Bool, error: NSError?) -> ()) { + + //bot is enabled if (there are any integrations) OR (there is a recent comment with a keyword to enable the bot in the pull request's conversation) + //which means that there are two ways of enabling a bot. + //a) manually start an integration through Xcode, API call or in Builda's GUI (TBB) + //b) (optional) comment an agreed keyword in the Pull Request, e.g. "lttm" - 'looks testable to me' is a frequent one + + if integrations.count > 0 || !syncer.waitForLttm { + completion(isEnabled: true, error: nil) + return + } + + let keyword = ["lttm"] + + if let repoName = syncer.repoName() { + + syncer.github.findMatchingCommentInIssue(keyword, issue: pr.number, repo: repoName) { + (foundComments, error) -> () in + + if error != nil { + let e = Error.withInfo("Fetching comments", internalError: error) + completion(isEnabled: false, error: e) + return + } + + if let foundComments = foundComments { + completion(isEnabled: foundComments.count > 0, error: nil) + } else { + completion(isEnabled: false, error: nil) + } + } + + } else { + completion(isEnabled: false, error: Error.withInfo("No repo name, cannot find the GitHub repo!")) + } + } + + private class func syncPRWithBotIntegrations(#syncer: HDGitHubXCBotSyncer, pr: PullRequest, bot: Bot, integrations: [Integration], completion: Completion) { + + let uniqueIntegrations = Set(integrations) + + //------------ + // Split integrations into two groups: 1) for this SHA, 2) the rest + //------------ + + let headCommit: String = pr.head.sha + + //1) for this SHA + let headCommitIntegrations = uniqueIntegrations.filterSet { + (integration: Integration) -> Bool in + + //if it's not pending, we need to take a look at the blueprint and inspect the SHA. + if let blueprint = integration.blueprint, let sha = blueprint.commitSHA { + return sha == headCommit + } + + //when an integration is Pending, Preparing or Checking out, it doesn't have a blueprint, but it is, by definition, a headCommit + //integration (because it will check out the latest commit on the branch when it starts running) + if + integration.currentStep == .Pending || + integration.currentStep == .Preparing || + integration.currentStep == .Checkout + { + return true + } + + if integration.currentStep == .Completed { + + if let result = integration.result { + + //if the result doesn't have a SHA yet and isn't pending - take a look at the result + //if it's a checkout-error, assume that it is a malformed SSH key bot, so don't keep + //restarting integrations - at least until someone fixes it (by closing the PR and fixing + //their SSH keys in Buildasaur so that when the next bot gets created, it does so with the right + //SSH keys. + if result == .CheckoutError { + Log.error("Integration #\(integration.number) finished with a checkout error - please check that your SSH keys setup in Buildasaur are correct! If you need to fix them, please do so and then you need to recreate the bot - e.g. by closing the Pull Request, waiting for a sync (bot will disappear) and then reopening the Pull Request - should do the job!") + return true + } + + if result == .Canceled { + + //another case is when the integration gets doesn't yet have a blueprint AND was cancelled - + //we should assume it belongs to the latest commit, because we can't tell otherwise. + return true + } + } + } + + return false + } + + //2) the rest + let otherCommitIntegrations = uniqueIntegrations.subtract(headCommitIntegrations) + let noncompletedOtherCommitIntegrations: Set = otherCommitIntegrations.filterSet { + return $0.currentStep != .Completed + } + + let group = dispatch_group_create() + var lastGroupError: NSError? + + //2.1) Ok, now first cancel all unfinished integrations of the non-current commits + dispatch_group_enter(group) + syncer.cancelIntegrations(Array(noncompletedOtherCommitIntegrations), completion: { () -> () in + dispatch_group_leave(group) + }) + + //------------ + // Now we're resolving Integrations for the current commit only + //------------ + /* + The resolving logic goes like this now. We have an array of integrations I for the latest commits. + A. is array empty? + A1. true -> there are no integrations for this commit. kick one off! we're done. + A2. false -> keep resolving (all references to "integrations" below mean only integrations of the current commit + B. take all pending integrations, keep the most recent one, if it's there, cancel all the other ones. + C. take the running integration, if it's there + D. take all completed integrations + + resolve the status of the PR as follows + + E. is there a latest pending integration? + E1. true -> status is ["Pending": "Waiting on the queue"]. also, if there's a running integration, cancel it. + E2. false -> + F. is there a running integration? + F1. true -> status is ["Pending": "Integration in progress..."]. update status and do nothing else. + F2. false -> + G. are there any completed integrations? + G1. true -> based on the result of the integrations create the PR status + G2. false -> this shouldn't happen, print a very angry message. + */ + + //A. is this array empty? + if headCommitIntegrations.count == 0 { + + //A1. - it's empty, kick off an integration for the latest commit + dispatch_group_enter(group) + syncer.xcodeServer.postIntegration(bot.id, completion: { (integration, error) -> () in + + if let integration = integration where error == nil { + Log.info("Bot \(bot.name) successfully enqueued Integration #\(integration.number)") + } else { + let e = Error.withInfo("Bot \(bot.name) failed to enqueue an integration", internalError: error) + lastGroupError = e + } + + dispatch_group_leave(group) + }) + //nothing else to do + + } else { + + //A2. not empty, keep resolving + + //B. get pending Integrations + let pending = headCommitIntegrations.filterSet { + $0.currentStep == .Pending + } + + var latestPendingIntegration: Integration? + if pending.count > 0 { + + //we should cancel all but the most recent one + //turn the pending set into an array and sort by integration number in ascending order + var pendingSortedArray: Array = Array(pending).sorted({ (integrationA, integrationB) -> Bool in + return integrationA.number < integrationB.number + }) + + //keep the latest, which will be the last in the array + //let this one run, it might have been a force rebuild. + latestPendingIntegration = pendingSortedArray.removeLast() + + //however, cancel the rest of the pending integrations + dispatch_group_enter(group) + syncer.cancelIntegrations(pendingSortedArray) { + dispatch_group_leave(group) + } + } + + //Get the running integration, if it's there + let runningIntegration = headCommitIntegrations.filterSet { + $0.currentStep != .Completed && $0.currentStep != .Pending + }.first + + //Get all completed integrations for this commit + let completedIntegrations = headCommitIntegrations.filterSet { + $0.currentStep == .Completed + } + + //resolve + dispatch_group_enter(group) + self.resolvePRStatusFromLatestIntegrations(syncer: syncer, pending: latestPendingIntegration, running: runningIntegration, completed: completedIntegrations, completion: { (statusWithComment) -> () in + + //we now have the status and an optional comment to add. + //in order to know what to do, we need to fetch the current status of this commit first. + let repoName = syncer.repoName()! + syncer.github.getStatusOfCommit(headCommit, repo: repoName, completion: { (status, error) -> () in + + if error != nil { + let e = Error.withInfo("Failed to fetch status of commit \(headCommit) in repo \(repoName)", internalError: error) + lastGroupError = e + dispatch_group_leave(group) + return + } + + let updateStatus: Bool + if let currentStatus = status { + //we have the current status! + updateStatus = (statusWithComment.status != currentStatus) + } else { + //doesn't have a status yet, update + updateStatus = true + } + + if updateStatus { + + let oldStatus = status?.description ?? "[no status]" + let newStatus = statusWithComment + let comment = newStatus.comment ?? "[no comment]" + Log.info("Updating status of commit \(headCommit) in PR #\(pr.number) from \(oldStatus) to \(newStatus), will add comment \(comment)") + + //we need to update status + syncer.postStatusWithComment(statusWithComment, commit: headCommit, repo: repoName, pr: pr, completion: { (error) -> () in + if let error = error { + lastGroupError = error + } + dispatch_group_leave(group) + }) + + } else { + //everything is how it's supposed to be + dispatch_group_leave(group) + } + }) + }) + + } + + //when all actions finished, complete + dispatch_group_notify(group, dispatch_get_main_queue(), { + completion(error: lastGroupError) + }) + } + + private class func resolvePRStatusFromLatestIntegrations(#syncer: HDGitHubXCBotSyncer, pending: Integration?, running: Integration?, completed: Set, completion: (HDGitHubXCBotSyncer.GitHubStatusAndComment) -> ()) { + + let group = dispatch_group_create() + let statusWithComment: HDGitHubXCBotSyncer.GitHubStatusAndComment + + //if there's any pending integration, we're ["Pending" - Waiting in the queue] + if let pending = pending { + + //TODO: show how many builds are ahead in the queue and estimate when it will be + //started and when finished? (there is an average running time on each bot, it should be easy) + let status = syncer.createStatusFromState(.Pending, description: "Build waiting in the queue...") + statusWithComment = (status: status, comment: nil) + + //also, cancel the running integration, if it's there any + if let running = running { + dispatch_group_enter(group) + syncer.cancelIntegrations([running], completion: { () -> () in + dispatch_group_leave(group) + }) + } + } else { + + //there's no pending integration, it's down to running and completed + if let running = running { + + //there is a running integration. + //TODO: estimate, based on the average running time of this bot and on the started timestamp, when it will finish. add that to the description. + let currentStepString = running.currentStep.rawValue + let status = syncer.createStatusFromState(.Pending, description: "Integration step: \(currentStepString)...") + statusWithComment = (status: status, comment: nil) + + } else { + + //there no running integration, we're down to completed integration. + if completed.count > 0 { + + //we have some completed integrations + statusWithComment = self.resolveStatusFromCompletedIntegrations(syncer: syncer, integrations: completed) + + } else { + //this shouldn't happen. + Log.error("LOGIC ERROR! This shouldn't happen, there are no completed integrations!") + let status = syncer.createStatusFromState(.Error, description: "* UNKNOWN STATE, Builda ERROR *") + statusWithComment = (status: status, "Builda error, unknown state!") + } + } + } + + dispatch_group_notify(group, dispatch_get_main_queue()) { () -> Void in + completion(statusWithComment) + } + } + + private class func resolveStatusFromCompletedIntegrations(#syncer: HDGitHubXCBotSyncer, integrations: Set) -> HDGitHubXCBotSyncer.GitHubStatusAndComment { + + //get integrations sorted by number + let sortedDesc = Array(integrations).sorted { $0.number > $1.number } + + //if there are any succeeded, it wins - iterating from the end + if let passingIntegration = sortedDesc.filter({ + (integration: Integration) -> Bool in + switch integration.result! { + case Integration.Result.Succeeded, Integration.Result.Warnings, Integration.Result.AnalyzerWarnings: + return true + default: + return false + } + }).first { + + let baseComment = syncer.baseCommentFromIntegration(passingIntegration) + let comment: String + let status = syncer.createStatusFromState(.Success, description: "Build passed!") + let summary = passingIntegration.buildResultSummary! + if passingIntegration.result == .Succeeded { + comment = baseComment + "Perfect build! All \(summary.testsCount) tests passed. :+1:" + } else if passingIntegration.result == .Warnings { + comment = baseComment + "All \(summary.testsCount) tests passed, but please fix \(summary.warningCount) warnings." + } else { + comment = baseComment + "All \(summary.testsCount) tests passed, but please fix \(summary.analyzerWarningCount) analyzer warnings." + } + return (status: status, comment: comment) + } + + //ok, no succeeded, warnings or analyzer warnings, get down to test failures + if let testFailingIntegration = sortedDesc.filter({ + $0.result! == Integration.Result.TestFailures + }).first { + + let baseComment = syncer.baseCommentFromIntegration(testFailingIntegration) + let status = syncer.createStatusFromState(.Failure, description: "Build failed tests!") + let summary = testFailingIntegration.buildResultSummary! + let comment = baseComment + "Build failed \(summary.testFailureCount) tests out of \(summary.testsCount)" + return (status: status, comment: comment) + } + + //ok, the build didn't even run then. it either got cancelled or failed + if let erroredIntegration = sortedDesc.filter({ + $0.result! != Integration.Result.Canceled + }).first { + + let baseComment = syncer.baseCommentFromIntegration(erroredIntegration) + let errorCount: String + if let summary = erroredIntegration.buildResultSummary { + errorCount = "\(summary.errorCount)" + } else { + errorCount = "?" + } + let status = syncer.createStatusFromState(.Error, description: "Build error!") + let comment = baseComment + "\(errorCount) build errors: \(erroredIntegration.result!.rawValue)" + return (status: status, comment: comment) + } + + //cool, not even build error. it must be just canceled ones then. + if let canceledIntegration = sortedDesc.filter({ + $0.result! == Integration.Result.Canceled + }).first { + + let baseComment = syncer.baseCommentFromIntegration(canceledIntegration) + let status = syncer.createStatusFromState(.Error, description: "Build canceled!") + let comment = baseComment + "Build was manually canceled." + return (status: status, comment: comment) + } + + //hmm no idea, if we got all the way here. just leave it with no state. + let status = syncer.createStatusFromState(.NoState, description: nil) + return (status: status, comment: nil) + } +} + diff --git a/Buildasaur/SyncPair_PR_NoBot.swift b/Buildasaur/SyncPair_PR_NoBot.swift new file mode 100644 index 0000000..795b6d4 --- /dev/null +++ b/Buildasaur/SyncPair_PR_NoBot.swift @@ -0,0 +1,44 @@ +// +// SyncPair_PR_NoBot.swift +// Buildasaur +// +// Created by Honza Dvorsky on 16/05/2015. +// Copyright (c) 2015 Honza Dvorsky. All rights reserved. +// + +import Foundation +import BuildaCIServer +import BuildaGitServer + +class SyncPair_PR_NoBot: SyncPair { + + let pr: PullRequest + + init(pr: PullRequest) { + self.pr = pr + super.init() + } + + override func sync(completion: Completion) { + + //create a bot for this PR + let syncer = self.syncer + let pr = self.pr + + SyncPair_PR_NoBot.createBotForPR(syncer: syncer, pr: pr, completion: completion) + } + + override func syncPairName() -> String { + return "PR (\(self.pr.head.ref)) + No Bot" + } + + //MARK: Internal + + private class func createBotForPR(#syncer: HDGitHubXCBotSyncer, pr: PullRequest, completion: Completion) { + + syncer.createBotFromPR(pr, completion: { () -> () in + completion(error: nil) + }) + } + +} diff --git a/Buildasaur/Syncer.swift b/Buildasaur/Syncer.swift index f428b8f..d7900f6 100644 --- a/Buildasaur/Syncer.swift +++ b/Buildasaur/Syncer.swift @@ -117,6 +117,10 @@ public protocol SyncerDelegate: class { } } + func notifyErrorString(errorString: String, context: String?) { + self.notifyError(Error.withInfo(errorString), context: context) + } + func notifyError(error: NSError?, context: String?) { var message = "Syncing encountered a problem. " @@ -129,7 +133,7 @@ public protocol SyncerDelegate: class { } Log.error(message) self.currentSyncError = error - self.delegate?.syncerEncounteredError(self, error: Errors.errorWithInfo(message)) + self.delegate?.syncerEncounteredError(self, error: Error.withInfo(message)) } /** diff --git a/Buildasaur/SyncerBotManipulation.swift b/Buildasaur/SyncerBotManipulation.swift new file mode 100644 index 0000000..26d8ebe --- /dev/null +++ b/Buildasaur/SyncerBotManipulation.swift @@ -0,0 +1,105 @@ +// +// GitHubXCBotUtils.swift +// Buildasaur +// +// Created by Honza Dvorsky on 16/05/2015. +// Copyright (c) 2015 Honza Dvorsky. All rights reserved. +// + +import Foundation +import BuildaCIServer +import BuildaGitServer +import BuildaUtils + +extension HDGitHubXCBotSyncer { + + //MARK: Bot manipulation utils + + func cancelIntegrations(integrations: [Integration], completion: () -> ()) { + + integrations.mapVoidAsync({ (integration, itemCompletion) -> () in + + self.xcodeServer.cancelIntegration(integration.id, completion: { (success, error) -> () in + if error != nil { + self.notifyError(error, context: "Failed to cancel integration \(integration.number)") + } else { + Log.info("Successfully cancelled integration \(integration.number)") + } + itemCompletion() + }) + + }, completion: completion) + } + + func deleteBot(bot: Bot, completion: () -> ()) { + + self.xcodeServer.deleteBot(bot.id, revision: bot.rev, completion: { (success, error) -> () in + + if error != nil { + self.notifyError(error, context: "Failed to delete bot with name \(bot.name)") + } else { + Log.info("Successfully deleted bot \(bot.name)") + } + completion() + }) + } + + func createBotFromPR(pr: PullRequest, completion: () -> ()) { + + /* + synced bots must have a manual schedule, Builda tells the bot to reintegrate in case of a new commit. + this has the advantage in cases when someone pushes 10 commits. if we were using Xcode Server's "On Commit" + schedule, it'd schedule 10 integrations, which could take ages. Builda's logic instead only schedules one + integration for the latest commit's SHA. + + even though this is desired behavior in this syncer, technically different syncers can have completely different + logic. here I'm just explaining why "On Commit" schedule isn't generally a good idea for when managed by Builda. + */ + let schedule = BotSchedule.manualBotSchedule() + let botName = BotNaming.nameForBotWithPR(pr, repoName: self.repoName()!) + let template = self.currentBuildTemplate() + + //to handle forks + let headOriginUrl = pr.head.repo.repoUrlSSH + let localProjectOriginUrl = self.localSource.projectURL!.absoluteString + + let project: LocalSource + if headOriginUrl != localProjectOriginUrl { + + //we have a fork, duplicate the metadata with the fork's origin + if let source = self.localSource.duplicateForForkAtOriginURL(headOriginUrl) { + project = source + } else { + self.notifyError(Error.withInfo("Couldn't create a LocalSource for fork with origin at url \(headOriginUrl)"), context: "Creating a bot from a PR") + completion() + return + } + } else { + //a normal PR in the same repo, no need to duplicate, just use the existing localSource + project = self.localSource + } + + let xcodeServer = self.xcodeServer + let branch = pr.head.ref + + XcodeServerSyncerUtils.createBotFromBuildTemplate(botName, template: template, project: project, branch: branch, scheduleOverride: schedule, xcodeServer: xcodeServer) { (bot, error) -> () in + + if error != nil { + self.notifyError(error, context: "Failed to create bot with name \(botName)") + } + completion() + } + } + + private func currentBuildTemplate() -> BuildTemplate! { + + if + let preferredTemplateId = self.localSource.preferredTemplateId, + let template = StorageManager.sharedInstance.buildTemplates.filter({ $0.uniqueId == preferredTemplateId }).first { + return template + } + + assertionFailure("Couldn't get the current build template, this syncer should NOT be running!") + return nil + } +} diff --git a/Buildasaur/SyncerBotNaming.swift b/Buildasaur/SyncerBotNaming.swift new file mode 100644 index 0000000..a0a7ba0 --- /dev/null +++ b/Buildasaur/SyncerBotNaming.swift @@ -0,0 +1,34 @@ +// +// BotNaming.swift +// Buildasaur +// +// Created by Honza Dvorsky on 16/05/2015. +// Copyright (c) 2015 Honza Dvorsky. All rights reserved. +// + +import Foundation +import BuildaCIServer +import BuildaGitServer + +class BotNaming { + + class func isBuildaBot(bot: Bot) -> Bool { + return bot.name.hasPrefix(self.prefixForBuildaBot()) + } + + class func isBuildaBotBelongingToRepoWithName(bot: Bot, repoName: String) -> Bool { + return bot.name.hasPrefix(self.prefixForBuildaBotInRepoWithName(repoName)) + } + + class func nameForBotWithPR(pr: PullRequest, repoName: String) -> String { + return "\(self.prefixForBuildaBotInRepoWithName(repoName)) PR #\(pr.number)" + } + + class func prefixForBuildaBotInRepoWithName(repoName: String) -> String { + return "\(self.prefixForBuildaBot()) [\(repoName)]" + } + + class func prefixForBuildaBot() -> String { + return "BuildaBot" + } +} diff --git a/Buildasaur/SyncerBotUtils.swift b/Buildasaur/SyncerBotUtils.swift new file mode 100644 index 0000000..794c939 --- /dev/null +++ b/Buildasaur/SyncerBotUtils.swift @@ -0,0 +1,38 @@ +// +// SyncerBotUtils.swift +// Buildasaur +// +// Created by Honza Dvorsky on 16/05/2015. +// Copyright (c) 2015 Honza Dvorsky. All rights reserved. +// + +import Foundation +import BuildaCIServer +import BuildaGitServer +import BuildaUtils + +extension HDGitHubXCBotSyncer { + + func formattedDurationOfIntegration(integration: Integration) -> String? { + + if let seconds = integration.duration { + + var result = TimeUtils.secondsToNaturalTime(Int(seconds)) + return result + + } else { + Log.error("No duration provided in integration \(integration)") + return "[NOT PROVIDED]" + } + } + + func baseCommentFromIntegration(integration: Integration) -> String { + + var comment = "Result of integration \(integration.number)\n" + if let duration = self.formattedDurationOfIntegration(integration) { + comment += "Integration took " + duration + ".\n" + } + return comment + } + +} \ No newline at end of file diff --git a/Buildasaur/SyncerGitHubUtils.swift b/Buildasaur/SyncerGitHubUtils.swift new file mode 100644 index 0000000..c6441da --- /dev/null +++ b/Buildasaur/SyncerGitHubUtils.swift @@ -0,0 +1,101 @@ +// +// SyncerGitHubUtils.swift +// Buildasaur +// +// Created by Honza Dvorsky on 16/05/2015. +// Copyright (c) 2015 Honza Dvorsky. All rights reserved. +// + +import Foundation +import BuildaCIServer +import BuildaGitServer +import BuildaUtils + +extension HDGitHubXCBotSyncer { + + func createStatusFromState(state: Status.State, description: String?) -> Status { + + //TODO: add useful targetUrl and potentially have multiple contexts to show multiple stats on the PR + let context = "Buildasaur" + let newDescription: String? + if let description = description { + newDescription = "\(context): \(description)" + } else { + newDescription = nil + } + return Status(state: state, description: newDescription, targetUrl: nil, context: context) + } + + func updatePRStatusIfNecessary(newStatus: GitHubStatusAndComment, prNumber: Int, completion: SyncPair.Completion) { + + let repoName = self.repoName()! + + self.github.getPullRequest(prNumber, repo: repoName) { (pr, error) -> () in + + if error != nil { + let e = Error.withInfo("PR \(prNumber) failed to return data", internalError: error) + completion(error: e) + return + } + + if let pr = pr { + + let latestCommit = pr.head.sha + + self.github.getStatusOfCommit(latestCommit, repo: repoName, completion: { (status, error) -> () in + + if error != nil { + let e = Error.withInfo("PR \(prNumber) failed to return status", internalError: error) + completion(error: e) + return + } + + if status == nil || newStatus.status != status! { + + self.postStatusWithComment(newStatus, commit: latestCommit, repo: repoName, pr: pr, completion: completion) + + } else { + completion(error: nil) + } + }) + + } else { + let e = Error.withInfo("Fetching a PR", internalError: Error.withInfo("PR is nil and error is nil")) + completion(error: e) + } + } + } + + func postStatusWithComment(statusWithComment: GitHubStatusAndComment, commit: String, repo: String, pr: PullRequest, completion: SyncPair.Completion) { + + self.github.postStatusOfCommit(statusWithComment.status, sha: commit, repo: repo) { (status, error) -> () in + + if error != nil { + let e = Error.withInfo("Failed to post a status on commit \(commit) of repo \(repo)", internalError: error) + completion(error: e) + return + } + + //have a chance to NOT post a status comment... + let postStatusComments = self.postStatusComments + + //optional there can be a comment to be posted as well + if let comment = statusWithComment.comment where postStatusComments { + + //we have a comment, post it + self.github.postCommentOnIssue(comment, issueNumber: pr.number, repo: repo, completion: { (comment, error) -> () in + + if error != nil { + let e = Error.withInfo("Failed to post a comment \"\(comment)\" on PR \(pr.number) of repo \(repo)", internalError: error) + completion(error: e) + } else { + completion(error: nil) + } + }) + + } else { + completion(error: nil) + } + } + } +} diff --git a/Buildasaur/XcodeServerSyncerUtils.swift b/Buildasaur/XcodeServerSyncerUtils.swift index 91f3755..6a4ceae 100644 --- a/Buildasaur/XcodeServerSyncerUtils.swift +++ b/Buildasaur/XcodeServerSyncerUtils.swift @@ -47,7 +47,7 @@ class XcodeServerSyncerUtils { } else if let bot = bot { Log.info("Successfully created bot \(bot.name)") } else { - outError = Errors.errorWithInfo("Failed to return bot after creation even after error was nil!") + outError = Error.withInfo("Failed to return bot after creation even after error was nil!") Log.error(outError?.description ?? "") } NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in diff --git a/BuildasaurTests/Info.plist b/BuildasaurTests/Info.plist new file mode 100644 index 0000000..fb59dc8 --- /dev/null +++ b/BuildasaurTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.honzadvorsky.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/BuildasaurTests/SyncerTests.swift b/BuildasaurTests/SyncerTests.swift new file mode 100644 index 0000000..381ea11 --- /dev/null +++ b/BuildasaurTests/SyncerTests.swift @@ -0,0 +1,67 @@ +// +// SyncerTests.swift +// Buildasaur +// +// Created by Honza Dvorsky on 17/05/2015. +// Copyright (c) 2015 Honza Dvorsky. All rights reserved. +// + +import Foundation +import XCTest +import BuildaUtils +import BuildaGitServer +import BuildaCIServer +import Buildasaur + +class SyncerTests: XCTestCase { + + func mockedSyncer() -> HDGitHubXCBotSyncer { + class MockXcodeServer: XcodeServer { + init() { + let config = XcodeServerConfig(host: "", user: "", password: "") + super.init(config: config, endpoints: XcodeServerEndPoints(serverConfig: config)) + } + } + + class MockGitHubServer: GitHubServer { + init() { + super.init(endpoints: GitHubEndpoints(baseURL: "", token: "")) + } + } + + class MockLocalSource: LocalSource { + override init() { + super.init() + } + required init?(json: NSDictionary) { fatalError("init(json:) has not been implemented") } + } + + let xcodeServer = MockXcodeServer() + let githubServer = MockGitHubServer() + let project = MockLocalSource() + let syncInterval = 15.0 + let waitForLttm = true + let postStatusComments = true + + let syncer = HDGitHubXCBotSyncer(integrationServer: xcodeServer, sourceServer: githubServer, localSource: project, syncInterval: syncInterval, waitForLttm: waitForLttm, postStatusComments: postStatusComments) + return syncer + } + + func testCreatingChangeActions() { + + let syncer = self.mockedSyncer() + let (toSync, toCreate, toDelete) = syncer.resolvePRsAndBots(repoName: "Repo", prs: [], bots: []) + XCTAssert(toSync.count == 0) + XCTAssert(toCreate.count == 0) + XCTAssert(toDelete.count == 0) + } + + //TODO: figure out an easy way to mock PRs, Bots etc +} + + + + + + +