Skip to content

Commit b52842f

Browse files
committed
Merge pull request #559 from libgit2/piet/write-merge-conflicted-files
Write merge conflicted files when pulling
2 parents 5978a65 + 28afdb8 commit b52842f

File tree

7 files changed

+299
-150
lines changed

7 files changed

+299
-150
lines changed

ObjectiveGit/GTRepository+Merging.h

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// GTRepository+Merging.h
3+
// ObjectiveGitFramework
4+
//
5+
// Created by Piet Brauer on 02/03/16.
6+
// Copyright © 2016 GitHub, Inc. All rights reserved.
7+
//
8+
9+
#import "GTRepository.h"
10+
#import "git2/merge.h"
11+
12+
NS_ASSUME_NONNULL_BEGIN
13+
14+
/// UserInfo key for conflicted files when pulling fails with a merge conflict
15+
extern NSString * const GTPullMergeConflictedFiles;
16+
17+
/// An enum describing the result of the merge analysis.
18+
/// See `git_merge_analysis_t`.
19+
typedef NS_OPTIONS(NSInteger, GTMergeAnalysis) {
20+
GTMergeAnalysisNone = GIT_MERGE_ANALYSIS_NONE,
21+
GTMergeAnalysisNormal = GIT_MERGE_ANALYSIS_NORMAL,
22+
GTMergeAnalysisUpToDate = GIT_MERGE_ANALYSIS_UP_TO_DATE,
23+
GTMergeAnalysisUnborn = GIT_MERGE_ANALYSIS_UNBORN,
24+
GTMergeAnalysisFastForward = GIT_MERGE_ANALYSIS_FASTFORWARD,
25+
};
26+
27+
@interface GTRepository (Merging)
28+
29+
/// Enumerate all available merge head entries.
30+
///
31+
/// error - The error if one ocurred. Can be NULL.
32+
/// block - A block to execute for each MERGE_HEAD entry. `mergeHeadEntry` will
33+
/// be the current merge head entry. Setting `stop` to YES will cause
34+
/// enumeration to stop after the block returns. Must not be nil.
35+
///
36+
/// Returns YES if the operation succedded, NO otherwise.
37+
- (BOOL)enumerateMergeHeadEntriesWithError:(NSError **)error usingBlock:(void (^)(GTOID *mergeHeadEntry, BOOL *stop))block;
38+
39+
/// Convenience method for -enumerateMergeHeadEntriesWithError:usingBlock: that retuns an NSArray with all the fetch head entries.
40+
///
41+
/// error - The error if one ocurred. Can be NULL.
42+
///
43+
/// Retruns a (possibly empty) array with GTOID objects. Will not be nil.
44+
- (NSArray <GTOID *>*)mergeHeadEntriesWithError:(NSError **)error;
45+
46+
/// Merge Branch into current branch
47+
///
48+
/// fromBranch - The branch to merge from.
49+
/// error - The error if one occurred. Can be NULL.
50+
///
51+
/// Returns YES if the merge was successful, NO otherwise (and `error`, if provided,
52+
/// will point to an error describing what happened).
53+
- (BOOL)mergeBranchIntoCurrentBranch:(GTBranch *)fromBranch withError:(NSError **)error;
54+
55+
/// Analyze which merge to perform.
56+
///
57+
/// analysis - The resulting analysis.
58+
/// fromBranch - The branch to merge from.
59+
/// error - The error if one occurred. Can be NULL.
60+
///
61+
/// Returns YES if the analysis was successful, NO otherwise (and `error`, if provided,
62+
/// will point to an error describing what happened).
63+
- (BOOL)analyzeMerge:(GTMergeAnalysis *)analysis fromBranch:(GTBranch *)fromBranch error:(NSError **)error;
64+
65+
@end
66+
67+
NS_ASSUME_NONNULL_END

