Skip to content

Commit 4057f02

Browse files
committed
Wrap the basic file merging mechanisms
1 parent 3668183 commit 4057f02

File tree

5 files changed

+295
-0
lines changed

5 files changed

+295
-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:(GTMergeFile *)ancestorFile ourFile:(GTMergeFile *)ourFile theirFile:(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: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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 (_result.path ? [NSString stringWithUTF8String:_result.path] : nil);
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+
NSParameterAssert(data);
72+
self = [super init];
73+
if (!self) return nil;
74+
75+
_data = data;
76+
_path = path;
77+
_mode = mode;
78+
79+
git_merge_file_init_input(&_file, GIT_MERGE_FILE_INPUT_VERSION);
80+
81+
_file.ptr = self.data.bytes;
82+
_file.size = self.data.length;
83+
_file.path = [self.path UTF8String];
84+
_file.mode = self.mode;
85+
86+
return self;
87+
}
88+
89+
- (git_merge_file_input *)git_merge_file_input {
90+
return &_file;
91+
}
92+
93+
+ (BOOL)handleMergeFileOptions:(git_merge_file_options *)opts optionsDict:(NSDictionary *)dict error:(NSError **)error {
94+
NSParameterAssert(opts);
95+
96+
int gitError = git_merge_file_init_options(opts, GIT_MERGE_FILE_OPTIONS_VERSION);
97+
if (gitError != 0) {
98+
if (error) *error = [NSError git_errorFor:gitError description:@"Invalid option initialization"];
99+
return NO;
100+
}
101+
102+
if (dict.count != 0) {
103+
if (error) *error = [NSError git_errorFor:-1 description:@"No options handled"];
104+
return NO;
105+
}
106+
return YES;
107+
}
108+
109+
+ (GTMergeResult *)performMergeWithAncestor:(GTMergeFile *)ancestorFile ourFile:(GTMergeFile *)ourFile theirFile:(GTMergeFile *)theirFile options:(NSDictionary *)options error:(NSError **)error {
110+
NSParameterAssert(ourFile);
111+
NSParameterAssert(theirFile);
112+
NSParameterAssert(ancestorFile);
113+
114+
git_merge_file_result gitResult;
115+
git_merge_file_options opts;
116+
117+
BOOL success = [GTMergeFile handleMergeFileOptions:&opts optionsDict:options error:error];
118+
if (!success) return nil;
119+
120+
int gitError = git_merge_file(&gitResult, ancestorFile.git_merge_file_input, ourFile.git_merge_file_input, theirFile.git_merge_file_input, &opts);
121+
if (gitError != 0) {
122+
if (error) *error = [NSError git_errorFor:gitError description:@"Merge file failed"];
123+
return nil;
124+
}
125+
126+
return [[GTMergeResult alloc] initWithGitMergeFileResult:&gitResult];
127+
}
128+
129+
@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 */; };
@@ -491,6 +497,9 @@
491497
4D79C0EC17DF9F4D00997DE4 /* GTCredential.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTCredential.h; sourceTree = "<group>"; };
492498
4D79C0ED17DF9F4D00997DE4 /* GTCredential.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTCredential.m; sourceTree = "<group>"; };
493499
4D79C0F617DFAA7100997DE4 /* GTCredential+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTCredential+Private.h"; sourceTree = "<group>"; };
500+
4D7BA1B82183C4C9003CD3CE /* GTMerge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GTMerge.h; sourceTree = "<group>"; };
501+
4D7BA1B92183C4C9003CD3CE /* GTMerge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMerge.m; sourceTree = "<group>"; };
502+
4D7BA1BF2183DD55003CD3CE /* GTMergeSpec.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GTMergeSpec.m; sourceTree = "<group>"; };
494503
4D9BCD23206D84AD003CD3CE /* libgit2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgit2.a; path = External/build/lib/libgit2.a; sourceTree = "<group>"; };
495504
4DBA4A3117DA73CE006CD5F5 /* GTRemoteSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTRemoteSpec.m; sourceTree = "<group>"; };
496505
4DC55AE31AD859AD0032563C /* GTCheckoutOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTCheckoutOptions.h; sourceTree = "<group>"; };
@@ -836,6 +845,7 @@
836845
D0751CD818BE520400134314 /* GTFilterListSpec.m */,
837846
886E623618AECD86000611A0 /* GTFilterSpec.m */,
838847
8832811E173D8816006D7DCF /* GTIndexSpec.m */,
848+
4D7BA1BF2183DD55003CD3CE /* GTMergeSpec.m */,
839849
F9D1D4221CEB79D1009E5855 /* GTNoteSpec.m */,
840850
88948AC81779243600809CDA /* GTObjectDatabaseSpec.m */,
841851
88F05AA816011FFD00B7AD1D /* GTObjectSpec.m */,
@@ -976,6 +986,8 @@
976986
6EEB51A0199D62B9001D72C0 /* GTFetchHeadEntry.m */,
977987
4DC55AE31AD859AD0032563C /* GTCheckoutOptions.h */,
978988
4DC55AE41AD859AD0032563C /* GTCheckoutOptions.m */,
989+
4D7BA1B82183C4C9003CD3CE /* GTMerge.h */,
990+
4D7BA1B92183C4C9003CD3CE /* GTMerge.m */,
979991
);
980992
path = ObjectiveGit;
981993
sourceTree = "<group>";
@@ -1095,6 +1107,7 @@
10951107
88F6D9FB1320467500CC0BA8 /* GTObject.h in Headers */,
10961108
AA046112134F4D2000DF526B /* GTOdbObject.h in Headers */,
10971109
D0A0129519F99EF8007F1914 /* NSDate+GTTimeAdditions.h in Headers */,
1110+
4D7BA1BA2183C4C9003CD3CE /* GTMerge.h in Headers */,
10981111
4DFFB15B183AA8D600D1565E /* GTRepository+RemoteOperations.h in Headers */,
10991112
BDB2B1301386F34300C88D55 /* GTObjectDatabase.h in Headers */,
11001113
88F6D9FC1320467800CC0BA8 /* GTSignature.h in Headers */,
@@ -1173,6 +1186,7 @@
11731186
D01B6F2F19F82F8700D411BC /* GTObject.h in Headers */,
11741187
4DC55AE61AD859AD0032563C /* GTCheckoutOptions.h in Headers */,
11751188
D01B6F4B19F82F8700D411BC /* GTConfiguration.h in Headers */,
1189+
4D7BA1BB2183C4C9003CD3CE /* GTMerge.h in Headers */,
11761190
D01B6F6719F82FA600D411BC /* GTFetchHeadEntry.h in Headers */,
11771191
D01B6F5F19F82FA600D411BC /* GTFilter.h in Headers */,
11781192
D01B6F5319F82FA600D411BC /* GTBlameHunk.h in Headers */,
@@ -1472,6 +1486,7 @@
14721486
D040AF70177B9779001AD9EB /* GTOIDSpec.m in Sources */,
14731487
D040AF78177B9A9E001AD9EB /* GTSignatureSpec.m in Sources */,
14741488
4DBA4A3217DA73CE006CD5F5 /* GTRemoteSpec.m in Sources */,
1489+
4D7BA1C02183DD55003CD3CE /* GTMergeSpec.m in Sources */,
14751490
4D123240178E009E0048F785 /* GTRepositoryCommittingSpec.m in Sources */,
14761491
);
14771492
runOnlyForDeploymentPostprocessing = 0;
@@ -1531,6 +1546,7 @@
15311546
8821547F17147B3600D76B76 /* GTOID.m in Sources */,
15321547
D03B57A418BFFF07007124F4 /* GTDiffPatch.m in Sources */,
15331548
D03B07F71965DAB0009E5624 /* NSData+Git.m in Sources */,
1549+
4D7BA1BC2183C4C9003CD3CE /* GTMerge.m in Sources */,
15341550
20F43DE618A2F668007D3621 /* GTRepository+Blame.m in Sources */,
15351551
5BE6128A1745EE3400266D8C /* GTTreeBuilder.m in Sources */,
15361552
D09C2E381755F16200065E36 /* GTSubmodule.m in Sources */,
@@ -1593,6 +1609,7 @@
15931609
D01B6F5E19F82FA600D411BC /* GTCredential.m in Sources */,
15941610
D01B6F6219F82FA600D411BC /* GTFilterSource.m in Sources */,
15951611
D01B6F1C19F82F7B00D411BC /* NSDate+GTTimeAdditions.m in Sources */,
1612+
4D7BA1BD2183C4C9003CD3CE /* GTMerge.m in Sources */,
15961613
D01B6F1619F82F7B00D411BC /* NSData+Git.m in Sources */,
15971614
D01B6F1E19F82F7B00D411BC /* NSArray+StringArray.m in Sources */,
15981615
D01B6F5819F82FA600D411BC /* GTReflogEntry.m in Sources */,
@@ -1642,6 +1659,7 @@
16421659
F8D007A01B4FA03B009A8DAF /* GTRepository+StatusSpec.m in Sources */,
16431660
F8D007961B4FA03B009A8DAF /* GTRemotePushSpec.m in Sources */,
16441661
F8D007A51B4FA03B009A8DAF /* GTDiffDeltaSpec.m in Sources */,
1662+
4D7BA1C12183DD55003CD3CE /* GTMergeSpec.m in Sources */,
16451663
);
16461664
runOnlyForDeploymentPostprocessing = 0;
16471665
};

