diff --git a/ObjectiveGit/GTRepository+Merging.h b/ObjectiveGit/GTRepository+Merging.h index 00a025073..53649a095 100644 --- a/ObjectiveGit/GTRepository+Merging.h +++ b/ObjectiveGit/GTRepository+Merging.h @@ -7,6 +7,7 @@ // #import "GTRepository.h" +#import "GTIndexEntry.h" #import "git2/merge.h" NS_ASSUME_NONNULL_BEGIN @@ -52,6 +53,18 @@ typedef NS_OPTIONS(NSInteger, GTMergeAnalysis) { /// will point to an error describing what happened). - (BOOL)mergeBranchIntoCurrentBranch:(GTBranch *)fromBranch withError:(NSError **)error; +/// Gets the file content with conflict markers for the given file +/// +/// The parameters taked are the ones received from `enumerateConflictedFiles`. +/// +/// ancestor - The ancestor entry +/// ours - The index entry of our side +/// theirs - The index entry of their side +/// error - The error if one occurred. Can be NULL. +/// +/// Returns The file content annotated with conflict markers or null on error +- (NSString * _Nullable)contentsOfDiffWithAncestor:(GTIndexEntry *)ancestor ourSide:(GTIndexEntry *)ourSide theirSide:(GTIndexEntry *)theirSide error:(NSError **)error; + /// Analyze which merge to perform. /// /// analysis - The resulting analysis. diff --git a/ObjectiveGit/GTRepository+Merging.m b/ObjectiveGit/GTRepository+Merging.m index 958f53ec6..6e347f3d6 100644 --- a/ObjectiveGit/GTRepository+Merging.m +++ b/ObjectiveGit/GTRepository+Merging.m @@ -17,6 +17,8 @@ #import "GTTree.h" #import "GTIndex.h" #import "GTIndexEntry.h" +#import "GTOdbObject.h" +#import "GTObjectDatabase.h" typedef void (^GTRemoteFetchTransferProgressBlock)(const git_transfer_progress *stats, BOOL *stop); @@ -170,6 +172,87 @@ - (BOOL)mergeBranchIntoCurrentBranch:(GTBranch *)branch withError:(NSError **)er return NO; } +- (NSString * _Nullable)contentsOfDiffWithAncestor:(GTIndexEntry *)ancestor ourSide:(GTIndexEntry *)ourSide theirSide:(GTIndexEntry *)theirSide error:(NSError **)error { + + GTObjectDatabase *database = [self objectDatabaseWithError:error]; + if (database == nil) { + return nil; + } + + // initialize the ancestor's merge file input + git_merge_file_input ancestorInput; + int gitError = git_merge_file_init_input(&ancestorInput, GIT_MERGE_FILE_INPUT_VERSION); + if (gitError != GIT_OK) { + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to create merge file input for ancestor"]; + return nil; + } + + git_oid ancestorId = ancestor.git_index_entry->id; + GTOID *ancestorOID = [[GTOID alloc] initWithGitOid:&ancestorId]; + NSData *ancestorData = [[database objectWithOID:ancestorOID error: error] data]; + if (ancestorData == nil) { + return nil; + } + ancestorInput.ptr = ancestorData.bytes; + ancestorInput.size = ancestorData.length; + + + // initialize our merge file input + git_merge_file_input ourInput; + gitError = git_merge_file_init_input(&ourInput, GIT_MERGE_FILE_INPUT_VERSION); + if (gitError != GIT_OK) { + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to create merge file input for our side"]; + return nil; + } + + git_oid ourId = ourSide.git_index_entry->id; + GTOID *ourOID = [[GTOID alloc] initWithGitOid:&ourId]; + NSData *ourData = [[database objectWithOID:ourOID error: error] data]; + if (ourData == nil) { + return nil; + } + ourInput.ptr = ourData.bytes; + ourInput.size = ourData.length; + + + // initialize their merge file input + git_merge_file_input theirInput; + gitError = git_merge_file_init_input(&theirInput, GIT_MERGE_FILE_INPUT_VERSION); + if (gitError != GIT_OK) { + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to create merge file input other side"]; + return nil; + } + + git_oid theirId = theirSide.git_index_entry->id; + GTOID *theirOID = [[GTOID alloc] initWithGitOid:&theirId]; + NSData *theirData = [[database objectWithOID:theirOID error: error] data]; + if (theirData == nil) { + return nil; + } + theirInput.ptr = theirData.bytes; + theirInput.size = theirData.length; + + + git_merge_file_result result; + gitError = git_merge_file(&result, &ancestorInput, &ourInput, &theirInput, nil); + if (gitError != GIT_OK) { + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to create merge file"]; + return nil; + } + + char *cString = malloc(result.len * sizeof(char *) + 1); + strncpy(cString, result.ptr, result.len); + cString[result.len] = '\0'; + + NSString *mergedContent = [[NSString alloc] initWithCString:cString encoding:NSUTF8StringEncoding]; + + free(cString); + + git_merge_file_result_free(&result); + + return mergedContent; +} + - (BOOL)annotatedCommit:(git_annotated_commit **)annotatedCommit fromCommit:(GTCommit *)fromCommit error:(NSError **)error { int gitError = git_annotated_commit_lookup(annotatedCommit, self.git_repository, fromCommit.OID.git_oid); if (gitError != GIT_OK) { diff --git a/ObjectiveGitTests/GTRepositorySpec.m b/ObjectiveGitTests/GTRepositorySpec.m index efe61d1bf..b6ebc780a 100644 --- a/ObjectiveGitTests/GTRepositorySpec.m +++ b/ObjectiveGitTests/GTRepositorySpec.m @@ -260,6 +260,48 @@ }); }); +describe(@"-contentsOfDiffWithAncestor:ourSide:theirSide:error:", ^{ + it(@"should produce a nice merge conflict description", ^{ + NSURL *mainURL = [repository.fileURL URLByAppendingPathComponent:@"main.m"]; + NSData *mainData = [[NSFileManager defaultManager] contentsAtPath:mainURL.path]; + expect(mainData).notTo(beNil()); + + NSString *mainString = [[NSString alloc] initWithData:mainData encoding:NSUTF8StringEncoding]; + NSData *masterData = [[mainString stringByReplacingOccurrencesOfString:@"return" withString:@"//The meaning of life is 41\n return"] dataUsingEncoding:NSUTF8StringEncoding]; + NSData *otherData = [[mainString stringByReplacingOccurrencesOfString:@"return" withString:@"//The meaning of life is 42\n return"] dataUsingEncoding:NSUTF8StringEncoding]; + + expect(@([[NSFileManager defaultManager] createFileAtPath:mainURL.path contents:masterData attributes:nil])).to(beTruthy()); + + GTIndex *index = [repository indexWithError:NULL]; + expect(@([index addFile:mainURL.lastPathComponent error:NULL])).to(beTruthy()); + GTReference *head = [repository headReferenceWithError:NULL]; + GTCommit *parent = [repository lookUpObjectByOID:head.targetOID objectType:GTObjectTypeCommit error:NULL]; + expect(parent).toNot(beNil()); + GTTree *masterTree = [index writeTree:NULL]; + expect(masterTree).toNot(beNil()); + + GTBranch *otherBranch = [repository lookUpBranchWithName:@"other-branch" type:GTBranchTypeLocal success:NULL error:NULL]; + expect(otherBranch).toNot(beNil()); + expect(@([repository checkoutReference:otherBranch.reference options:nil error:NULL])).to(beTruthy()); + + expect(@([[NSFileManager defaultManager] createFileAtPath:mainURL.path contents:otherData attributes:nil])).to(beTruthy()); + + index = [repository indexWithError:NULL]; + expect(@([index addFile:mainURL.lastPathComponent error:NULL])).to(beTruthy()); + GTTree *otherTree = [index writeTree:NULL]; + expect(otherTree).toNot(beNil()); + + GTIndex *conflictIndex = [otherTree merge:masterTree ancestor:parent.tree error:NULL]; + expect(@([conflictIndex hasConflicts])).to(beTruthy()); + + [conflictIndex enumerateConflictedFilesWithError:NULL usingBlock:^(GTIndexEntry * _Nonnull ancestor, GTIndexEntry * _Nonnull ours, GTIndexEntry * _Nonnull theirs, BOOL * _Nonnull stop) { + + NSString *conflictString = [repository contentsOfDiffWithAncestor:ancestor ourSide:ours theirSide:theirs error:NULL]; + expect(conflictString).to(equal(@"//\n// main.m\n// Test\n//\n// Created by Joe Ricioppo on 9/28/10.\n// Copyright 2010 __MyCompanyName__. All rights reserved.\n//\n\n#import \n\nint main(int argc, char *argv[])\n{\n<<<<<<< file.txt\n //The meaning of life is 42\n=======\n //The meaning of life is 41\n>>>>>>> file.txt\n return NSApplicationMain(argc, (const char **) argv);\n}\n123456789\n123456789\n123456789\n123456789!blah!\n")); + }]; + }); +}); + describe(@"-mergeBaseBetweenFirstOID:secondOID:error:", ^{ it(@"should find the merge base between two branches", ^{ NSError *error = nil;