Skip to content

Commit 7f38ae9

Browse files
committed
Wrap the basic file merging mechanisms
1 parent 6fe2c65 commit 7f38ae9

File tree

5 files changed

+270
-0
lines changed

5 files changed

+270
-0
lines changed

ObjectiveGit/GTMerge.h

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// GTMerge.h
3+
// ObjectiveGitFramework
4+
//
5+
// Created by Etienne on 26/10/2018.
6+
// Copyright © 2018 GitHub, Inc. All rights reserved.
7+
//
8+
9+
#import <Foundation/Foundation.h>
10+
#import "git2/merge.h"
11+
12+
NS_ASSUME_NONNULL_BEGIN
13+
14+
/// Represents the result of a merge
15+
@interface GTMergeResult : NSObject
16+
17+
/// Was the merge automerable ?
18+
@property (readonly,getter=isAutomergeable) BOOL automergeable;
19+
20+
/// The path of the resulting merged file, nil in case of conflicts
21+
@property (readonly) NSString * _Nullable path;
22+
23+
/// The resulting mode of the merged file
24+
@property (readonly) unsigned int mode;
25+
26+
/// The contents of the resulting merged file
27+
@property (readonly) NSData *data;
28+
29+
/// Initialize the merge result from a libgit2 struct.
30+
/// Ownership of the memory will be transferred to the receiver.
31+
- (instancetype)initWithGitMergeFileResult:(git_merge_file_result *)result;
32+
33+
- (instancetype)init NS_UNAVAILABLE;
34+
35+
@end
36+
37+
/// Represents inputs for a tentative merge
38+
@interface GTMergeFile : NSObject
39+
40+
/// The file data
41+
@property (readonly) NSData *data;
42+
43+
/// The file path. Can be nil to not merge paths.
44+
@property (readonly) NSString * _Nullable path;
45+
46+
/// The file mode. Can be 0 to not merge modes.
47+
@property (readonly) unsigned int mode;
48+
49+
/// Perform a merge between files
50+
///
51+
/// ancestorFile - The file to consider the ancestor
52+
/// ourFile - The file to consider as our version
53+
/// theirFile - The file to consider as the incoming version
54+
/// options - The options of the merge. Can be nil.
55+
/// error - A pointer to an error object. Can be NULL.
56+
///
57+
/// Returns the result of the merge, or nil if an error occurred.
58+
+ (GTMergeResult * _Nullable)performMergeWithAncestor:(const GTMergeFile *)ancestorFile ourFile:(const GTMergeFile *)ourFile theirFile:(const GTMergeFile *)theirFile options:(NSDictionary * _Nullable)options error:(NSError **)error;
59+
60+
+ (instancetype)fileWithString:(NSString *)string path:(NSString * _Nullable)path mode:(unsigned int)mode;
61+
62+
/// Initialize an input file for a merge
63+
- (instancetype)initWithData:(NSData *)data path:(NSString * _Nullable)path mode:(unsigned int)mode NS_DESIGNATED_INITIALIZER;
64+
65+
- (instancetype)init NS_UNAVAILABLE;
66+
67+
/// Inner pointer to a libgit2-compatible git_merge_file_input struct.
68+
- (git_merge_file_input *)git_merge_file_input __attribute__((objc_returns_inner_pointer));
69+
70+
@end
71+
72+
NS_ASSUME_NONNULL_END