ObjectiveGit/GTRepository+Merging.m

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
//
2+
// GTRepository+Merging.m
3+
// ObjectiveGitFramework
4+
//
5+
// Created by Piet Brauer on 02/03/16.
6+
// Copyright © 2016 GitHub, Inc. All rights reserved.
7+
//
8+
9+
#import "GTRepository+Merging.h"
10+
#import "GTOID.h"
11+
#import "NSError+Git.h"
12+
#import "git2/errors.h"
13+
#import "GTCommit.h"
14+
#import "GTReference.h"
15+
#import "GTRepository+Committing.h"
16+
#import "GTRepository+Pull.h"
17+
#import "GTTree.h"
18+
#import "GTIndex.h"
19+
#import "GTIndexEntry.h"
20+
21+
typedef void (^GTRemoteFetchTransferProgressBlock)(const git_transfer_progress *stats, BOOL *stop);
22+
23+
@implementation GTRepository (Merging)
24+
25+
typedef void (^GTRepositoryEnumerateMergeHeadEntryBlock)(GTOID *entry, BOOL *stop);
26+
27+
typedef struct {
28+
__unsafe_unretained GTRepositoryEnumerateMergeHeadEntryBlock enumerationBlock;
29+
} GTEnumerateMergeHeadEntriesPayload;
30+
31+
int GTMergeHeadEntriesCallback(const git_oid *oid, void *payload) {
32+
GTEnumerateMergeHeadEntriesPayload *entriesPayload = payload;
33+
34+
GTRepositoryEnumerateMergeHeadEntryBlock enumerationBlock = entriesPayload->enumerationBlock;
35+
36+
GTOID *gtoid = [GTOID oidWithGitOid:oid];
37+
38+
BOOL stop = NO;
39+
40+
enumerationBlock(gtoid, &stop);
41+
42+
return (stop == YES ? GIT_EUSER : 0);
43+
}
44+
45+
- (BOOL)enumerateMergeHeadEntriesWithError:(NSError **)error usingBlock:(void (^)(GTOID *mergeHeadEntry, BOOL *stop))block {
46+
NSParameterAssert(block != nil);
47+
48+
GTEnumerateMergeHeadEntriesPayload payload = {
49+
.enumerationBlock = block,
50+
};
51+
52+
int gitError = git_repository_mergehead_foreach(self.git_repository, GTMergeHeadEntriesCallback, &payload);
53+
54+
if (gitError != GIT_OK) {
55+
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to get mergehead entries"];
56+
return NO;
57+
}
58+
59+
return YES;
60+
}
61+
62+
- (NSArray *)mergeHeadEntriesWithError:(NSError **)error {
63+
NSMutableArray *entries = [NSMutableArray array];
64+
65+
[self enumerateMergeHeadEntriesWithError:error usingBlock:^(GTOID *mergeHeadEntry, BOOL *stop) {
66+
[entries addObject:mergeHeadEntry];
67+
68+
*stop = NO;
69+
}];
70+
71+
return entries;
72+
}
73+
74+
- (BOOL)mergeBranchIntoCurrentBranch:(GTBranch *)branch withError:(NSError **)error {
75+
// Check if merge is necessary
76+
GTBranch *localBranch = [self currentBranchWithError:error];
77+
if (!localBranch) {
78+
return NO;
79+
}
80+
81+
GTCommit *localCommit = [localBranch targetCommitWithError:error];
82+
if (!localCommit) {
83+
return NO;
84+
}
85+
86+
GTCommit *remoteCommit = [branch targetCommitWithError:error];
87+
if (!remoteCommit) {
88+
return NO;
89+
}
90+
91+
if ([localCommit.SHA isEqualToString:remoteCommit.SHA]) {
92+
// Local and remote tracking branch are already in sync
93+
return YES;
94+
}
95+
96+
GTMergeAnalysis analysis = GTMergeAnalysisNone;
97+
BOOL success = [self analyzeMerge:&analysis fromBranch:branch error:error];
98+
if (!success) {
99+
return NO;
100+
}
101+
102+
if (analysis & GTMergeAnalysisUpToDate) {
103+
// Nothing to do
104+
return YES;
105+
} else if (analysis & GTMergeAnalysisFastForward ||
106+
analysis & GTMergeAnalysisUnborn) {
107+
// Fast-forward branch
108+
NSString *message = [NSString stringWithFormat:@"merge %@: Fast-forward", branch.name];
109+
GTReference *reference = [localBranch.reference referenceByUpdatingTarget:remoteCommit.SHA message:message error:error];
110+
BOOL checkoutSuccess = [self checkoutReference:reference strategy:GTCheckoutStrategyForce error:error progressBlock:nil];
111+
112+
return checkoutSuccess;
113+
} else if (analysis & GTMergeAnalysisNormal) {
114+
// Do normal merge
115+
GTTree *localTree = localCommit.tree;
116+
GTTree *remoteTree = remoteCommit.tree;
117+
118+
// TODO: Find common ancestor
119+
GTTree *ancestorTree = nil;
120+
121+
// Merge
122+
GTIndex *index = [localTree merge:remoteTree ancestor:ancestorTree error:error];
123+
if (!index) {
124+
return NO;
125+
}
126+
127+
// Check for conflict
128+
if (index.hasConflicts) {
129+
NSMutableArray <NSString *>*files = [NSMutableArray array];
130+
[index enumerateConflictedFilesWithError:error usingBlock:^(GTIndexEntry * _Nonnull ancestor, GTIndexEntry * _Nonnull ours, GTIndexEntry * _Nonnull theirs, BOOL * _Nonnull stop) {
131+
[files addObject:ours.path];
132+
}];
133+
134+
if (error != NULL) {
135+
NSDictionary *userInfo = @{GTPullMergeConflictedFiles: files};
136+
*error = [NSError git_errorFor:GIT_ECONFLICT description:@"Merge conflict" userInfo:userInfo failureReason:nil];
137+
}
138+
139+
// Write conflicts
140+
git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT;
141+
git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
142+
checkout_opts.checkout_strategy = GIT_CHECKOUT_ALLOW_CONFLICTS;
143+
144+
git_annotated_commit *annotatedCommit;
145+
[self annotatedCommit:&annotatedCommit fromCommit:remoteCommit error:error];
146+
147+
git_merge(self.git_repository, (const git_annotated_commit **)&annotatedCommit, 1, &merge_opts, &checkout_opts);
148+
149+
return NO;
150+
}
151+
152+
GTTree *newTree = [index writeTreeToRepository:self error:error];
153+
if (!newTree) {
154+
return NO;
155+
}
156+
157+
// Create merge commit
158+
NSString *message = [NSString stringWithFormat:@"Merge branch '%@'", localBranch.shortName];
159+
NSArray *parents = @[ localCommit, remoteCommit ];
160+
161+
// FIXME: This is stepping on the local tree
162+
GTCommit *mergeCommit = [self createCommitWithTree:newTree message:message parents:parents updatingReferenceNamed:localBranch.name error:error];
163+
if (!mergeCommit) {
164+
return NO;
165+
}
166+
167+
BOOL success = [self checkoutReference:localBranch.reference strategy:GTCheckoutStrategyForce error:error progressBlock:nil];
168+
return success;
169+
}
170+
171+
return NO;
172+
}
173+
174+
- (BOOL)annotatedCommit:(git_annotated_commit **)annotatedCommit fromCommit:(GTCommit *)fromCommit error:(NSError **)error {
175+
int gitError = git_annotated_commit_lookup(annotatedCommit, self.git_repository, fromCommit.OID.git_oid);
176+
if (gitError != GIT_OK) {
177+
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to lookup annotated commit for %@", fromCommit];
178+
return NO;
179+
}
180+
181+
return YES;
182+
}
183+
184+
- (BOOL)analyzeMerge:(GTMergeAnalysis *)analysis fromBranch:(GTBranch *)fromBranch error:(NSError **)error {
185+
NSParameterAssert(analysis != NULL);
186+
NSParameterAssert(fromBranch != nil);
187+
188+
GTCommit *fromCommit = [fromBranch targetCommitWithError:error];
189+
if (!fromCommit) {
190+
return NO;
191+
}
192+
193+
git_annotated_commit *annotatedCommit;
194+
[self annotatedCommit:&annotatedCommit fromCommit:fromCommit error:error];
195+
196+
// Allow fast-forward or normal merge
197+
git_merge_preference_t preference = GIT_MERGE_PREFERENCE_NONE;
198+
199+
// Merge analysis
200+
int gitError = git_merge_analysis((git_merge_analysis_t *)analysis, &preference, self.git_repository, (const git_annotated_commit **) &annotatedCommit, 1);
201+
if (gitError != GIT_OK) {
202+
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to analyze merge"];
203+
return NO;
204+
}
205+
206+
// Cleanup
207+
git_annotated_commit_free(annotatedCommit);
208+
209+
return YES;
210+
}
211+
212+
@end

