From 26a85b23d2ade8cdb315330143c50ac0084bd44e Mon Sep 17 00:00:00 2001 From: Alexander Hedges Date: Sun, 28 May 2017 18:41:37 +0200 Subject: [PATCH 1/3] Wrap git_merge_file --- ObjectiveGit/GTRepository+Merging.h | 13 +++++ ObjectiveGit/GTRepository+Merging.m | 82 +++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/ObjectiveGit/GTRepository+Merging.h b/ObjectiveGit/GTRepository+Merging.h index 00a025073..27026642a 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)stringForConflictWithAncestor:(GTIndexEntry *)ancestor ourSide:(GTIndexEntry *)ourSide theirSide:(GTIndexEntry *)theirSide withError:(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..4d6cefcf9 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,86 @@ - (BOOL)mergeBranchIntoCurrentBranch:(GTBranch *)branch withError:(NSError **)er return NO; } +- (NSString* _Nullable)stringForConflictWithAncestor:(GTIndexEntry *)ancestor ourSide:(GTIndexEntry *)ourSide theirSide:(GTIndexEntry *)theirSide withError:(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; + } + NSString *ancestorString = [[NSString alloc] initWithData: ancestorData encoding:NSUTF8StringEncoding]; + ancestorInput.ptr = [ancestorString cStringUsingEncoding:NSUTF8StringEncoding]; + ancestorInput.size = ancestorString.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; + } + NSString *ourString = [[NSString alloc] initWithData: ourData encoding:NSUTF8StringEncoding]; + ourInput.ptr = [ourString cStringUsingEncoding:NSUTF8StringEncoding]; + ourInput.size = ourString.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; + } + NSString *theirString = [[NSString alloc] initWithData: theirData encoding:NSUTF8StringEncoding]; + theirInput.ptr = [theirString cStringUsingEncoding:NSUTF8StringEncoding]; + theirInput.size = theirString.length; + + + git_merge_file_result result; + git_merge_file(&result, &ancestorInput, &ourInput, &theirInput, 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) { From 05bd5d4633a6a9897649a9d9ecb43af5ec3b04ce Mon Sep 17 00:00:00 2001 From: Alexander Hedges Date: Sun, 4 Jun 2017 23:53:01 +0200 Subject: [PATCH 2/3] Address comments and new test case --- ObjectiveGit/GTRepository+Merging.h | 2 +- ObjectiveGit/GTRepository+Merging.m | 25 ++++++++-------- ObjectiveGitTests/GTRepositorySpec.m | 44 ++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/ObjectiveGit/GTRepository+Merging.h b/ObjectiveGit/GTRepository+Merging.h index 27026642a..53649a095 100644 --- a/ObjectiveGit/GTRepository+Merging.h +++ b/ObjectiveGit/GTRepository+Merging.h @@ -63,7 +63,7 @@ typedef NS_OPTIONS(NSInteger, GTMergeAnalysis) { /// error - The error if one occurred. Can be NULL. /// /// Returns The file content annotated with conflict markers or null on error -- (NSString* _Nullable)stringForConflictWithAncestor:(GTIndexEntry *)ancestor ourSide:(GTIndexEntry *)ourSide theirSide:(GTIndexEntry *)theirSide withError:(NSError **)error; +- (NSString * _Nullable)contentsOfDiffWithAncestor:(GTIndexEntry *)ancestor ourSide:(GTIndexEntry *)ourSide theirSide:(GTIndexEntry *)theirSide error:(NSError **)error; /// Analyze which merge to perform. /// diff --git a/ObjectiveGit/GTRepository+Merging.m b/ObjectiveGit/GTRepository+Merging.m index 4d6cefcf9..6e347f3d6 100644 --- a/ObjectiveGit/GTRepository+Merging.m +++ b/ObjectiveGit/GTRepository+Merging.m @@ -172,7 +172,7 @@ - (BOOL)mergeBranchIntoCurrentBranch:(GTBranch *)branch withError:(NSError **)er return NO; } -- (NSString* _Nullable)stringForConflictWithAncestor:(GTIndexEntry *)ancestor ourSide:(GTIndexEntry *)ourSide theirSide:(GTIndexEntry *)theirSide withError:(NSError **)error { +- (NSString * _Nullable)contentsOfDiffWithAncestor:(GTIndexEntry *)ancestor ourSide:(GTIndexEntry *)ourSide theirSide:(GTIndexEntry *)theirSide error:(NSError **)error { GTObjectDatabase *database = [self objectDatabaseWithError:error]; if (database == nil) { @@ -193,9 +193,8 @@ - (NSString* _Nullable)stringForConflictWithAncestor:(GTIndexEntry *)ancestor ou if (ancestorData == nil) { return nil; } - NSString *ancestorString = [[NSString alloc] initWithData: ancestorData encoding:NSUTF8StringEncoding]; - ancestorInput.ptr = [ancestorString cStringUsingEncoding:NSUTF8StringEncoding]; - ancestorInput.size = ancestorString.length; + ancestorInput.ptr = ancestorData.bytes; + ancestorInput.size = ancestorData.length; // initialize our merge file input @@ -212,9 +211,8 @@ - (NSString* _Nullable)stringForConflictWithAncestor:(GTIndexEntry *)ancestor ou if (ourData == nil) { return nil; } - NSString *ourString = [[NSString alloc] initWithData: ourData encoding:NSUTF8StringEncoding]; - ourInput.ptr = [ourString cStringUsingEncoding:NSUTF8StringEncoding]; - ourInput.size = ourString.length; + ourInput.ptr = ourData.bytes; + ourInput.size = ourData.length; // initialize their merge file input @@ -231,15 +229,18 @@ - (NSString* _Nullable)stringForConflictWithAncestor:(GTIndexEntry *)ancestor ou if (theirData == nil) { return nil; } - NSString *theirString = [[NSString alloc] initWithData: theirData encoding:NSUTF8StringEncoding]; - theirInput.ptr = [theirString cStringUsingEncoding:NSUTF8StringEncoding]; - theirInput.size = theirString.length; + theirInput.ptr = theirData.bytes; + theirInput.size = theirData.length; git_merge_file_result result; - git_merge_file(&result, &ancestorInput, &ourInput, &theirInput, nil); + 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); + char *cString = malloc(result.len * sizeof(char *) + 1); strncpy(cString, result.ptr, result.len); cString[result.len] = '\0'; diff --git a/ObjectiveGitTests/GTRepositorySpec.m b/ObjectiveGitTests/GTRepositorySpec.m index efe61d1bf..19914c392 100644 --- a/ObjectiveGitTests/GTRepositorySpec.m +++ b/ObjectiveGitTests/GTRepositorySpec.m @@ -260,6 +260,50 @@ }); }); +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()); + GTCommit *masterCommit = [repository createCommitWithTree:[index writeTree:NULL] message:@"MOLTAE is 41" parents:[NSArray arrayWithObject:parent] updatingReferenceNamed:head.name error:NULL]; + expect(masterCommit).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()); + parent = [repository lookUpObjectByOID:otherBranch.OID objectType:GTObjectTypeCommit error:NULL]; + expect(parent).toNot(beNil()); + GTCommit *otherCommit = [repository createCommitWithTree:[index writeTree:NULL] message:@"MOLTAE is 42!" parents:[NSArray arrayWithObject:parent] updatingReferenceNamed:otherBranch.name error:NULL]; + expect(otherCommit).toNot(beNil()); + + GTIndex *conflictIndex = [otherCommit merge:masterCommit 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; From 831722b85e6cbbc28f03f93712eaeaff360c39cf Mon Sep 17 00:00:00 2001 From: Alexander Hedges Date: Mon, 5 Jun 2017 10:15:31 +0200 Subject: [PATCH 3/3] Use trees in test instead of commits --- ObjectiveGitTests/GTRepositorySpec.m | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/ObjectiveGitTests/GTRepositorySpec.m b/ObjectiveGitTests/GTRepositorySpec.m index 19914c392..b6ebc780a 100644 --- a/ObjectiveGitTests/GTRepositorySpec.m +++ b/ObjectiveGitTests/GTRepositorySpec.m @@ -277,8 +277,8 @@ GTReference *head = [repository headReferenceWithError:NULL]; GTCommit *parent = [repository lookUpObjectByOID:head.targetOID objectType:GTObjectTypeCommit error:NULL]; expect(parent).toNot(beNil()); - GTCommit *masterCommit = [repository createCommitWithTree:[index writeTree:NULL] message:@"MOLTAE is 41" parents:[NSArray arrayWithObject:parent] updatingReferenceNamed:head.name error:NULL]; - expect(masterCommit).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()); @@ -288,12 +288,10 @@ index = [repository indexWithError:NULL]; expect(@([index addFile:mainURL.lastPathComponent error:NULL])).to(beTruthy()); - parent = [repository lookUpObjectByOID:otherBranch.OID objectType:GTObjectTypeCommit error:NULL]; - expect(parent).toNot(beNil()); - GTCommit *otherCommit = [repository createCommitWithTree:[index writeTree:NULL] message:@"MOLTAE is 42!" parents:[NSArray arrayWithObject:parent] updatingReferenceNamed:otherBranch.name error:NULL]; - expect(otherCommit).toNot(beNil()); + GTTree *otherTree = [index writeTree:NULL]; + expect(otherTree).toNot(beNil()); - GTIndex *conflictIndex = [otherCommit merge:masterCommit error:NULL]; + 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) {