ObjectiveGitTests/GTMergeSpec.m

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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+
describe(@"+performMergeWithAncestor:ourFile:theirFile:options:error:", ^{
32+
it(@"can merge conflicting strings", ^{
33+
GTMergeFile *ourFile = [GTMergeFile fileWithString:@"A test string\n" path:@"ours.txt" mode:0];
34+
GTMergeFile *theirFile = [GTMergeFile fileWithString:@"A better test string\n" path:@"theirs.txt" mode:0];
35+
GTMergeFile *ancestorFile = [GTMergeFile fileWithString:@"A basic string\n" path:@"ancestor.txt" 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+
expect(result.isAutomergeable).to(beFalse());
43+
expect(result.path).to(beNil());
44+
expect(result.mode).to(equal(@(GTFileModeBlob)));
45+
NSString *mergedString = [[NSString alloc] initWithData:result.data encoding:NSUTF8StringEncoding];
46+
expect(mergedString).to(equal(@"<<<<<<< ours.txt\n"
47+
"A test string\n"
48+
"=======\n"
49+
"A better test string\n"
50+
">>>>>>> theirs.txt\n"));
51+
});
52+
53+
it(@"can merge non-conflicting files", ^{
54+
GTMergeFile *ourFile = [GTMergeFile fileWithString:@"A test string\n" path:@"ours.txt" mode:0];
55+
GTMergeFile *theirFile = [GTMergeFile fileWithString:@"A better test string\n" path:@"theirs.txt" mode:0];
56+
GTMergeFile *ancestorFile = [GTMergeFile fileWithString:@"A test string\n" path:@"ancestor.txt" mode:0];
57+
58+
NSError *error = nil;
59+
GTMergeResult *result = [GTMergeFile performMergeWithAncestor:ancestorFile ourFile:ourFile theirFile:theirFile options:nil error:&error];
60+
expect(result).notTo(beNil());
61+
expect(error).to(beNil());
62+
63+
expect(result.isAutomergeable).to(beTrue());
64+
expect(result.path).to(beNil());
65+
expect(result.mode).to(equal(@(GTFileModeBlob)));
66+
NSString *mergedString = [[NSString alloc] initWithData:result.data encoding:NSUTF8StringEncoding];
67+
expect(mergedString).to(equal(@"A better test string\n"));
68+
});
69+
});
70+
71+
afterEach(^{
72+
[self tearDown];
73+
});
74+
75+
QuickSpecEnd

0 commit comments

Comments
 (0)