ObjectiveGit/GTMerge.m

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//
2+
// GTMergeFile.m
3+
// ObjectiveGitFramework
4+
//
5+
// Created by Etienne on 26/10/2018.
6+
// Copyright © 2018 GitHub, Inc. All rights reserved.
7+
//
8+
9+
#import "GTMerge.h"
10+
#import "NSError+Git.h"
11+
12+
@interface GTMergeResult ()
13+
14+
@property (assign) git_merge_file_result result;
15+
16+
@end
17+
18+
@implementation GTMergeResult
19+
20+
- (instancetype)initWithGitMergeFileResult:(git_merge_file_result *)result {
21+
self = [super init];
22+
if (!self) return nil;
23+
24+
memcpy(&_result, result, sizeof(_result));
25+
26+
return self;
27+
}
28+
29+
- (void)dealloc {
30+
git_merge_file_result_free(&_result);
31+
}
32+
33+
- (BOOL)isAutomergeable {
34+
return !!_result.automergeable;
35+
}
36+
37+
- (NSString *)path {
38+
return [NSString stringWithUTF8String:_result.path];
39+
}
40+
41+
- (unsigned int)mode {
42+
return _result.mode;
43+
}
44+
45+
- (NSData *)data {
46+
return [[NSData alloc] initWithBytesNoCopy:(void *)_result.ptr length:_result.len freeWhenDone:NO];
47+
}
48+
49+
@end
50+
51+
@interface GTMergeFile ()
52+
53+
@property (copy) NSData *data;
54+
@property (copy) NSString *path;
55+
@property (assign) unsigned int mode;
56+
@property (assign) git_merge_file_input file;
57+
58+
@end
59+
60+
@implementation GTMergeFile
61+
62+
+ (instancetype)fileWithString:(NSString *)string path:(NSString * _Nullable)path mode:(unsigned int)mode {
63+
NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding];
64+
65+
NSAssert(stringData != nil, @"String couldn't be converted to UTF-8");
66+
67+
return [[self alloc] initWithData:stringData path:path mode:mode];
68+
}
69+
70+
- (instancetype)initWithData:(NSData *)data path:(NSString *)path mode:(unsigned int)mode {
71+
self = [super init];
72+
if (!self) return nil;
73+
74+
_data = data;
75+
_path = path;
76+
_mode = mode;
77+
78+
git_merge_file_init_input(&_file, GIT_MERGE_FILE_INPUT_VERSION);
79+
80+
_file.ptr = self.data.bytes;
81+
_file.size = self.data.length;
82+
_file.path = [self.path UTF8String];
83+
_file.mode = self.mode;
84+
85+
return self;
86+
}
87+
88+
- (git_merge_file_input *)git_merge_file_input {
89+
return &_file;
90+
}
91+
92+
+ (BOOL)handleMergeFileOptions:(git_merge_file_options *)opts optionsDict:(NSDictionary *)dict error:(NSError **)error {
93+
NSParameterAssert(opts);
94+
95+
int gitError = git_merge_file_init_options(opts, GIT_MERGE_FILE_OPTIONS_VERSION);
96+
if (gitError != 0) {
97+
if (error) *error = [NSError git_errorFor:gitError description:@"Invalid option initialization"];
98+
return NO;
99+
}
100+
101+
if (dict.count != 0) {
102+
if (error) *error = [NSError git_errorFor:-1 description:@"No options handled"];
103+
return NO;
104+
}
105+
return YES;
106+
}
107+
108+
+ (GTMergeResult *)performMergeWithAncestor:(const GTMergeFile *)ancestorFile ourFile:(const GTMergeFile *)ourFile theirFile:(const GTMergeFile *)theirFile options:(NSDictionary *)options error:(NSError **)error {
109+
NSParameterAssert(ourFile);
110+
NSParameterAssert(theirFile);
111+
NSParameterAssert(ancestorFile);
112+
113+
git_merge_file_result gitResult;
114+
git_merge_file_options opts;
115+
116+
BOOL success = [GTMergeFile handleMergeFileOptions:&opts optionsDict:options error:error];
117+
if (!success) return nil;
118+
119+
int gitError = git_merge_file(&gitResult, ancestorFile.git_merge_file_input, ourFile.git_merge_file_input, theirFile.git_merge_file_input, &opts);
120+
if (gitError != 0) {
121+
if (error) *error = [NSError git_errorFor:gitError description:@"Merge file failed"];
122+
return nil;
123+
}
124+
125+
return [[GTMergeResult alloc] initWithGitMergeFileResult:&gitResult];
126+
}
127+
128+
@end

