From 498bf96661e50136f887e08aecfbe8895d87fc9a Mon Sep 17 00:00:00 2001 From: Felix Lapalme Date: Fri, 28 Jun 2024 21:21:17 -0400 Subject: [PATCH] Create a test for `rebaseBranch` (with a basic conflict) (#989) * Create `rebaseBranch` tests and move `resolveConflictsWithResolver` method into GCLiveRepository category * Reorganize rebase tests to include a simple rebase, one with conflict and one with multiple commits and conflicts (the last one fails with the current version) * Add GCLiveRepository+Conflicts.m to the iOS GitUpKit target * Fix release compilation by making GCLiveRepository+Conflicts.h public --- GitUp/Application/Document.m | 2 +- GitUpKit/Core/GCLiveRepository+Conflicts.h | 24 ++ GitUpKit/Core/GCLiveRepository+Conflicts.m | 73 ++++++ GitUpKit/Core/GCLiveRepository-Tests.m | 221 ++++++++++++++++++ GitUpKit/Core/GCTestCase.h | 4 + GitUpKit/Core/GCTestCase.m | 41 +++- GitUpKit/GitUpKit.xcodeproj/project.pbxproj | 16 ++ .../Utilities/GIViewController+Utilities.h | 8 +- .../Utilities/GIViewController+Utilities.m | 57 +---- .../Views/GICommitRewriterViewController.h | 2 +- .../Views/GICommitSplitterViewController.h | 2 +- GitUpKit/Views/GIMapViewController.h | 3 +- 12 files changed, 394 insertions(+), 59 deletions(-) create mode 100644 GitUpKit/Core/GCLiveRepository+Conflicts.h create mode 100644 GitUpKit/Core/GCLiveRepository+Conflicts.m create mode 100644 GitUpKit/Core/GCLiveRepository-Tests.m diff --git a/GitUp/Application/Document.m b/GitUp/Application/Document.m index ad908774..725ede12 100644 --- a/GitUp/Application/Document.m +++ b/GitUp/Application/Document.m @@ -1532,7 +1532,7 @@ - (void)conflictResolverViewControllerDidFinish:(GIConflictResolverViewControlle _resolvingConflicts = 1; } -#pragma mark - GIMergeConflictResolver +#pragma mark - GCMergeConflictResolver - (BOOL)resolveMergeConflictsWithOurCommit:(GCCommit*)ourCommit theirCommit:(GCCommit*)theirCommit { [self _enterResolveWithOurCommit:ourCommit theirCommit:theirCommit]; diff --git a/GitUpKit/Core/GCLiveRepository+Conflicts.h b/GitUpKit/Core/GCLiveRepository+Conflicts.h new file mode 100644 index 00000000..20a75740 --- /dev/null +++ b/GitUpKit/Core/GCLiveRepository+Conflicts.h @@ -0,0 +1,24 @@ +// +// GCLiveRepository+Conflicts.h +// GitUpKit (macOS) +// +// Created by Felix Lapalme on 2024-04-13. +// + +#import + +@protocol GCMergeConflictResolver +- (BOOL)resolveMergeConflictsWithOurCommit:(GCCommit*)ourCommit theirCommit:(GCCommit*)theirCommit; +@end + +@interface GCLiveRepository (Conflicts) + +- (GCCommit*)resolveConflictsWithResolver:(id)resolver + index:(GCIndex*)index + ourCommit:(GCCommit*)ourCommit + theirCommit:(GCCommit*)theirCommit + parentCommits:(NSArray*)parentCommits + message:(NSString*)message + error:(NSError**)error; + +@end diff --git a/GitUpKit/Core/GCLiveRepository+Conflicts.m b/GitUpKit/Core/GCLiveRepository+Conflicts.m new file mode 100644 index 00000000..d0035e27 --- /dev/null +++ b/GitUpKit/Core/GCLiveRepository+Conflicts.m @@ -0,0 +1,73 @@ +// +// GCLiveRepository+Conflicts.m +// GitUpKit (macOS) +// +// Created by Felix Lapalme on 2024-04-13. +// + +#import "GCLiveRepository+Conflicts.h" + +#import "GCRepository+Utilities.h" +#import "GCRepository+HEAD.h" + +@implementation GCLiveRepository (Conflicts) + +- (GCCommit*)resolveConflictsWithResolver:(id)resolver + index:(GCIndex*)index + ourCommit:(GCCommit*)ourCommit + theirCommit:(GCCommit*)theirCommit + parentCommits:(NSArray*)parentCommits + message:(NSString*)message + error:(NSError**)error { + + + // Save HEAD + GCCommit* headCommit; + GCLocalBranch* headBranch; + if (![self lookupHEADCurrentCommit:&headCommit branch:&headBranch error:error]) { + return nil; + } + + // Detach HEAD to "ours" commit + if (![self checkoutCommit:parentCommits[0] options:0 error:error]) { + return nil; + } + + // Check out index with conflicts + if (![self checkoutIndex:index withOptions:kGCCheckoutOption_UpdateSubmodulesRecursively error:error]) { + return nil; + } + + // Have user resolve conflicts + BOOL resolved = [resolver resolveMergeConflictsWithOurCommit:ourCommit theirCommit:theirCommit]; + + // Unless user cancelled, create commit with "ours" and "theirs" parent commits (if applicable) + GCCommit* commit = nil; + if (resolved) { + if (![self syncIndexWithWorkingDirectory:error]) { + return nil; + } + commit = [self createCommitFromHEADAndOtherParent:(parentCommits.count > 1 ? parentCommits[1] : nil) withMessage:message error:error]; + if (commit == nil) { + return nil; + } + } + + // Restore HEAD + if ((headBranch && ![self setHEADToReference:headBranch error:error]) || (!headBranch && ![self setDetachedHEADToCommit:headCommit error:error])) { + return nil; + } + if (![self forceCheckoutHEAD:YES error:error]) { + return nil; + } + + // Check if user cancelled + if (!resolved) { + GC_SET_USER_CANCELLED_ERROR(); + return nil; + } + + return commit; +} + +@end diff --git a/GitUpKit/Core/GCLiveRepository-Tests.m b/GitUpKit/Core/GCLiveRepository-Tests.m new file mode 100644 index 00000000..0f584646 --- /dev/null +++ b/GitUpKit/Core/GCLiveRepository-Tests.m @@ -0,0 +1,221 @@ +// +// GCLiveRepository-Tests.m +// Tests +// +// Created by Felix Lapalme on 2024-04-12. +// + +#import +#import "GCTestCase.h" +#import "GCHistory+Rewrite.h" +#import "GCRepository+Index.h" +#import "GCLiveRepository+Conflicts.h" +#import "GIViewController+Utilities.h" + +// block based object that conforms to GCMergeConflictResolver +@interface GCBlockConflictResolver : NSObject +@property(nonatomic, copy) BOOL (^resolveBlock)(GCCommit* ourCommit, GCCommit* theirCommit); + +- (instancetype)initWithBlock:(BOOL (^)(GCCommit* ourCommit, GCCommit* theirCommit))resolveBlock; +@end + +@implementation GCBlockConflictResolver + +- (instancetype)initWithBlock:(BOOL (^)(GCCommit* ourCommit, GCCommit* theirCommit))resolveBlock { + self = [super init]; + if (self) { + self.resolveBlock = resolveBlock; + } + return self; +} + +- (BOOL)resolveMergeConflictsWithOurCommit:(GCCommit*)ourCommit theirCommit:(GCCommit*)theirCommit { + return self.resolveBlock(ourCommit, theirCommit); +} + +@end + +@implementation GCEmptyLiveRepositoryTestCase (GCLiveRepository) + +- (void)testRebase { + // Initial setup: create a base commit on master. + GCCommit* baseCommit = [self makeCommitWithUpdatedFileAtPath:@"shared.txt" string:@"Initial content\n" message:@"Base commit"]; + + // Create a new branch from the base commit. + XCTAssertTrue([self.liveRepository createLocalBranchFromCommit:baseCommit withName:@"other_branch" force:NO error:NULL]); + + GCLocalBranch* masterBranch = [self.liveRepository findLocalBranchWithName:@"master" error:NULL]; + GCLocalBranch* otherBranch = [self.liveRepository findLocalBranchWithName:@"other_branch" error:NULL]; + + XCTAssertTrue([self.liveRepository checkoutLocalBranch:masterBranch options:0 error:NULL]); + GCCommit* masterCommit = [self makeCommitWithUpdatedFileAtPath:@"shared2.txt" string:@"new text file\n" message:@"Master commit 1"]; + XCTAssertNotNil(masterCommit); + + NSError* error; + + // Make a commit on the other branch that also modifies the same shared file. + XCTAssertTrue([self.liveRepository checkoutLocalBranch:otherBranch options:0 error:NULL]); + GCCommit* otherCommit1 = [self makeCommitWithUpdatedFileAtPath:@"shared.txt" string:@"Initial content\nAnd a new line\n" message:@"Other commit 1"]; + XCTAssertNotNil(otherCommit1); + + XCTAssertNil(error); + + [self rebaseAndSolveConflictsWithBaseCommit:baseCommit expectedCommitTotalCount:3]; + + // Verify the results of the rebase. + NSString* finalContent = [NSString stringWithContentsOfFile:[self.liveRepository.workingDirectoryPath stringByAppendingPathComponent:@"shared.txt"] encoding:NSUTF8StringEncoding error:NULL]; + XCTAssertEqualObjects(finalContent, @"Initial content\nAnd a new line\n"); +} + +- (void)testRebaseConflict { + // Initial setup: create a base commit on master. + GCCommit* baseCommit = [self makeCommitWithUpdatedFileAtPath:@"shared.txt" string:@"Initial content\n" message:@"Base commit"]; + + // Create a new branch from the base commit. + XCTAssertTrue([self.liveRepository createLocalBranchFromCommit:baseCommit withName:@"other_branch" force:NO error:NULL]); + + GCLocalBranch* masterBranch = [self.liveRepository findLocalBranchWithName:@"master" error:NULL]; + GCLocalBranch* otherBranch = [self.liveRepository findLocalBranchWithName:@"other_branch" error:NULL]; + + XCTAssertTrue([self.liveRepository checkoutLocalBranch:masterBranch options:0 error:NULL]); + GCCommit* masterCommit = [self makeCommitWithUpdatedFileAtPath:@"shared.txt" string:@"Initial content\nMaster modification 1\n" message:@"Master commit 1"]; + XCTAssertNotNil(masterCommit); + + NSError* error; + + // Make a commit on the other branch that also modifies the same shared file. + XCTAssertTrue([self.liveRepository checkoutLocalBranch:otherBranch options:0 error:NULL]); + GCCommit* otherCommit1 = [self makeCommitWithUpdatedFileAtPath:@"shared.txt" string:@"Initial content\nOther modification 1\n" message:@"Other commit 1"]; + XCTAssertNotNil(otherCommit1); + + XCTAssertNil(error); + + [self rebaseAndSolveConflictsWithBaseCommit:baseCommit expectedCommitTotalCount:3]; + + // Verify the results of the rebase. + NSString* finalContent = [NSString stringWithContentsOfFile:[self.liveRepository.workingDirectoryPath stringByAppendingPathComponent:@"shared.txt"] encoding:NSUTF8StringEncoding error:NULL]; + XCTAssertEqualObjects(finalContent, @"Conflict resolved\n", @"File content should reflect resolved conflict."); +} + +- (void)testMultipleCommitsRebaseWithConflict { + // Initial setup: create a base commit on master. + GCCommit* baseCommit = [self makeCommitWithUpdatedFileAtPath:@"shared.txt" string:@"Initial content\n" message:@"Base commit"]; + + // Create a new branch from the base commit. + XCTAssertTrue([self.liveRepository createLocalBranchFromCommit:baseCommit withName:@"other_branch" force:NO error:NULL]); + + GCLocalBranch* masterBranch = [self.liveRepository findLocalBranchWithName:@"master" error:NULL]; + GCLocalBranch* otherBranch = [self.liveRepository findLocalBranchWithName:@"other_branch" error:NULL]; + + XCTAssertTrue([self.liveRepository checkoutLocalBranch:masterBranch options:0 error:NULL]); + GCCommit* masterCommit = [self makeCommitWithUpdatedFileAtPath:@"shared.txt" string:@"Initial content\nMaster modification 1\n" message:@"Master commit 1"]; + XCTAssertNotNil(masterCommit); + + // create other changed files + GCCommit* masterCommit2 = [self makeCommitWithUpdatedFileAtPath:@"shared2.txt" string:@"Initial content\nMaster modification 2\n" message:@"Master commit 2"]; + XCTAssertNotNil(masterCommit2); + + GCCommit* masterCommit3 = [self makeCommitWithUpdatedFileAtPath:@"shared3.txt" string:@"Initial content\nMaster modification 3\n" message:@"Master commit 3"]; + XCTAssertNotNil(masterCommit3); + + NSError* error; + + // Make a commit on the other branch that also modifies the same shared file. + XCTAssertTrue([self.liveRepository checkoutLocalBranch:otherBranch options:0 error:NULL]); + GCCommit* otherCommit1 = [self makeCommitWithUpdatedFileAtPath:@"shared.txt" string:@"Initial content\nOther modification 1\n" message:@"Other commit 1"]; + XCTAssertNotNil(otherCommit1); + + // create other changed files + GCCommit* otherCommit2 = [self makeCommitWithUpdatedFileAtPath:@"shared4.txt" string:@"Initial content\nOther modification 2\n" message:@"Other commit 2"]; + XCTAssertNotNil(otherCommit2); + + GCCommit* otherCommit3 = [self makeCommitWithUpdatedFileAtPath:@"shared5.txt" string:@"Initial content\nOther modification 3\n" message:@"Other commit 3"]; + XCTAssertNotNil(otherCommit3); + + XCTAssertNil(error); + + [self rebaseAndSolveConflictsWithBaseCommit:baseCommit expectedCommitTotalCount:7]; + + // Verify the results of the rebase. + NSString* finalContent = [NSString stringWithContentsOfFile:[self.liveRepository.workingDirectoryPath stringByAppendingPathComponent:@"shared.txt"] encoding:NSUTF8StringEncoding error:NULL]; + XCTAssertEqualObjects(finalContent, @"Conflict resolved\n", @"File content should reflect resolved conflict."); +} + +- (void)rebaseAndSolveConflictsWithBaseCommit:(GCCommit*)baseCommit expectedCommitTotalCount:(int)expectedTotalCommitCount { + NSError* error; + GCHistory* history = [self.liveRepository loadHistoryUsingSorting:kGCHistorySorting_ReverseChronological error:&error]; + GCHistoryLocalBranch* otherHistoryBranch = history.HEADBranch; + GCHistoryLocalBranch* masterHistoryBranch = [history.localBranches filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(GCHistoryLocalBranch* _Nullable localBranch, NSDictionary* _Nullable bindings) { + return [localBranch.name isEqualToString:@"master"]; + }]] + .firstObject; + GCHistoryCommit* otherHistoryCommit = otherHistoryBranch.tipCommit; + GCHistoryCommit* masterHistoryCommit = masterHistoryBranch.tipCommit; + + GCCommit* foundBaseCommit = [self.liveRepository findMergeBaseForCommits:@[ otherHistoryCommit, masterHistoryCommit ] error:&error]; + XCTAssertNotNil(foundBaseCommit); + GCHistoryCommit* fromCommit = [history historyCommitForCommit:baseCommit]; + + // Attempt to rebase the other branch onto master. + NSError* rebaseError = NULL; + [self.liveRepository suspendHistoryUpdates]; + + [self.liveRepository setStatusMode:kGCLiveRepositoryStatusMode_Normal]; + + __block GCCommit* newCommit = nil; + [self.liveRepository setUndoActionName:NSLocalizedString(@"Rebase test", nil)]; + + BOOL rebaseSuccess = [self.liveRepository performReferenceTransformWithReason:@"rebase_branch" + argument:masterHistoryBranch.name + error:&rebaseError + usingBlock:^GCReferenceTransform*(GCLiveRepository* repository, NSError** outError1) { + return [history rebaseBranch:otherHistoryBranch + fromCommit:fromCommit + ontoCommit:masterHistoryCommit + conflictHandler:^GCCommit*(GCIndex* index, GCCommit* ourCommit, GCCommit* theirCommit, NSArray* parentCommits, NSString* message, NSError** outError2) { + GCBlockConflictResolver* blockResolver = [[GCBlockConflictResolver alloc] initWithBlock:^BOOL(GCCommit* ourCommit, GCCommit* theirCommit) { + XCTAssertTrue([index hasConflicts]); + [index enumerateConflictsUsingBlock:^(GCIndexConflict* conflict, BOOL* stop) { + [self updateFileAtPath:conflict.path withString:@"Conflict resolved\n"]; + NSError* conflictResolutionError; + [self.liveRepository resolveConflictAtPath:conflict.path error:&conflictResolutionError]; + XCTAssertNil(conflictResolutionError); + }]; + + return YES; + }]; + + return [self.liveRepository resolveConflictsWithResolver:blockResolver + index:index + ourCommit:ourCommit + theirCommit:theirCommit + parentCommits:parentCommits + message:message + error:outError2]; + } + newTipCommit:&newCommit + error:outError1]; + }]; + [self.liveRepository resumeHistoryUpdates]; + + XCTAssertNil(rebaseError, @"Rebase should not error out with proper conflict handling."); + XCTAssertTrue(rebaseSuccess, @"Rebase should complete successfully."); + + // make sure the working directory is still clean + XCTAssertEqual(self.liveRepository.workingDirectoryStatus.deltas.count, 0); + + //  count to make sure the number of parents makes sense + GCHistoryCommit* currentCommit = self.liveRepository.history.HEADCommit; + int numberOfCommits = 0; + while (true) { + numberOfCommits++; + currentCommit = currentCommit.parents.firstObject; + if (!currentCommit) { + break; + } + } + + XCTAssertEqual(numberOfCommits, expectedTotalCommitCount); +} + +@end diff --git a/GitUpKit/Core/GCTestCase.h b/GitUpKit/Core/GCTestCase.h index 4755cceb..992d1fc1 100644 --- a/GitUpKit/Core/GCTestCase.h +++ b/GitUpKit/Core/GCTestCase.h @@ -49,6 +49,10 @@ - (void)assertContentsOfFileAtPath:(NSString*)path equalsString:(NSString*)string; @end +@interface GCEmptyLiveRepositoryTestCase : GCEmptyRepositoryTestCase +@property(nonatomic, readonly) GCLiveRepository* liveRepository; +@end + @interface GCEmptyRepositoryTests : GCEmptyRepositoryTestCase @end diff --git a/GitUpKit/Core/GCTestCase.m b/GitUpKit/Core/GCTestCase.m index 4da70590..3e3ca8dc 100644 --- a/GitUpKit/Core/GCTestCase.m +++ b/GitUpKit/Core/GCTestCase.m @@ -43,8 +43,8 @@ - (GCRepository*)createLocalRepositoryAtPath:(NSString*)path bare:(BOOL)bare { XCTAssertTrue([[NSFileManager defaultManager] createDirectoryAtPath:configDirectory withIntermediateDirectories:YES attributes:nil error:NULL]); NSString* configPath = [configDirectory stringByAppendingPathComponent:@"config"]; NSString* configString = @"[user]\n\ - name = Bot\n\ - email = bot@example.com\n\ + name = Bot\n\ + email = bot@example.com\n\ "; XCTAssertTrue([configString writeToFile:configPath atomically:YES encoding:NSASCIIStringEncoding error:NULL]); @@ -194,6 +194,43 @@ - (void)assertContentsOfFileAtPath:(NSString*)path equalsString:(NSString*)strin @implementation GCEmptyRepositoryTests @end +@implementation GCEmptyLiveRepositoryTestCase + +- (GCLiveRepository *)liveRepository { + return (GCLiveRepository *)self.repository; +} + +- (GCRepository *)createLocalRepositoryAtPath:(NSString *)path bare:(BOOL)bare { + GCLiveRepository* repo = [[GCLiveRepository alloc] initWithNewLocalRepository:path bare:bare error:NULL]; + XCTAssertNotNil(repo); + + repo.delegate = self; + + NSString* configDirectory = [[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]] stringByAppendingPathComponent:@"git"]; + XCTAssertTrue([[NSFileManager defaultManager] createDirectoryAtPath:configDirectory withIntermediateDirectories:YES attributes:nil error:NULL]); + NSString* configPath = [configDirectory stringByAppendingPathComponent:@"config"]; + NSString* configString = @"[user]\n\ + name = Bot\n\ + email = bot@example.com\n\ +"; + XCTAssertTrue([configString writeToFile:configPath atomically:YES encoding:NSASCIIStringEncoding error:NULL]); + + git_config* config; + XCTAssertEqual(git_config_new(&config), GIT_OK); + if (!repo.bare) { + XCTAssertEqual(git_config_add_file_ondisk(config, [[repo.repositoryPath stringByAppendingPathComponent:@"config"] fileSystemRepresentation], GIT_CONFIG_LEVEL_LOCAL, repo.private, true), GIT_OK); + } + XCTAssertEqual(git_config_add_file_ondisk(config, configPath.fileSystemRepresentation, GIT_CONFIG_LEVEL_APP, repo.private, true), GIT_OK); + git_repository_set_config(repo.private, config); + git_config_free(config); + + objc_setAssociatedObject(repo, _associatedObjectKey, [configDirectory stringByDeletingLastPathComponent], OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + return repo; +} + +@end + @implementation GCSingleCommitRepositoryTestCase - (void)setUp { diff --git a/GitUpKit/GitUpKit.xcodeproj/project.pbxproj b/GitUpKit/GitUpKit.xcodeproj/project.pbxproj index 6a48a69d..1629da6a 100644 --- a/GitUpKit/GitUpKit.xcodeproj/project.pbxproj +++ b/GitUpKit/GitUpKit.xcodeproj/project.pbxproj @@ -44,6 +44,11 @@ DBDFBC1122B61135003EEC6C /* NSBundle+GitUpKit.m in Sources */ = {isa = PBXBuildFile; fileRef = DBDFBC0E22B61135003EEC6C /* NSBundle+GitUpKit.m */; }; DBDFBC1222B61135003EEC6C /* NSBundle+GitUpKit.m in Sources */ = {isa = PBXBuildFile; fileRef = DBDFBC0E22B61135003EEC6C /* NSBundle+GitUpKit.m */; }; DBDFBC1D22B61290003EEC6C /* NSColor+GINamedColors.m in Sources */ = {isa = PBXBuildFile; fileRef = DBDFBC1B22B61290003EEC6C /* NSColor+GINamedColors.m */; }; + DC040FC52BC9FECC00DF54D5 /* GCLiveRepository-Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = DC040FC42BC9FECC00DF54D5 /* GCLiveRepository-Tests.m */; }; + DC040FCA2BCB21D000DF54D5 /* GCLiveRepository+Conflicts.m in Sources */ = {isa = PBXBuildFile; fileRef = DC040FC92BCB21D000DF54D5 /* GCLiveRepository+Conflicts.m */; }; + DC040FCB2BCB21D000DF54D5 /* GCLiveRepository+Conflicts.h in Headers */ = {isa = PBXBuildFile; fileRef = DC040FC82BCB21D000DF54D5 /* GCLiveRepository+Conflicts.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC040FCC2BCB22FC00DF54D5 /* GCLiveRepository+Conflicts.m in Sources */ = {isa = PBXBuildFile; fileRef = DC040FC92BCB21D000DF54D5 /* GCLiveRepository+Conflicts.m */; }; + DC040FCD2BCB417A00DF54D5 /* GCLiveRepository+Conflicts.m in Sources */ = {isa = PBXBuildFile; fileRef = DC040FC92BCB21D000DF54D5 /* GCLiveRepository+Conflicts.m */; }; E200A3BA1B02DDA100C4E39D /* GCPrivate.m in Sources */ = {isa = PBXBuildFile; fileRef = E200A3B81B02DDA100C4E39D /* GCPrivate.m */; }; E20EB08C19FC75CA0031A075 /* GCRepository+Reset.m in Sources */ = {isa = PBXBuildFile; fileRef = E20EB08A19FC75CA0031A075 /* GCRepository+Reset.m */; }; E20EB09019FC76160031A075 /* GCRepository+Status.m in Sources */ = {isa = PBXBuildFile; fileRef = E20EB08E19FC76160031A075 /* GCRepository+Status.m */; }; @@ -370,6 +375,9 @@ DBDFBC0E22B61135003EEC6C /* NSBundle+GitUpKit.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSBundle+GitUpKit.m"; sourceTree = ""; }; DBDFBC1A22B61290003EEC6C /* NSColor+GINamedColors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSColor+GINamedColors.h"; sourceTree = ""; }; DBDFBC1B22B61290003EEC6C /* NSColor+GINamedColors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSColor+GINamedColors.m"; sourceTree = ""; }; + DC040FC42BC9FECC00DF54D5 /* GCLiveRepository-Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "GCLiveRepository-Tests.m"; sourceTree = ""; }; + DC040FC82BCB21D000DF54D5 /* GCLiveRepository+Conflicts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GCLiveRepository+Conflicts.h"; sourceTree = ""; }; + DC040FC92BCB21D000DF54D5 /* GCLiveRepository+Conflicts.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "GCLiveRepository+Conflicts.m"; sourceTree = ""; }; E200A3B81B02DDA100C4E39D /* GCPrivate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCPrivate.m; sourceTree = ""; }; E20EB08919FC75CA0031A075 /* GCRepository+Reset.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GCRepository+Reset.h"; sourceTree = ""; }; E20EB08A19FC75CA0031A075 /* GCRepository+Reset.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GCRepository+Reset.m"; sourceTree = ""; }; @@ -811,6 +819,9 @@ E2790D4C1ACF130A00965A98 /* GCIndex.m */, E23C1A891A9019610060F6AD /* GCLiveRepository.h */, E23C1A8A1A9019610060F6AD /* GCLiveRepository.m */, + DC040FC82BCB21D000DF54D5 /* GCLiveRepository+Conflicts.h */, + DC040FC92BCB21D000DF54D5 /* GCLiveRepository+Conflicts.m */, + DC040FC42BC9FECC00DF54D5 /* GCLiveRepository-Tests.m */, E244538F1A70CDA200E61DE7 /* GCMacros.h */, E2146C8C1A57F3BC00F4550B /* GCObject.h */, E2146C8D1A57F3BC00F4550B /* GCObject.m */, @@ -1042,6 +1053,7 @@ E267E2541B84DC7D00BAB377 /* GIMapViewController.h in Headers */, E267E2551B84DC7D00BAB377 /* GIMapViewController+Operations.h in Headers */, E267E2561B84DC7D00BAB377 /* GIQuickViewController.h in Headers */, + DC040FCB2BCB21D000DF54D5 /* GCLiveRepository+Conflicts.h in Headers */, E267E2571B84DC7D00BAB377 /* GISimpleCommitViewController.h in Headers */, E267E2581B84DC7D00BAB377 /* GIStashListViewController.h in Headers */, DB7CBCA225762721001185AA /* GICustomToolbarItem.h in Headers */, @@ -1255,6 +1267,7 @@ E21753831B91635800BE234A /* GCRepository+Index.m in Sources */, E21753841B91635800BE234A /* GCRepository+Utilities.m in Sources */, E2B9878F1B9171D20097629D /* GIBranch.m in Sources */, + DC040FCD2BCB417A00DF54D5 /* GCLiveRepository+Conflicts.m in Sources */, E2B987901B9171D20097629D /* GIFunctions.m in Sources */, E2B987911B9171D20097629D /* GIGraph.m in Sources */, E2B987921B9171D20097629D /* GILayer.m in Sources */, @@ -1296,6 +1309,7 @@ E267E1E81B84D83100BAB377 /* GCSnapshot.m in Sources */, E267E1E91B84D83100BAB377 /* GCSQLiteRepository.m in Sources */, E267E1EA1B84D83100BAB377 /* GCStash.m in Sources */, + DC040FCA2BCB21D000DF54D5 /* GCLiveRepository+Conflicts.m in Sources */, E267E1EB1B84D83100BAB377 /* GCSubmodule.m in Sources */, E267E1EC1B84D83100BAB377 /* GCTag.m in Sources */, E267E1ED1B84D84900BAB377 /* GCHistory+Rewrite.m in Sources */, @@ -1364,6 +1378,7 @@ E259C2D51A64FAA40079616B /* GCHistory+Rewrite-Tests.m in Sources */, E2790D4E1ACF130A00965A98 /* GCIndex.m in Sources */, E2790D4A1ACF12E200965A98 /* GCRepository+Index.m in Sources */, + DC040FC52BC9FECC00DF54D5 /* GCLiveRepository-Tests.m in Sources */, E27E43021A74A94700D04ED1 /* GIGraph-Tests.m in Sources */, E259C2C71A64C9980079616B /* GCRepository-Tests.m in Sources */, E24509031A9A50F3003E602D /* GCRepository+Config-Tests.m in Sources */, @@ -1390,6 +1405,7 @@ E2B1BF341A85923800A999DF /* GIFunctions.m in Sources */, E259C2E11A64FE4C0079616B /* GCRepository+Status-Tests.m in Sources */, E27E43041A74A96000D04ED1 /* GIGraph.m in Sources */, + DC040FCC2BCB22FC00DF54D5 /* GCLiveRepository+Conflicts.m in Sources */, E21A88F41A9471B300255AC3 /* GIPrivate.m in Sources */, E2B14B5F1A8A764400003E64 /* GCDiff.m in Sources */, E2F5C27F1A8171C900C30739 /* GCSnapshot.m in Sources */, diff --git a/GitUpKit/Utilities/GIViewController+Utilities.h b/GitUpKit/Utilities/GIViewController+Utilities.h index e0f2e98e..59929c48 100644 --- a/GitUpKit/Utilities/GIViewController+Utilities.h +++ b/GitUpKit/Utilities/GIViewController+Utilities.h @@ -16,11 +16,9 @@ #import "GIViewController.h" #import "GILaunchServicesLocator.h" -@class GCCommit, GCIndex, GCDiffDelta, GCIndexConflict; +@protocol GCMergeConflictResolver; -@protocol GIMergeConflictResolver -- (BOOL)resolveMergeConflictsWithOurCommit:(GCCommit*)ourCommit theirCommit:(GCCommit*)theirCommit; -@end +@class GCCommit, GCIndex, GCDiffDelta, GCIndexConflict, GCRepository; @interface GIViewController (Utilities) - (void)discardAllFiles; // Prompts user @@ -56,7 +54,7 @@ - (void)resolveConflictInMergeTool:(GCIndexConflict*)conflict; - (void)markConflictAsResolved:(GCIndexConflict*)conflict; -- (GCCommit*)resolveConflictsWithResolver:(id)resolver +- (GCCommit*)resolveConflictsWithResolver:(id)resolver index:(GCIndex*)index ourCommit:(GCCommit*)ourCommit theirCommit:(GCCommit*)theirCommit diff --git a/GitUpKit/Utilities/GIViewController+Utilities.m b/GitUpKit/Utilities/GIViewController+Utilities.m index 0ba9b4f3..b88c013e 100644 --- a/GitUpKit/Utilities/GIViewController+Utilities.m +++ b/GitUpKit/Utilities/GIViewController+Utilities.m @@ -23,6 +23,7 @@ #import "GCCore.h" #import "GCRepository+Index.h" #import "GCRepository+Utilities.h" +#import "GCLiveRepository+Conflicts.h" #import "GIAppKit.h" #import "XLFacilityMacros.h" @@ -622,7 +623,7 @@ - (void)markConflictAsResolved:(GCIndexConflict*)conflict { } } -- (GCCommit*)resolveConflictsWithResolver:(id)resolver +- (GCCommit*)resolveConflictsWithResolver:(id)resolver index:(GCIndex*)index ourCommit:(GCCommit*)ourCommit theirCommit:(GCCommit*)theirCommit @@ -643,53 +644,13 @@ - (GCCommit*)resolveConflictsWithResolver:(id)resolver return nil; } - // Save HEAD - GCCommit* headCommit; - GCLocalBranch* headBranch; - if (![self.repository lookupHEADCurrentCommit:&headCommit branch:&headBranch error:error]) { - return nil; - } - - // Detach HEAD to "ours" commit - if (![self.repository checkoutCommit:parentCommits[0] options:0 error:error]) { - return nil; - } - - // Check out index with conflicts - if (![self.repository checkoutIndex:index withOptions:kGCCheckoutOption_UpdateSubmodulesRecursively error:error]) { - return nil; - } - - // Have user resolve conflicts - BOOL resolved = [resolver resolveMergeConflictsWithOurCommit:ourCommit theirCommit:theirCommit]; - - // Unless user cancelled, create commit with "ours" and "theirs" parent commits (if applicable) - GCCommit* commit = nil; - if (resolved) { - if (![self.repository syncIndexWithWorkingDirectory:error]) { - return nil; - } - commit = [self.repository createCommitFromHEADAndOtherParent:(parentCommits.count > 1 ? parentCommits[1] : nil) withMessage:message error:error]; - if (commit == nil) { - return nil; - } - } - - // Restore HEAD - if ((headBranch && ![self.repository setHEADToReference:headBranch error:error]) || (!headBranch && ![self.repository setDetachedHEADToCommit:headCommit error:error])) { - return nil; - } - if (![self.repository forceCheckoutHEAD:YES error:error]) { - return nil; - } - - // Check if user cancelled - if (!resolved) { - GC_SET_USER_CANCELLED_ERROR(); - return nil; - } - - return commit; + return [self.repository resolveConflictsWithResolver:resolver + index:index + ourCommit:ourCommit + theirCommit:theirCommit + parentCommits:parentCommits + message:message + error:error]; } // Keep logic in sync with method below! diff --git a/GitUpKit/Views/GICommitRewriterViewController.h b/GitUpKit/Views/GICommitRewriterViewController.h index d4db4e4e..b75c3908 100644 --- a/GitUpKit/Views/GICommitRewriterViewController.h +++ b/GitUpKit/Views/GICommitRewriterViewController.h @@ -18,7 +18,7 @@ @class GICommitRewriterViewController, GCHistoryCommit; -@protocol GICommitRewriterViewControllerDelegate +@protocol GICommitRewriterViewControllerDelegate - (void)commitRewriterViewControllerShouldFinish:(GICommitRewriterViewController*)controller withMessage:(NSString*)message; - (void)commitRewriterViewControllerShouldCancel:(GICommitRewriterViewController*)controller; @end diff --git a/GitUpKit/Views/GICommitSplitterViewController.h b/GitUpKit/Views/GICommitSplitterViewController.h index 19ee0273..f2c24658 100644 --- a/GitUpKit/Views/GICommitSplitterViewController.h +++ b/GitUpKit/Views/GICommitSplitterViewController.h @@ -18,7 +18,7 @@ @class GICommitSplitterViewController, GCHistoryCommit; -@protocol GICommitSplitterViewControllerDelegate +@protocol GICommitSplitterViewControllerDelegate - (void)commitSplitterViewControllerShouldFinish:(GICommitSplitterViewController*)controller withOldMessage:(NSString*)oldMessage newMessage:(NSString*)newMessage; - (void)commitSplitterViewControllerShouldCancel:(GICommitSplitterViewController*)controller; @end diff --git a/GitUpKit/Views/GIMapViewController.h b/GitUpKit/Views/GIMapViewController.h index e697ed23..a4dd210d 100644 --- a/GitUpKit/Views/GIMapViewController.h +++ b/GitUpKit/Views/GIMapViewController.h @@ -15,11 +15,12 @@ #import "GIViewController+Utilities.h" +#import "GCLiveRepository+Conflicts.h" #import "GCRepository.h" @class GIMapViewController, GIGraph, GINode, GCHistory, GCHistoryCommit, GCCommit; -@protocol GIMapViewControllerDelegate +@protocol GIMapViewControllerDelegate - (void)mapViewControllerDidReloadGraph:(GIMapViewController*)controller; - (void)mapViewControllerDidChangeSelection:(GIMapViewController*)controller;