ObjectiveGit/GTRepository+Pull.h

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,9 @@
77
//
88

99
#import "GTRepository.h"
10-
#import "git2/merge.h"
1110

1211
NS_ASSUME_NONNULL_BEGIN
1312

14-
/// UserInfo key for conflicted files when pulling fails with a merge conflict
15-
extern NSString * const GTPullMergeConflictedFiles;
16-
17-
/// An enum describing the result of the merge analysis.
18-
/// See `git_merge_analysis_t`.
19-
typedef NS_OPTIONS(NSInteger, GTMergeAnalysis) {
20-
GTMergeAnalysisNone = GIT_MERGE_ANALYSIS_NONE,
21-
GTMergeAnalysisNormal = GIT_MERGE_ANALYSIS_NORMAL,
22-
GTMergeAnalysisUpToDate = GIT_MERGE_ANALYSIS_UP_TO_DATE,
23-
GTMergeAnalysisUnborn = GIT_MERGE_ANALYSIS_UNBORN,
24-
GTMergeAnalysisFastForward = GIT_MERGE_ANALYSIS_FASTFORWARD,
25-
};
26-
2713
typedef void (^GTRemoteFetchTransferProgressBlock)(const git_transfer_progress *progress, BOOL *stop);
2814

2915
@interface GTRepository (Pull)
@@ -44,16 +30,6 @@ typedef void (^GTRemoteFetchTransferProgressBlock)(const git_transfer_progress *
4430
/// will point to an error describing what happened).
4531
- (BOOL)pullBranch:(GTBranch *)branch fromRemote:(GTRemote *)remote withOptions:(nullable NSDictionary *)options error:(NSError **)error progress:(nullable GTRemoteFetchTransferProgressBlock)progressBlock;
4632

47-
/// Analyze which merge to perform.
48-
///
49-
/// analysis - The resulting analysis.
50-
/// fromBranch - The branch to merge from.
51-
/// error - The error if one occurred. Can be NULL.
52-
///
53-
/// Returns YES if the analysis was successful, NO otherwise (and `error`, if provided,
54-
/// will point to an error describing what happened).
55-
- (BOOL)analyzeMerge:(GTMergeAnalysis *)analysis fromBranch:(GTBranch *)fromBranch error:(NSError **)error;
56-
5733
@end
5834

5935
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)