diff --git a/CHANGELOG.md b/CHANGELOG.md index 24be967..0b1db50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # xcode-github Change Log +## v1.1.1 - February 6, 2019 +* Fixed a bug where the PR branch was not checked out: Instead the template bot branch was used. + ## v1.1.0 - January 29, 2019 * Fixed Xcode server login. * Updated documentation. diff --git a/xcode-github-app/xcode-github-app.md b/Documentation/xcode-github-app.md similarity index 100% rename from xcode-github-app/xcode-github-app.md rename to Documentation/xcode-github-app.md diff --git a/xcode-github-cli/xcode-github-cli.md b/Documentation/xcode-github-cli.md similarity index 99% rename from xcode-github-cli/xcode-github-cli.md rename to Documentation/xcode-github-cli.md index 1acde35..eec1738 100644 --- a/xcode-github-cli/xcode-github-cli.md +++ b/Documentation/xcode-github-cli.md @@ -102,6 +102,7 @@ GitHub "Close PR" Event -> xcode-github deletes Xcode Bot ### Xcode Bot Documentation * [Xcode Bot Documentation](https://developer.apple.com/library/content/documentation/Xcode/Conceptual/XcodeServerAPIReference/Bots.html) +* Debugging hint: In console include messages with subsystem contains `xcsd`. #### Xcode Schemes * [Xcode URI Scheme Examples](https://cocoaengineering.com/2018/01/01/some-useful-url-schemes-in-xcode-9/) diff --git a/XcodeGitHub/XGGitHubPullRequest.h b/XcodeGitHub/XGGitHubPullRequest.h index ba51b9e..a2b97d6 100644 --- a/XcodeGitHub/XGGitHubPullRequest.h +++ b/XcodeGitHub/XGGitHubPullRequest.h @@ -42,7 +42,8 @@ FOUNDATION_EXPORT NSString*_Nonnull NSStringFromXGPullRequestStatus(XGPullReques @property (strong, readonly) NSString*_Nullable body; @property (strong, readonly) NSString*_Nullable state; @property (strong, readonly) NSDictionary*_Nullable dictionary; -@property (strong, readonly) NSString*_Nullable sha; +@property (strong, readonly) NSString*_Nullable headSHA; +@property (strong, readonly) NSString*_Nullable baseSHA; @property (strong, readonly) NSString*_Nullable githubPRURL; + (instancetype _Nonnull) new NS_UNAVAILABLE; diff --git a/XcodeGitHub/XGGitHubPullRequest.m b/XcodeGitHub/XGGitHubPullRequest.m index 48d169f..46c0061 100644 --- a/XcodeGitHub/XGGitHubPullRequest.m +++ b/XcodeGitHub/XGGitHubPullRequest.m @@ -99,7 +99,8 @@ - (instancetype) initWithDictionary:(NSDictionary *)dictionary { NSInteger index = range.location + range.length; if (index < fullname.length) _repoName = [fullname substringFromIndex:index]; } - _sha = _dictionary[@"head"][@"sha"]; + _headSHA = _dictionary[@"head"][@"sha"]; + _baseSHA = _dictionary[@"base"][@"sha"]; _githubPRURL = _dictionary[@"url"]; return self; } @@ -231,7 +232,7 @@ + (NSString*) stringFromStatus:(XGPullRequestStatus)status { { NSString* string = [NSString stringWithFormat: @"https://api.github.com/repos/%@/%@/commits/%@/statuses", - self.repoOwner, self.repoName, self.sha]; + self.repoOwner, self.repoName, self.headSHA]; NSURL *URL = [NSURL URLWithString:string]; if (!URL) { error = @@ -305,7 +306,7 @@ - (NSError*_Nullable) setStatus:(XGPullRequestStatus)status NSError *error = nil; NSString* string = [NSString stringWithFormat: @"https://api.github.com/repos/%@/%@/statuses/%@", - self.repoOwner, self.repoName, self.sha]; + self.repoOwner, self.repoName, self.headSHA]; NSURL *URL = [NSURL URLWithString:string]; if (!URL) { error = @@ -366,7 +367,7 @@ - (NSError*_Nullable) addComment:(NSString*)comment { NSError *error = nil; NSString* string = [NSString stringWithFormat: @"https://api.github.com/repos/%@/%@/commits/%@/comments", - self.repoOwner, self.repoName, self.sha]; + self.repoOwner, self.repoName, self.headSHA]; NSURL *URL = [NSURL URLWithString:string]; if (!URL) { error = diff --git a/XcodeGitHub/XGXcodeBot.m b/XcodeGitHub/XGXcodeBot.m index 6cb710f..abf3632 100644 --- a/XcodeGitHub/XGXcodeBot.m +++ b/XcodeGitHub/XGXcodeBot.m @@ -295,7 +295,10 @@ - (instancetype) initWithServer:(XGServer*)server dictionary:(NSDictionary *)dic _repoOwner = [self.sourceControlRepository substringWithRange:NSMakeRange(ownerRange.location+1, repoRange.location - ownerRange.location - 1)]; _repoName = [self.sourceControlRepository - substringWithRange:NSMakeRange(repoRange.location+1, self.sourceControlRepository.length - repoRange.location - 1)]; + substringWithRange:NSMakeRange( + repoRange.location+1, + self.sourceControlRepository.length - repoRange.location - 1 + )]; if ([_repoName hasSuffix:@".git"]) { _repoName = [_repoName substringWithRange:NSMakeRange(0, _repoName.length-4)]; } @@ -511,10 +514,21 @@ - (XGXcodeBot*_Nullable) duplicateBotWithNewName:(NSString*_Nonnull)newBotName XGXcodeBot *bot = nil; NSError *localError = nil; { + // Simply using the 'duplicate' bot api and changing the git branch won't actually change + // the git branch to the branch of the PR. + // + // Steps: + // 1. Duplicate the bot. + // 2. Get the new bot. + // 3. Modify the bot to the new branch with a PATCH. + + // + // Duplicate the bot: + // + NSString *string = [NSString stringWithFormat:@"https://%@:20343/api/bots/%@/duplicate", - self.server.server, - self.botID]; + self.server.server, self.botID]; NSURL *URL = [NSURL URLWithString:string]; if (!URL) { localError = @@ -528,47 +542,143 @@ - (XGXcodeBot*_Nullable) duplicateBotWithNewName:(NSString*_Nonnull)newBotName BNCLogError(@"Bad server name '%@'.", self.server.server); goto exit; } + __auto_type dictionary = [NSMutableDictionary new]; + dictionary[@"name"] = newBotName; + dictionary[@"templateBotName"] = self.name; + dictionary[@"pullRequestNumber"] = pullRequestNumber; + dictionary[@"pullRequestTitle"] = pullRequestTitle; + + NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&localError]; + if (!data) { + localError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError + userInfo:@{ NSLocalizedDescriptionKey: @"Can't create bot dictionary."}]; + } + if (localError || !data) goto exit; + + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + BNCNetworkOperation *operation = + [[BNCNetworkService shared] + postOperationWithURL:URL + contentType:@"application/json" + data:data + completion:^(BNCNetworkOperation *operation) { + dispatch_semaphore_signal(semaphore); + }]; + if (self.server.user.length > 0) + [operation setUser:self.server.user password:self.server.password]; + [operation.request addValue:@"7" forHTTPHeaderField:@"X-XCSClientVersion"]; + [operation start]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + if (operation.error) { + localError = operation.error; + goto exit; + } + if (operation.HTTPStatusCode != 201) { + localError = [NSError errorWithDomain:NSNetServicesErrorDomain + code:NSNetServicesInvalidError userInfo:@{NSLocalizedDescriptionKey: + [NSString stringWithFormat:@"HTTP Status %ld", (long) operation.HTTPStatusCode]}]; + BNCLogDebug(@"Response was: %@.", [operation stringFromResponseData]); + goto exit; + } + [operation deserializeJSONResponseData]; + NSDictionary *d = (id) operation.responseData; + if (!([d isKindOfClass:NSDictionary.class] && [d[@"_id"] isKindOfClass:NSString.class])) { + localError = + [NSError errorWithDomain:NSNetServicesErrorDomain + code:NSURLErrorBadServerResponse + userInfo:@{ NSLocalizedDescriptionKey: @"Expected an Xcode bot response." }]; + goto exit; + } + NSString*newBotID = d[@"_id"]; + + // + // Get the just created bot: + // + + string = + [NSString stringWithFormat:@"https://%@:20343/api/bots/%@", + self.server.server, newBotID]; + URL = [NSURL URLWithString:string]; + semaphore = dispatch_semaphore_create(0); + operation = + [[BNCNetworkService shared] + getOperationWithURL:URL + completion:^(BNCNetworkOperation *operation) { + dispatch_semaphore_signal(semaphore); + }]; + if (self.server.user.length > 0) + [operation setUser:self.server.user password:self.server.password]; + [operation.request addValue:@"7" forHTTPHeaderField:@"X-XCSClientVersion"]; + [operation start]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + if (operation.error) { + localError = operation.error; + goto exit; + } + if (operation.HTTPStatusCode != 200) { + localError = [NSError errorWithDomain:NSNetServicesErrorDomain + code:NSNetServicesInvalidError userInfo:@{NSLocalizedDescriptionKey: + [NSString stringWithFormat:@"HTTP Status %ld", (long) operation.HTTPStatusCode]}]; + BNCLogDebug(@"Response was: %@.", [operation stringFromResponseData]); + goto exit; + } + [operation deserializeJSONResponseData]; + d = (id) operation.responseData; + if (!([d isKindOfClass:NSDictionary.class] && + [d[@"_id"] isKindOfClass:NSString.class] && + [newBotID isEqualToString:d[@"_id"]])) { + localError = + [NSError errorWithDomain:NSNetServicesErrorDomain + code:NSURLErrorBadServerResponse + userInfo:@{ NSLocalizedDescriptionKey: @"Expected an Xcode bot response." }]; + goto exit; + } + + // + // Fix up the git branch of the duplicated bot: + // - NSMutableDictionary *dictionary = (__bridge_transfer NSMutableDictionary*) + string = + [NSString stringWithFormat:@"https://%@:20343/api/bots/%@?overwriteBlueprint=true", + self.server.server, newBotID]; + URL = [NSURL URLWithString:string]; + + dictionary = (__bridge_transfer NSMutableDictionary*) CFPropertyListCreateDeepCopy( kCFAllocatorDefault, - (CFDictionaryRef)self.dictionary, + (CFDictionaryRef)d, kCFPropertyListMutableContainers ); - dictionary[@"configuration"] - [@"sourceControlBlueprint"] - [@"DVTSourceControlWorkspaceBlueprintIdentifierKey"] = - [NSUUID UUID].UUIDString; + dictionary[@"_id"] = nil; + dictionary[@"_rev"] = nil; + dictionary[@"tinyID"] = nil; dictionary[@"configuration"] [@"sourceControlBlueprint"] [@"DVTSourceControlWorkspaceBlueprintLocationsKey"] [self.sourceControlWorkspaceBlueprintLocationsID] - [@"DVTSourceControlBranchIdentifierKey"] = - branchName; + [@"DVTSourceControlBranchIdentifierKey"] + = branchName; dictionary[@"configuration"] [@"sourceControlBlueprint"] - [@"DVTSourceControlWorkspaceBlueprintLocationsKey"] - [self.sourceControlWorkspaceBlueprintLocationsID] - [@"DVTSourceControlLocationRevisionKey"] = - nil; + [@"DVTSourceControlWorkspaceBlueprintRemoteRepositoryAuthenticationStrategiesKey"] + = @{}; + dictionary[@"configuration"] + [@"sourceControlBlueprint"] + [@"DVTSourceControlWorkspaceBlueprintIdentifierKey"] + = [NSUUID UUID].UUIDString; dictionary[@"configuration"][@"scheduleType"] = @2; // 2: On commit - dictionary[@"integration_counter"] = @0; - dictionary[@"lastRevisionBlueprint"] = @{}; - dictionary[@"name"] = newBotName; - dictionary[@"templateBotName"] = self.name; - dictionary[@"pullRequestNumber"] = pullRequestNumber; - dictionary[@"pullRequestTitle"] = pullRequestTitle; - dictionary[@"sourceControlBlueprintIdentifier"] = [NSUUID UUID].UUIDString; - - NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&localError]; + dictionary[@"sourceControlBlueprintIdentifier"] = nil; + dictionary[@"lastRevisionBlueprint"] = nil; + + data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&localError]; if (!data) { localError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:@{ NSLocalizedDescriptionKey: @"Can't create bot dictionary."}]; } if (localError || !data) goto exit; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - BNCNetworkOperation *operation = + semaphore = dispatch_semaphore_create(0); + operation = [[BNCNetworkService shared] postOperationWithURL:URL contentType:@"application/json" @@ -578,23 +688,23 @@ - (XGXcodeBot*_Nullable) duplicateBotWithNewName:(NSString*_Nonnull)newBotName }]; if (self.server.user.length > 0) [operation setUser:self.server.user password:self.server.password]; + operation.request.HTTPMethod = @"PATCH"; + [operation.request addValue:@"7" forHTTPHeaderField:@"X-XCSClientVersion"]; [operation start]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); if (operation.error) { localError = operation.error; goto exit; } - - if (operation.HTTPStatusCode != 201) { + if (operation.HTTPStatusCode != 200) { localError = [NSError errorWithDomain:NSNetServicesErrorDomain code:NSNetServicesInvalidError userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"HTTP Status %ld", (long) operation.HTTPStatusCode]}]; BNCLogDebug(@"Response was: %@.", [operation stringFromResponseData]); goto exit; } - [operation deserializeJSONResponseData]; - NSDictionary *d = (id) operation.responseData; + d = (id) operation.responseData; if ([d isKindOfClass:NSDictionary.class]) { bot = [[XGXcodeBot alloc] initWithServer:self.server dictionary:d]; if (bot) { diff --git a/xcode-github-app.xcodeproj/project.pbxproj b/xcode-github-app.xcodeproj/project.pbxproj index beba32b..da32cd5 100644 --- a/xcode-github-app.xcodeproj/project.pbxproj +++ b/xcode-github-app.xcodeproj/project.pbxproj @@ -53,7 +53,8 @@ /* Begin PBXFileReference section */ 2BB6F4D221F9E3FF001E8693 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = Base; path = Base.lproj/Credits.rtf; sourceTree = ""; }; - 4D0DD54420597FE6001721C1 /* xcode-github-app.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = "xcode-github-app.md"; path = "xcode-github-app/xcode-github-app.md"; sourceTree = ""; }; + 2BC31A1222041978003B0EE4 /* xcode-github-cli.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = "xcode-github-cli.md"; path = "Documentation/xcode-github-cli.md"; sourceTree = ""; }; + 4D0DD54420597FE6001721C1 /* xcode-github-app.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = "xcode-github-app.md"; path = "Documentation/xcode-github-app.md"; sourceTree = ""; }; 4D12BA0A20A252CE00E5B1DB /* XGALogViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XGALogViewController.h; sourceTree = ""; }; 4D12BA0B20A252CE00E5B1DB /* XGALogViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XGALogViewController.m; sourceTree = ""; }; 4D12BA0C20A252CE00E5B1DB /* XGALogViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = XGALogViewController.xib; sourceTree = ""; }; @@ -114,6 +115,7 @@ isa = PBXGroup; children = ( 4D0DD54420597FE6001721C1 /* xcode-github-app.md */, + 2BC31A1222041978003B0EE4 /* xcode-github-cli.md */, 4DC9E5C4216DCD6700F1F4EE /* XcodeGitHub.xcodeproj */, 4D76616A2057565200216B72 /* xcode-github-app */, 4D7661692057565200216B72 /* Products */, diff --git a/xcode-github-app/Base.lproj/Main.storyboard b/xcode-github-app/Base.lproj/Main.storyboard index 00d7a93..11cdb75 100644 --- a/xcode-github-app/Base.lproj/Main.storyboard +++ b/xcode-github-app/Base.lproj/Main.storyboard @@ -63,7 +63,7 @@ - + diff --git a/xcode-github-app/XGAAppDelegate.m b/xcode-github-app/XGAAppDelegate.m index 2d92f8f..2b2d386 100644 --- a/xcode-github-app/XGAAppDelegate.m +++ b/xcode-github-app/XGAAppDelegate.m @@ -15,6 +15,11 @@ #import "XGASettings.h" #import +/* + All about About Panels: + http://cocoadevcentral.com/articles/000071.php +*/ + @interface XGAAppDelegate () @property (nonatomic, strong) IBOutlet XGALogViewController*logController; @property (nonatomic, strong) IBOutlet XGAStatusViewController*statusController; @@ -54,8 +59,11 @@ - (IBAction)addNewServer:(id)sender { } - (BOOL)windowShouldClose:(NSWindow*)window { - self.preferencesController = nil; - return YES; + if (window == self.preferencesController.window) { + self.preferencesController = nil; + return YES; + } + return NO; } - (void)applicationDidFinishLaunching:(NSNotification *)notification { diff --git a/xcode-github-app/XGAStatusViewController.m b/xcode-github-app/XGAStatusViewController.m index 0598144..b351507 100644 --- a/xcode-github-app/XGAStatusViewController.m +++ b/xcode-github-app/XGAStatusViewController.m @@ -113,6 +113,9 @@ - (IBAction) showInfo:(id)sender { [detail append:status.statusDetail]; if (status.botName.length) [detail italicText:@"\n\nBot: %@", status.botName]; + if (status.branchOrPRName.length) + [detail italicText:@"\nRepo: %@/%@ %@", + status.bot.repoOwner, status.bot.repoName, status.bot.branch]; self.statusPopover.detailTextField.attributedStringValue = [detail renderAttributedStringWithFont:font]; @@ -231,6 +234,18 @@ - (IBAction)startStopIntegration:(id)sender { }); } +- (IBAction)showPullRequest:(id)sender { + XGAStatusViewItem*item = [self selectedTableItem]; + if (item.bot.repoOwner == nil || item.bot.repoName == nil || item.bot.pullRequestNumber == nil) + return; + + NSString*string = [NSString stringWithFormat:@"https://github.com/%@/%@/pull/%@", + item.bot.repoOwner, item.bot.repoName, item.bot.pullRequestNumber]; + NSURL*URL = [NSURL URLWithString:string]; + if (!URL) return; + [[NSWorkspace sharedWorkspace] openURL:URL]; +} + - (IBAction)reload:(id)sender { [self updateStatusNow]; } @@ -267,16 +282,24 @@ - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { } return NO; } + XGAStatusViewItem*statusItem = [self selectedTableItem]; if (menuItem.action == @selector(startStopIntegration:)) { - XGAStatusViewItem*item = [self selectedTableItem]; - if (!item) return NO; - if (item.botStatus.integrationID != nil && - ![item.botStatus.currentStep isEqualToString:@"completed"]) + if (!statusItem) return NO; + if (statusItem.botStatus.integrationID != nil && + ![statusItem.botStatus.currentStep isEqualToString:@"completed"]) menuItem.title = @"Cancel Integration"; else menuItem.title = @"Start Integration"; return YES; } + if (menuItem.action == @selector(showPullRequest:)) { + if (statusItem.bot.repoOwner == nil || + statusItem.bot.repoName == nil || + statusItem.bot.pullRequestNumber == nil) + return NO; + return YES; + } + SEL contextMenuItems[] = { @selector(showInfo:), @selector(monitorRepo:), diff --git a/xcode-github-app/XGAStatusViewController.xib b/xcode-github-app/XGAStatusViewController.xib index c74d983..9f82e98 100644 --- a/xcode-github-app/XGAStatusViewController.xib +++ b/xcode-github-app/XGAStatusViewController.xib @@ -306,6 +306,12 @@ + + + + + + diff --git a/xcode-github-cli.xcodeproj/project.pbxproj b/xcode-github-cli.xcodeproj/project.pbxproj index 76c9493..0a847ca 100644 --- a/xcode-github-cli.xcodeproj/project.pbxproj +++ b/xcode-github-cli.xcodeproj/project.pbxproj @@ -12,7 +12,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 4D7335452097CF6700A0D416 /* xcode-github-cli.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = "xcode-github-cli.md"; path = "xcode-github-cli/xcode-github-cli.md"; sourceTree = ""; }; + 4D7335452097CF6700A0D416 /* xcode-github-cli.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = "xcode-github-cli.md"; path = "Documentation/xcode-github-cli.md"; sourceTree = ""; }; 4D744C032047A53B002CA796 /* xcode-github */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "xcode-github"; sourceTree = BUILT_PRODUCTS_DIR; }; 4D744C062047A53B002CA796 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 4DDAA55A216AEC95002F3F8E /* XcodeGitHub.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XcodeGitHub.framework; path = Products/XcodeGitHub.framework; sourceTree = ""; };