diff --git a/ObjectiveGit/GTIndex.h b/ObjectiveGit/GTIndex.h index d0df6e4ec..45d662642 100644 --- a/ObjectiveGit/GTIndex.h +++ b/ObjectiveGit/GTIndex.h @@ -33,6 +33,7 @@ @class GTIndexEntry; @class GTRepository; @class GTTree; +@class GTMergeResult; NS_ASSUME_NONNULL_BEGIN @@ -234,6 +235,23 @@ NS_ASSUME_NONNULL_BEGIN - (GTIndexEntry * _Nullable)entryWithName:(NSString *)name error:(NSError **)error __deprecated_msg("use entryWithPath:error: instead."); +@end + +@interface GTIndex (FileMerging) + +/// Gets the result of a merge with the given file entries +/// +/// 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 +/// options - The merge options to use. Can be nil. +/// error - The error if one occurred. Can be NULL. +/// +/// Returns The results of the merge or nil on error +- (GTMergeResult * _Nullable)resultOfMergingAncestorEntry:(GTIndexEntry *)ancestor ourEntry:(GTIndexEntry *)ours theirEntry:(GTIndexEntry *)theirs options:(NSDictionary * _Nullable)options error:(NSError * _Nullable __autoreleasing *)error; + @end NS_ASSUME_NONNULL_END diff --git a/ObjectiveGit/GTIndex.m b/ObjectiveGit/GTIndex.m index 8ee64fde6..32f5bff64 100644 --- a/ObjectiveGit/GTIndex.m +++ b/ObjectiveGit/GTIndex.m @@ -39,8 +39,10 @@ #import "GTBlob.h" #import "NSArray+StringArray.h" #import "NSError+Git.h" +#import "GTMerge+Private.h" #import "git2/errors.h" +#import "git2/merge.h" // The block synonymous with libgit2's `git_index_matched_path_cb` callback. typedef BOOL (^GTIndexPathspecMatchedBlock)(NSString *matchedPathspec, NSString *path, BOOL *stop); @@ -406,4 +408,29 @@ - (GTIndexEntry *)entryWithName:(NSString *)name { - (GTIndexEntry *)entryWithName:(NSString *)name error:(NSError **)error { return [self entryWithPath:name error:error]; } + +@end + +@implementation GTIndex (FileMerging) + +- (GTMergeResult *)resultOfMergingAncestorEntry:(GTIndexEntry *)ancestorEntry ourEntry:(GTIndexEntry *)ourEntry theirEntry:(GTIndexEntry *)theirEntry options:(NSDictionary *)options error:(NSError * _Nullable __autoreleasing *)error { + NSParameterAssert(ourEntry); + NSParameterAssert(theirEntry); + NSParameterAssert(ancestorEntry); + + git_merge_file_result gitResult; + git_merge_file_options opts; + + BOOL success = [GTMergeFile handleMergeFileOptions:&opts optionsDict:options error:error]; + if (!success) return nil; + + int gitError = git_merge_file_from_index(&gitResult, self.repository.git_repository, ancestorEntry.git_index_entry, ourEntry.git_index_entry, theirEntry.git_index_entry, &opts); + if (gitError != 0) { + if (error) *error = [NSError git_errorFor:gitError description:@"Merging entries failed"]; + return nil; + } + + return [[GTMergeResult alloc] initWithGitMergeFileResult:&gitResult]; +} + @end diff --git a/ObjectiveGit/GTMerge+Private.h b/ObjectiveGit/GTMerge+Private.h new file mode 100644 index 000000000..6cd950bf3 --- /dev/null +++ b/ObjectiveGit/GTMerge+Private.h @@ -0,0 +1,15 @@ +// +// GTMerge+Private.h +// ObjectiveGitFramework +// +// Created by Etienne on 27/10/2018. +// Copyright © 2018 GitHub, Inc. All rights reserved. +// + +#import "GTMerge.h" + +@interface GTMergeFile (Private) + ++ (BOOL)handleMergeFileOptions:(git_merge_file_options *)opts optionsDict:(NSDictionary *)dict error:(NSError **)error; + +@end diff --git a/ObjectiveGit/GTMerge.h b/ObjectiveGit/GTMerge.h new file mode 100644 index 000000000..35d1dda22 --- /dev/null +++ b/ObjectiveGit/GTMerge.h @@ -0,0 +1,72 @@ +// +// GTMerge.h +// ObjectiveGitFramework +// +// Created by Etienne on 26/10/2018. +// Copyright © 2018 GitHub, Inc. All rights reserved. +// + +#import +#import "git2/merge.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Represents the result of a merge +@interface GTMergeResult : NSObject + +/// Was the merge automerable ? +@property (readonly,getter=isAutomergeable) BOOL automergeable; + +/// The path of the resulting merged file, nil in case of conflicts +@property (readonly) NSString * _Nullable path; + +/// The resulting mode of the merged file +@property (readonly) unsigned int mode; + +/// The contents of the resulting merged file +@property (readonly) NSData *data; + +/// Initialize the merge result from a libgit2 struct. +/// Ownership of the memory will be transferred to the receiver. +- (instancetype)initWithGitMergeFileResult:(git_merge_file_result *)result; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +/// Represents inputs for a tentative merge +@interface GTMergeFile : NSObject + +/// The file data +@property (readonly) NSData *data; + +/// The file path. Can be nil to not merge paths. +@property (readonly) NSString * _Nullable path; + +/// The file mode. Can be 0 to not merge modes. +@property (readonly) unsigned int mode; + +/// Perform a merge between files +/// +/// ancestorFile - The file to consider the ancestor +/// ourFile - The file to consider as our version +/// theirFile - The file to consider as the incoming version +/// options - The options of the merge. Can be nil. +/// error - A pointer to an error object. Can be NULL. +/// +/// Returns the result of the merge, or nil if an error occurred. ++ (GTMergeResult * _Nullable)performMergeWithAncestor:(GTMergeFile *)ancestorFile ourFile:(GTMergeFile *)ourFile theirFile:(GTMergeFile *)theirFile options:(NSDictionary * _Nullable)options error:(NSError **)error; + ++ (instancetype)fileWithString:(NSString *)string path:(NSString * _Nullable)path mode:(unsigned int)mode; + +/// Initialize an input file for a merge +- (instancetype)initWithData:(NSData *)data path:(NSString * _Nullable)path mode:(unsigned int)mode NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/// Inner pointer to a libgit2-compatible git_merge_file_input struct. +- (git_merge_file_input *)git_merge_file_input __attribute__((objc_returns_inner_pointer)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/ObjectiveGit/GTMerge.m b/ObjectiveGit/GTMerge.m new file mode 100644 index 000000000..95b321eae --- /dev/null +++ b/ObjectiveGit/GTMerge.m @@ -0,0 +1,150 @@ +// +// GTMergeFile.m +// ObjectiveGitFramework +// +// Created by Etienne on 26/10/2018. +// Copyright © 2018 GitHub, Inc. All rights reserved. +// + +#import "GTMerge.h" +#import "GTOID.h" +#import "GTObjectDatabase.h" +#import "GTOdbObject.h" +#import "GTRepository.h" +#import "GTIndex.h" +#import "GTIndexEntry.h" +#import "NSError+Git.h" + +@interface GTMergeResult () + +@property (assign) git_merge_file_result result; + +@end + +@implementation GTMergeResult + +- (instancetype)initWithGitMergeFileResult:(git_merge_file_result *)result { + self = [super init]; + if (!self) return nil; + + memcpy(&_result, result, sizeof(_result)); + + return self; +} + +- (void)dealloc { + git_merge_file_result_free(&_result); +} + +- (BOOL)isAutomergeable { + return !!_result.automergeable; +} + +- (NSString *)path { + return (_result.path ? [NSString stringWithUTF8String:_result.path] : nil); +} + +- (unsigned int)mode { + return _result.mode; +} + +- (NSData *)data { + return [[NSData alloc] initWithBytesNoCopy:(void *)_result.ptr length:_result.len freeWhenDone:NO]; +} + +@end + +@interface GTMergeFile () + +@property (copy) NSData *data; +@property (copy) NSString *path; +@property (assign) unsigned int mode; +@property (assign) git_merge_file_input file; + +@end + +@implementation GTMergeFile + ++ (instancetype)fileWithString:(NSString *)string path:(NSString * _Nullable)path mode:(unsigned int)mode { + NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding]; + + NSAssert(stringData != nil, @"String couldn't be converted to UTF-8"); + + return [[self alloc] initWithData:stringData path:path mode:mode]; +} + ++ (instancetype)fileWithIndexEntry:(GTIndexEntry *)entry error:(NSError **)error { + NSParameterAssert(entry); + + const git_index_entry *git_entry = entry.git_index_entry; + GTOID *ancestorOID = [[GTOID alloc] initWithGitOid:&git_entry->id]; + GTRepository *repository = entry.index.repository; + GTObjectDatabase *database = [repository objectDatabaseWithError:error]; + NSData *contents = [[database objectWithOID:ancestorOID error:error] data]; + if (contents == nil) { + return nil; + } + + return [[self alloc] initWithData:contents path:entry.path mode:git_entry->mode]; +} + +- (instancetype)initWithData:(NSData *)data path:(NSString *)path mode:(unsigned int)mode { + NSParameterAssert(data); + self = [super init]; + if (!self) return nil; + + _data = data; + _path = path; + _mode = mode; + + git_merge_file_init_input(&_file, GIT_MERGE_FILE_INPUT_VERSION); + + _file.ptr = self.data.bytes; + _file.size = self.data.length; + _file.path = [self.path UTF8String]; + _file.mode = self.mode; + + return self; +} + +- (git_merge_file_input *)git_merge_file_input { + return &_file; +} + ++ (BOOL)handleMergeFileOptions:(git_merge_file_options *)opts optionsDict:(NSDictionary *)dict error:(NSError **)error { + NSParameterAssert(opts); + + int gitError = git_merge_file_init_options(opts, GIT_MERGE_FILE_OPTIONS_VERSION); + if (gitError != 0) { + if (error) *error = [NSError git_errorFor:gitError description:@"Invalid option initialization"]; + return NO; + } + + if (dict.count != 0) { + if (error) *error = [NSError git_errorFor:-1 description:@"No options handled"]; + return NO; + } + return YES; +} + ++ (GTMergeResult *)performMergeWithAncestor:(GTMergeFile *)ancestorFile ourFile:(GTMergeFile *)ourFile theirFile:(GTMergeFile *)theirFile options:(NSDictionary *)options error:(NSError **)error { + NSParameterAssert(ourFile); + NSParameterAssert(theirFile); + NSParameterAssert(ancestorFile); + + git_merge_file_result gitResult; + git_merge_file_options opts; + + BOOL success = [GTMergeFile handleMergeFileOptions:&opts optionsDict:options error:error]; + if (!success) return nil; + + int gitError = git_merge_file(&gitResult, ancestorFile.git_merge_file_input, ourFile.git_merge_file_input, theirFile.git_merge_file_input, &opts); + if (gitError != 0) { + if (error) *error = [NSError git_errorFor:gitError description:@"Merge file failed"]; + return nil; + } + + return [[GTMergeResult alloc] initWithGitMergeFileResult:&gitResult]; +} + +@end diff --git a/ObjectiveGit/GTRepository+Merging.m b/ObjectiveGit/GTRepository+Merging.m index f7b9837df..486add6bc 100644 --- a/ObjectiveGit/GTRepository+Merging.m +++ b/ObjectiveGit/GTRepository+Merging.m @@ -19,6 +19,7 @@ #import "GTIndexEntry.h" #import "GTOdbObject.h" #import "GTObjectDatabase.h" +#import "GTMerge.h" typedef void (^GTRemoteFetchTransferProgressBlock)(const git_transfer_progress *stats, BOOL *stop); @@ -173,78 +174,19 @@ - (BOOL)mergeBranchIntoCurrentBranch:(GTBranch *)branch withError:(NSError **)er } - (NSString * _Nullable)contentsOfDiffWithAncestor:(GTIndexEntry *)ancestor ourSide:(GTIndexEntry *)ourSide theirSide:(GTIndexEntry *)theirSide error:(NSError **)error { + NSParameterAssert(ancestor && ourSide && theirSide); - GTObjectDatabase *database = [self objectDatabaseWithError:error]; - if (database == nil) { + GTIndex *index = [self indexWithError:error]; + if (index == 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"]; + GTMergeResult *result = [index resultOfMergingAncestorEntry:ancestor ourEntry:ourSide theirEntry:theirSide options:nil error:error]; + if (result == nil) { 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; - } - - NSString *mergedContent = [[NSString alloc] initWithBytes:result.ptr length:result.len encoding:NSUTF8StringEncoding]; - - git_merge_file_result_free(&result); - - return mergedContent; + return [[NSString alloc] initWithData:result.data encoding:NSUTF8StringEncoding]; } - (BOOL)annotatedCommit:(git_annotated_commit **)annotatedCommit fromCommit:(GTCommit *)fromCommit error:(NSError **)error { diff --git a/ObjectiveGit/ObjectiveGit.h b/ObjectiveGit/ObjectiveGit.h index 5c8cba5bc..d27e537ae 100644 --- a/ObjectiveGit/ObjectiveGit.h +++ b/ObjectiveGit/ObjectiveGit.h @@ -72,6 +72,7 @@ FOUNDATION_EXPORT const unsigned char ObjectiveGitVersionString[]; #import #import #import +#import #import #import diff --git a/ObjectiveGitFramework.xcodeproj/project.pbxproj b/ObjectiveGitFramework.xcodeproj/project.pbxproj index 5043f44c4..6b145fc1a 100644 --- a/ObjectiveGitFramework.xcodeproj/project.pbxproj +++ b/ObjectiveGitFramework.xcodeproj/project.pbxproj @@ -91,6 +91,12 @@ 4D1C40D8182C006D00BE2960 /* GTBlobSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D1C40D7182C006D00BE2960 /* GTBlobSpec.m */; }; 4D79C0EE17DF9F4D00997DE4 /* GTCredential.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D79C0EC17DF9F4D00997DE4 /* GTCredential.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4D79C0EF17DF9F4D00997DE4 /* GTCredential.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D79C0ED17DF9F4D00997DE4 /* GTCredential.m */; }; + 4D7BA1BA2183C4C9003CD3CE /* GTMerge.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D7BA1B82183C4C9003CD3CE /* GTMerge.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4D7BA1BB2183C4C9003CD3CE /* GTMerge.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D7BA1B82183C4C9003CD3CE /* GTMerge.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4D7BA1BC2183C4C9003CD3CE /* GTMerge.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D7BA1B92183C4C9003CD3CE /* GTMerge.m */; }; + 4D7BA1BD2183C4C9003CD3CE /* GTMerge.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D7BA1B92183C4C9003CD3CE /* GTMerge.m */; }; + 4D7BA1C02183DD55003CD3CE /* GTMergeSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D7BA1BF2183DD55003CD3CE /* GTMergeSpec.m */; }; + 4D7BA1C12183DD55003CD3CE /* GTMergeSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D7BA1BF2183DD55003CD3CE /* GTMergeSpec.m */; }; 4D9BCD24206D84AD003CD3CE /* libgit2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D9BCD23206D84AD003CD3CE /* libgit2.a */; }; 4D9BCD25206D84B2003CD3CE /* libgit2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D9BCD23206D84AD003CD3CE /* libgit2.a */; }; 4DBA4A3217DA73CE006CD5F5 /* GTRemoteSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DBA4A3117DA73CE006CD5F5 /* GTRemoteSpec.m */; }; @@ -491,6 +497,10 @@ 4D79C0EC17DF9F4D00997DE4 /* GTCredential.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTCredential.h; sourceTree = ""; }; 4D79C0ED17DF9F4D00997DE4 /* GTCredential.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTCredential.m; sourceTree = ""; }; 4D79C0F617DFAA7100997DE4 /* GTCredential+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTCredential+Private.h"; sourceTree = ""; }; + 4D7BA1B82183C4C9003CD3CE /* GTMerge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMerge.h; sourceTree = ""; }; + 4D7BA1B92183C4C9003CD3CE /* GTMerge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMerge.m; sourceTree = ""; }; + 4D7BA1BF2183DD55003CD3CE /* GTMergeSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMergeSpec.m; sourceTree = ""; }; + 4D7BA1BE2183D3EE003CD3CE /* GTMerge+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GTMerge+Private.h"; sourceTree = ""; }; 4D9BCD23206D84AD003CD3CE /* libgit2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgit2.a; path = External/build/lib/libgit2.a; sourceTree = ""; }; 4DBA4A3117DA73CE006CD5F5 /* GTRemoteSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTRemoteSpec.m; sourceTree = ""; }; 4DC55AE31AD859AD0032563C /* GTCheckoutOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTCheckoutOptions.h; sourceTree = ""; }; @@ -836,6 +846,7 @@ D0751CD818BE520400134314 /* GTFilterListSpec.m */, 886E623618AECD86000611A0 /* GTFilterSpec.m */, 8832811E173D8816006D7DCF /* GTIndexSpec.m */, + 4D7BA1BF2183DD55003CD3CE /* GTMergeSpec.m */, F9D1D4221CEB79D1009E5855 /* GTNoteSpec.m */, 88948AC81779243600809CDA /* GTObjectDatabaseSpec.m */, 88F05AA816011FFD00B7AD1D /* GTObjectSpec.m */, @@ -976,6 +987,9 @@ 6EEB51A0199D62B9001D72C0 /* GTFetchHeadEntry.m */, 4DC55AE31AD859AD0032563C /* GTCheckoutOptions.h */, 4DC55AE41AD859AD0032563C /* GTCheckoutOptions.m */, + 4D7BA1B82183C4C9003CD3CE /* GTMerge.h */, + 4D7BA1BE2183D3EE003CD3CE /* GTMerge+Private.h */, + 4D7BA1B92183C4C9003CD3CE /* GTMerge.m */, ); path = ObjectiveGit; sourceTree = ""; @@ -1095,6 +1109,7 @@ 88F6D9FB1320467500CC0BA8 /* GTObject.h in Headers */, AA046112134F4D2000DF526B /* GTOdbObject.h in Headers */, D0A0129519F99EF8007F1914 /* NSDate+GTTimeAdditions.h in Headers */, + 4D7BA1BA2183C4C9003CD3CE /* GTMerge.h in Headers */, 4DFFB15B183AA8D600D1565E /* GTRepository+RemoteOperations.h in Headers */, BDB2B1301386F34300C88D55 /* GTObjectDatabase.h in Headers */, 88F6D9FC1320467800CC0BA8 /* GTSignature.h in Headers */, @@ -1173,6 +1188,7 @@ D01B6F2F19F82F8700D411BC /* GTObject.h in Headers */, 4DC55AE61AD859AD0032563C /* GTCheckoutOptions.h in Headers */, D01B6F4B19F82F8700D411BC /* GTConfiguration.h in Headers */, + 4D7BA1BB2183C4C9003CD3CE /* GTMerge.h in Headers */, D01B6F6719F82FA600D411BC /* GTFetchHeadEntry.h in Headers */, D01B6F5F19F82FA600D411BC /* GTFilter.h in Headers */, D01B6F5319F82FA600D411BC /* GTBlameHunk.h in Headers */, @@ -1472,6 +1488,7 @@ D040AF70177B9779001AD9EB /* GTOIDSpec.m in Sources */, D040AF78177B9A9E001AD9EB /* GTSignatureSpec.m in Sources */, 4DBA4A3217DA73CE006CD5F5 /* GTRemoteSpec.m in Sources */, + 4D7BA1C02183DD55003CD3CE /* GTMergeSpec.m in Sources */, 4D123240178E009E0048F785 /* GTRepositoryCommittingSpec.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1531,6 +1548,7 @@ 8821547F17147B3600D76B76 /* GTOID.m in Sources */, D03B57A418BFFF07007124F4 /* GTDiffPatch.m in Sources */, D03B07F71965DAB0009E5624 /* NSData+Git.m in Sources */, + 4D7BA1BC2183C4C9003CD3CE /* GTMerge.m in Sources */, 20F43DE618A2F668007D3621 /* GTRepository+Blame.m in Sources */, 5BE6128A1745EE3400266D8C /* GTTreeBuilder.m in Sources */, D09C2E381755F16200065E36 /* GTSubmodule.m in Sources */, @@ -1593,6 +1611,7 @@ D01B6F5E19F82FA600D411BC /* GTCredential.m in Sources */, D01B6F6219F82FA600D411BC /* GTFilterSource.m in Sources */, D01B6F1C19F82F7B00D411BC /* NSDate+GTTimeAdditions.m in Sources */, + 4D7BA1BD2183C4C9003CD3CE /* GTMerge.m in Sources */, D01B6F1619F82F7B00D411BC /* NSData+Git.m in Sources */, D01B6F1E19F82F7B00D411BC /* NSArray+StringArray.m in Sources */, D01B6F5819F82FA600D411BC /* GTReflogEntry.m in Sources */, @@ -1642,6 +1661,7 @@ F8D007A01B4FA03B009A8DAF /* GTRepository+StatusSpec.m in Sources */, F8D007961B4FA03B009A8DAF /* GTRemotePushSpec.m in Sources */, F8D007A51B4FA03B009A8DAF /* GTDiffDeltaSpec.m in Sources */, + 4D7BA1C12183DD55003CD3CE /* GTMergeSpec.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ObjectiveGitTests/GTIndexSpec.m b/ObjectiveGitTests/GTIndexSpec.m index 4e894153a..599788a23 100644 --- a/ObjectiveGitTests/GTIndexSpec.m +++ b/ObjectiveGitTests/GTIndexSpec.m @@ -328,6 +328,75 @@ }); }); +describe(@"-resultOfMergingAncestorEntry:ourEntry:theirEntry:options: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) { + + GTMergeResult *result = [conflictIndex resultOfMergingAncestorEntry:ancestor ourEntry:ours theirEntry:theirs options:nil error:NULL]; + expect(result).notTo(beNil()); + + NSString *conflictString = [[NSString alloc] initWithData:result.data encoding:NSUTF8StringEncoding]; + NSString *expectedString = @"//\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" + "\n" + "int main(int argc, char *argv[])\n" + "{\n" + "<<<<<<< main.m\n" + " //The meaning of life is 42\n" + "=======\n" + " //The meaning of life is 41\n" + ">>>>>>> main.m\n" + " return NSApplicationMain(argc, (const char **) argv);\n" + "}\n" + "123456789\n" + "123456789\n" + "123456789\n" + "123456789!blah!\n"; + + expect(conflictString).to(equal(expectedString)); + }]; + }); +}); + afterEach(^{ [self tearDown]; }); diff --git a/ObjectiveGitTests/GTMergeSpec.m b/ObjectiveGitTests/GTMergeSpec.m new file mode 100644 index 000000000..ca88d89c9 --- /dev/null +++ b/ObjectiveGitTests/GTMergeSpec.m @@ -0,0 +1,75 @@ +// +// GTMerge.h +// ObjectiveGitFramework +// +// Created by Etienne on 26/10/2018. +// Copyright © 2018 GitHub, Inc. All rights reserved. +// + + +@import ObjectiveGit; +@import Nimble; +@import Quick; + +#import "QuickSpec+GTFixtures.h" + +QuickSpecBegin(GTMergeSpec) + +__block GTRepository *repository; +__block GTIndex *index; + +beforeEach(^{ + repository = self.testAppFixtureRepository; + + index = [repository indexWithError:NULL]; + expect(index).notTo(beNil()); + + BOOL success = [index refresh:NULL]; + expect(@(success)).to(beTruthy()); +}); + +describe(@"+performMergeWithAncestor:ourFile:theirFile:options:error:", ^{ + it(@"can merge conflicting strings", ^{ + GTMergeFile *ourFile = [GTMergeFile fileWithString:@"A test string\n" path:@"ours.txt" mode:0]; + GTMergeFile *theirFile = [GTMergeFile fileWithString:@"A better test string\n" path:@"theirs.txt" mode:0]; + GTMergeFile *ancestorFile = [GTMergeFile fileWithString:@"A basic string\n" path:@"ancestor.txt" mode:0]; + + NSError *error = nil; + GTMergeResult *result = [GTMergeFile performMergeWithAncestor:ancestorFile ourFile:ourFile theirFile:theirFile options:nil error:&error]; + expect(result).notTo(beNil()); + expect(error).to(beNil()); + + expect(result.isAutomergeable).to(beFalse()); + expect(result.path).to(beNil()); + expect(result.mode).to(equal(@(GTFileModeBlob))); + NSString *mergedString = [[NSString alloc] initWithData:result.data encoding:NSUTF8StringEncoding]; + expect(mergedString).to(equal(@"<<<<<<< ours.txt\n" + "A test string\n" + "=======\n" + "A better test string\n" + ">>>>>>> theirs.txt\n")); + }); + + it(@"can merge non-conflicting files", ^{ + GTMergeFile *ourFile = [GTMergeFile fileWithString:@"A test string\n" path:@"ours.txt" mode:0]; + GTMergeFile *theirFile = [GTMergeFile fileWithString:@"A better test string\n" path:@"theirs.txt" mode:0]; + GTMergeFile *ancestorFile = [GTMergeFile fileWithString:@"A test string\n" path:@"ancestor.txt" mode:0]; + + NSError *error = nil; + GTMergeResult *result = [GTMergeFile performMergeWithAncestor:ancestorFile ourFile:ourFile theirFile:theirFile options:nil error:&error]; + expect(result).notTo(beNil()); + expect(error).to(beNil()); + + expect(result.isAutomergeable).to(beTrue()); + expect(result.path).to(beNil()); + expect(result.mode).to(equal(@(GTFileModeBlob))); + NSString *mergedString = [[NSString alloc] initWithData:result.data encoding:NSUTF8StringEncoding]; + expect(mergedString).to(equal(@"A better test string\n")); + }); +}); + +afterEach(^{ + [self tearDown]; +}); + +QuickSpecEnd diff --git a/ObjectiveGitTests/GTRepositorySpec.m b/ObjectiveGitTests/GTRepositorySpec.m index 2ff1aaf88..3381bd133 100644 --- a/ObjectiveGitTests/GTRepositorySpec.m +++ b/ObjectiveGitTests/GTRepositorySpec.m @@ -260,48 +260,6 @@ }); }); -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;