ObjectiveGit/ObjectiveGit.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ FOUNDATION_EXPORT const unsigned char ObjectiveGitVersionString[];
7272
#import <ObjectiveGit/GTFetchHeadEntry.h>
7373
#import <ObjectiveGit/GTNote.h>
7474
#import <ObjectiveGit/GTCheckoutOptions.h>
75+
#import <ObjectiveGit/GTMerge.h>
7576

7677
#import <ObjectiveGit/GTObjectDatabase.h>
7778
#import <ObjectiveGit/GTOdbObject.h>

ObjectiveGitFramework.xcodeproj/project.pbxproj

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@
9191
4D1C40D8182C006D00BE2960 /* GTBlobSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D1C40D7182C006D00BE2960 /* GTBlobSpec.m */; };
9292
4D79C0EE17DF9F4D00997DE4 /* GTCredential.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D79C0EC17DF9F4D00997DE4 /* GTCredential.h */; settings = {ATTRIBUTES = (Public, ); }; };
9393
4D79C0EF17DF9F4D00997DE4 /* GTCredential.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D79C0ED17DF9F4D00997DE4 /* GTCredential.m */; };
94+
4D7BA1BA2183C4C9003CD3CE /* GTMerge.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D7BA1B82183C4C9003CD3CE /* GTMerge.h */; settings = {ATTRIBUTES = (Public, ); }; };
95+
4D7BA1BB2183C4C9003CD3CE /* GTMerge.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D7BA1B82183C4C9003CD3CE /* GTMerge.h */; settings = {ATTRIBUTES = (Public, ); }; };
96+
4D7BA1BC2183C4C9003CD3CE /* GTMerge.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D7BA1B92183C4C9003CD3CE /* GTMerge.m */; };
97+
4D7BA1BD2183C4C9003CD3CE /* GTMerge.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D7BA1B92183C4C9003CD3CE /* GTMerge.m */; };
98+
4D7BA1C02183DD55003CD3CE /* GTMergeSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D7BA1BF2183DD55003CD3CE /* GTMergeSpec.m */; };
99+
4D7BA1C12183DD55003CD3CE /* GTMergeSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D7BA1BF2183DD55003CD3CE /* GTMergeSpec.m */; };
94100
4D9BCD24206D84AD003CD3CE /* libgit2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D9BCD23206D84AD003CD3CE /* libgit2.a */; };
95101
4D9BCD25206D84B2003CD3CE /* libgit2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D9BCD23206D84AD003CD3CE /* libgit2.a */; };
96102
4DBA4A3217DA73CE006CD5F5 /* GTRemoteSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DBA4A3117DA73CE006CD5F5 /* GTRemoteSpec.m */; };
@@ -492,6 +498,9 @@
492498
4D79C0EC17DF9F4D00997DE4 /* GTCredential.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTCredential.h; sourceTree = "<group>"; };
493499
4D79C0ED17DF9F4D00997DE4 /* GTCredential.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTCredential.m; sourceTree = "<group>"; };
494500
4D79C0F617DFAA7100997DE4 /* GTCredential+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTCredential+Private.h"; sourceTree = "<group>"; };
501+
4D7BA1B82183C4C9003CD3CE /* GTMerge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMerge.h; sourceTree = "<group>"; };
502+
4D7BA1B92183C4C9003CD3CE /* GTMerge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMerge.m; sourceTree = "<group>"; };
503+
4D7BA1BF2183DD55003CD3CE /* GTMergeSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMergeSpec.m; sourceTree = "<group>"; };
495504
4D9BCD23206D84AD003CD3CE /* libgit2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgit2.a; path = External/build/lib/libgit2.a; sourceTree = "<group>"; };
496505
4DBA4A3117DA73CE006CD5F5 /* GTRemoteSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTRemoteSpec.m; sourceTree = "<group>"; };
497506
4DC55AE31AD859AD0032563C /* GTCheckoutOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTCheckoutOptions.h; sourceTree = "<group>"; };
@@ -837,6 +846,7 @@
837846
D0751CD818BE520400134314 /* GTFilterListSpec.m */,
838847
886E623618AECD86000611A0 /* GTFilterSpec.m */,
839848
8832811E173D8816006D7DCF /* GTIndexSpec.m */,
849+
4D7BA1BF2183DD55003CD3CE /* GTMergeSpec.m */,
840850
F9D1D4221CEB79D1009E5855 /* GTNoteSpec.m */,
841851
88948AC81779243600809CDA /* GTObjectDatabaseSpec.m */,
842852
88F05AA816011FFD00B7AD1D /* GTObjectSpec.m */,
@@ -977,6 +987,8 @@
977987
6EEB51A0199D62B9001D72C0 /* GTFetchHeadEntry.m */,
978988
4DC55AE31AD859AD0032563C /* GTCheckoutOptions.h */,
979989
4DC55AE41AD859AD0032563C /* GTCheckoutOptions.m */,
990+
4D7BA1B82183C4C9003CD3CE /* GTMerge.h */,
991+
4D7BA1B92183C4C9003CD3CE /* GTMerge.m */,
980992
);
981993
path = ObjectiveGit;
982994
sourceTree = "<group>";
@@ -1096,6 +1108,7 @@
10961108
88F6D9FB1320467500CC0BA8 /* GTObject.h in Headers */,
10971109
AA046112134F4D2000DF526B /* GTOdbObject.h in Headers */,
10981110
D0A0129519F99EF8007F1914 /* NSDate+GTTimeAdditions.h in Headers */,
1111+
4D7BA1BA2183C4C9003CD3CE /* GTMerge.h in Headers */,
10991112
4DFFB15B183AA8D600D1565E /* GTRepository+RemoteOperations.h in Headers */,
11001113
BDB2B1301386F34300C88D55 /* GTObjectDatabase.h in Headers */,
11011114
88F6D9FC1320467800CC0BA8 /* GTSignature.h in Headers */,
@@ -1174,6 +1187,7 @@
11741187
D01B6F2F19F82F8700D411BC /* GTObject.h in Headers */,
11751188
4DC55AE61AD859AD0032563C /* GTCheckoutOptions.h in Headers */,
11761189
D01B6F4B19F82F8700D411BC /* GTConfiguration.h in Headers */,
1190+
4D7BA1BB2183C4C9003CD3CE /* GTMerge.h in Headers */,
11771191
D01B6F6719F82FA600D411BC /* GTFetchHeadEntry.h in Headers */,
11781192
D01B6F5F19F82FA600D411BC /* GTFilter.h in Headers */,
11791193
D01B6F5319F82FA600D411BC /* GTBlameHunk.h in Headers */,
@@ -1459,6 +1473,7 @@
14591473
D040AF70177B9779001AD9EB /* GTOIDSpec.m in Sources */,
14601474
D040AF78177B9A9E001AD9EB /* GTSignatureSpec.m in Sources */,
14611475
4DBA4A3217DA73CE006CD5F5 /* GTRemoteSpec.m in Sources */,
1476+
4D7BA1C02183DD55003CD3CE /* GTMergeSpec.m in Sources */,
14621477
4D123240178E009E0048F785 /* GTRepositoryCommittingSpec.m in Sources */,
14631478
);
14641479
runOnlyForDeploymentPostprocessing = 0;
@@ -1518,6 +1533,7 @@
15181533
8821547F17147B3600D76B76 /* GTOID.m in Sources */,
15191534
D03B57A418BFFF07007124F4 /* GTDiffPatch.m in Sources */,
15201535
D03B07F71965DAB0009E5624 /* NSData+Git.m in Sources */,
1536+
4D7BA1BC2183C4C9003CD3CE /* GTMerge.m in Sources */,
15211537
20F43DE618A2F668007D3621 /* GTRepository+Blame.m in Sources */,
15221538
5BE6128A1745EE3400266D8C /* GTTreeBuilder.m in Sources */,
15231539
D09C2E381755F16200065E36 /* GTSubmodule.m in Sources */,
@@ -1580,6 +1596,7 @@
15801596
D01B6F5E19F82FA600D411BC /* GTCredential.m in Sources */,
15811597
D01B6F6219F82FA600D411BC /* GTFilterSource.m in Sources */,
15821598
D01B6F1C19F82F7B00D411BC /* NSDate+GTTimeAdditions.m in Sources */,
1599+
4D7BA1BD2183C4C9003CD3CE /* GTMerge.m in Sources */,
15831600
D01B6F1619F82F7B00D411BC /* NSData+Git.m in Sources */,
15841601
D01B6F1E19F82F7B00D411BC /* NSArray+StringArray.m in Sources */,
15851602
D01B6F5819F82FA600D411BC /* GTReflogEntry.m in Sources */,
@@ -1629,6 +1646,7 @@
16291646
F8D007A01B4FA03B009A8DAF /* GTRepository+StatusSpec.m in Sources */,
16301647
F8D007961B4FA03B009A8DAF /* GTRemotePushSpec.m in Sources */,
16311648
F8D007A51B4FA03B009A8DAF /* GTDiffDeltaSpec.m in Sources */,
1649+
4D7BA1C12183DD55003CD3CE /* GTMergeSpec.m in Sources */,
16321650
);
16331651
runOnlyForDeploymentPostprocessing = 0;
16341652
};

