-
Notifications
You must be signed in to change notification settings - Fork 280
/
GTRepository+Merging.m
288 lines (224 loc) · 9.19 KB
/
GTRepository+Merging.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
//
// GTRepository+Merging.m
// ObjectiveGitFramework
//
// Created by Piet Brauer on 02/03/16.
// Copyright © 2016 GitHub, Inc. All rights reserved.
//
#import "GTRepository+Merging.h"
#import "GTOID.h"
#import "NSError+Git.h"
#import "git2/errors.h"
#import "GTCommit.h"
#import "GTReference.h"
#import "GTRepository+Committing.h"
#import "GTRepository+Pull.h"
#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);
@implementation GTRepository (Merging)
typedef void (^GTRepositoryEnumerateMergeHeadEntryBlock)(GTOID *entry, BOOL *stop);
typedef struct {
__unsafe_unretained GTRepositoryEnumerateMergeHeadEntryBlock enumerationBlock;
} GTEnumerateMergeHeadEntriesPayload;
int GTMergeHeadEntriesCallback(const git_oid *oid, void *payload) {
GTEnumerateMergeHeadEntriesPayload *entriesPayload = payload;
GTRepositoryEnumerateMergeHeadEntryBlock enumerationBlock = entriesPayload->enumerationBlock;
GTOID *gtoid = [GTOID oidWithGitOid:oid];
BOOL stop = NO;
enumerationBlock(gtoid, &stop);
return (stop == YES ? GIT_EUSER : 0);
}
- (BOOL)enumerateMergeHeadEntriesWithError:(NSError **)error usingBlock:(void (^)(GTOID *mergeHeadEntry, BOOL *stop))block {
NSParameterAssert(block != nil);
GTEnumerateMergeHeadEntriesPayload payload = {
.enumerationBlock = block,
};
int gitError = git_repository_mergehead_foreach(self.git_repository, GTMergeHeadEntriesCallback, &payload);
if (gitError != GIT_OK) {
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to get mergehead entries"];
return NO;
}
return YES;
}
- (NSArray *)mergeHeadEntriesWithError:(NSError **)error {
NSMutableArray *entries = [NSMutableArray array];
[self enumerateMergeHeadEntriesWithError:error usingBlock:^(GTOID *mergeHeadEntry, BOOL *stop) {
[entries addObject:mergeHeadEntry];
*stop = NO;
}];
return entries;
}
- (BOOL)mergeBranchIntoCurrentBranch:(GTBranch *)branch withError:(NSError **)error {
// Check if merge is necessary
GTBranch *localBranch = [self currentBranchWithError:error];
if (!localBranch) {
return NO;
}
GTCommit *localCommit = [localBranch targetCommitWithError:error];
if (!localCommit) {
return NO;
}
GTCommit *remoteCommit = [branch targetCommitWithError:error];
if (!remoteCommit) {
return NO;
}
if ([localCommit.SHA isEqualToString:remoteCommit.SHA]) {
// Local and remote tracking branch are already in sync
return YES;
}
GTMergeAnalysis analysis = GTMergeAnalysisNone;
BOOL success = [self analyzeMerge:&analysis fromBranch:branch error:error];
if (!success) {
return NO;
}
if (analysis & GTMergeAnalysisUpToDate) {
// Nothing to do
return YES;
} else if (analysis & GTMergeAnalysisFastForward ||
analysis & GTMergeAnalysisUnborn) {
// Fast-forward branch
NSString *message = [NSString stringWithFormat:@"merge %@: Fast-forward", branch.name];
GTReference *reference = [localBranch.reference referenceByUpdatingTarget:remoteCommit.SHA message:message error:error];
BOOL checkoutSuccess = [self checkoutReference:reference options:[GTCheckoutOptions checkoutOptionsWithStrategy:GTCheckoutStrategyForce] error:error];
return checkoutSuccess;
} else if (analysis & GTMergeAnalysisNormal) {
// Do normal merge
GTTree *localTree = localCommit.tree;
GTTree *remoteTree = remoteCommit.tree;
// TODO: Find common ancestor
GTTree *ancestorTree = nil;
// Merge
GTIndex *index = [localTree merge:remoteTree ancestor:ancestorTree error:error];
if (!index) {
return NO;
}
// Check for conflict
if (index.hasConflicts) {
NSMutableArray <NSString *>*files = [NSMutableArray array];
[index enumerateConflictedFilesWithError:error usingBlock:^(GTIndexEntry * _Nonnull ancestor, GTIndexEntry * _Nonnull ours, GTIndexEntry * _Nonnull theirs, BOOL * _Nonnull stop) {
[files addObject:ours.path];
}];
if (error != NULL) {
NSDictionary *userInfo = @{GTPullMergeConflictedFiles: files};
*error = [NSError git_errorFor:GIT_ECONFLICT description:@"Merge conflict" userInfo:userInfo failureReason:nil];
}
// Write conflicts
git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT;
git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
checkout_opts.checkout_strategy = (GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS);
git_annotated_commit *annotatedCommit;
[self annotatedCommit:&annotatedCommit fromCommit:remoteCommit error:error];
git_merge(self.git_repository, (const git_annotated_commit **)&annotatedCommit, 1, &merge_opts, &checkout_opts);
return NO;
}
GTTree *newTree = [index writeTreeToRepository:self error:error];
if (!newTree) {
return NO;
}
// Create merge commit
NSString *message = [NSString stringWithFormat:@"Merge branch '%@'", localBranch.shortName];
NSArray *parents = @[ localCommit, remoteCommit ];
// FIXME: This is stepping on the local tree
GTCommit *mergeCommit = [self createCommitWithTree:newTree message:message parents:parents updatingReferenceNamed:localBranch.reference.name error:error];
if (!mergeCommit) {
return NO;
}
BOOL success = [self checkoutReference:localBranch.reference options:[GTCheckoutOptions checkoutOptionsWithStrategy:GTCheckoutStrategyForce] error:error];
return success;
}
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;
}
NSString *mergedContent = [[NSString alloc] initWithBytes:result.ptr length:result.len encoding:NSUTF8StringEncoding];
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) {
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to lookup annotated commit for %@", fromCommit];
return NO;
}
return YES;
}
- (BOOL)analyzeMerge:(GTMergeAnalysis *)analysis fromBranch:(GTBranch *)fromBranch error:(NSError **)error {
NSParameterAssert(analysis != NULL);
NSParameterAssert(fromBranch != nil);
GTCommit *fromCommit = [fromBranch targetCommitWithError:error];
if (!fromCommit) {
return NO;
}
git_annotated_commit *annotatedCommit;
[self annotatedCommit:&annotatedCommit fromCommit:fromCommit error:error];
// Allow fast-forward or normal merge
git_merge_preference_t preference = GIT_MERGE_PREFERENCE_NONE;
// Merge analysis
int gitError = git_merge_analysis((git_merge_analysis_t *)analysis, &preference, self.git_repository, (const git_annotated_commit **) &annotatedCommit, 1);
if (gitError != GIT_OK) {
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to analyze merge"];
return NO;
}
// Cleanup
git_annotated_commit_free(annotatedCommit);
return YES;
}
@end