ObjectiveGitTests/GTMergeSpec.m

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// GTMerge.h
3+
// ObjectiveGitFramework
4+
//
5+
// Created by Etienne on 26/10/2018.
6+
// Copyright © 2018 GitHub, Inc. All rights reserved.
7+
//
8+
9+
10+
@import ObjectiveGit;
11+
@import Nimble;
12+
@import Quick;
13+
14+
#import "QuickSpec+GTFixtures.h"
15+
16+
QuickSpecBegin(GTMergeSpec)
17+
18+
__block GTRepository *repository;
19+
__block GTIndex *index;
20+
21+
beforeEach(^{
22+
repository = self.testAppFixtureRepository;
23+
24+
index = [repository indexWithError:NULL];
25+
expect(index).notTo(beNil());
26+
27+
BOOL success = [index refresh:NULL];
28+
expect(@(success)).to(beTruthy());
29+
});
30+
31+
fdescribe(@"+performMergeWithAncestor:ourFile:theirFile:options:error:", ^{
32+
it(@"can merge strings", ^{
33+
GTMergeFile *ourFile = [GTMergeFile fileWithString:@"A test string\n" path:nil mode:0];
34+
GTMergeFile *theirFile = [GTMergeFile fileWithString:@"A better test string\n" path:nil mode:0];
35+
GTMergeFile *ancestorFile = [GTMergeFile fileWithString:@"A basic string\n" path:nil mode:0];
36+
37+
NSError *error = nil;
38+
GTMergeResult *result = [GTMergeFile performMergeWithAncestor:ancestorFile ourFile:ourFile theirFile:theirFile options:nil error:&error];
39+
expect(result).notTo(beNil());
40+
expect(error).to(beNil());
41+
42+
NSString *mergedString = [[NSString alloc] initWithData:result.data encoding:NSUTF8StringEncoding];
43+
expect(mergedString).to(equal(@"<<<<<<< file.txt\nA test string\n=======\nA better test string\n>>>>>>> file.txt\n"));
44+
});
45+
});
46+
47+
afterEach(^{
48+
[self tearDown];
49+
});
50+
51+
QuickSpecEnd

0 commit comments

Comments
 (0)