|
10 | 10 | #import "GTOID.h"
|
11 | 11 | #import "NSError+Git.h"
|
12 | 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" |
13 | 20 |
|
14 | 21 | typedef void (^GTRemoteFetchTransferProgressBlock)(const git_transfer_progress *stats, BOOL *stop);
|
15 | 22 |
|
@@ -67,6 +74,116 @@ - (NSArray *)mergeHeadEntriesWithError:(NSError **)error {
|
67 | 74 | return entries;
|
68 | 75 | }
|
69 | 76 |
|
| 77 | +- (BOOL)mergeBranchIntoCurrentBranch:(GTBranch *)branch withError:(NSError **)error { |
| 78 | + // Check if merge is necessary |
| 79 | + GTBranch *localBranch = [self currentBranchWithError:error]; |
| 80 | + if (!localBranch) { |
| 81 | + return NO; |
| 82 | + } |
| 83 | + |
| 84 | + GTCommit *localCommit = [localBranch targetCommitWithError:error]; |
| 85 | + if (!localCommit) { |
| 86 | + return NO; |
| 87 | + } |
| 88 | + |
| 89 | + GTCommit *remoteCommit = [branch targetCommitWithError:error]; |
| 90 | + if (!remoteCommit) { |
| 91 | + return NO; |
| 92 | + } |
| 93 | + |
| 94 | + if ([localCommit.SHA isEqualToString:remoteCommit.SHA]) { |
| 95 | + // Local and remote tracking branch are already in sync |
| 96 | + return YES; |
| 97 | + } |
| 98 | + |
| 99 | + GTMergeAnalysis analysis = GTMergeAnalysisNone; |
| 100 | + BOOL success = [self analyzeMerge:&analysis fromBranch:branch error:error]; |
| 101 | + if (!success) { |
| 102 | + return NO; |
| 103 | + } |
| 104 | + |
| 105 | + if (analysis & GTMergeAnalysisUpToDate) { |
| 106 | + // Nothing to do |
| 107 | + return YES; |
| 108 | + } else if (analysis & GTMergeAnalysisFastForward || |
| 109 | + analysis & GTMergeAnalysisUnborn) { |
| 110 | + // Fast-forward branch |
| 111 | + NSString *message = [NSString stringWithFormat:@"merge %@: Fast-forward", branch.name]; |
| 112 | + GTReference *reference = [localBranch.reference referenceByUpdatingTarget:remoteCommit.SHA message:message error:error]; |
| 113 | + BOOL checkoutSuccess = [self checkoutReference:reference strategy:GTCheckoutStrategyForce error:error progressBlock:nil]; |
| 114 | + |
| 115 | + return checkoutSuccess; |
| 116 | + } else if (analysis & GTMergeAnalysisNormal) { |
| 117 | + // Do normal merge |
| 118 | + GTTree *localTree = localCommit.tree; |
| 119 | + GTTree *remoteTree = remoteCommit.tree; |
| 120 | + |
| 121 | + // TODO: Find common ancestor |
| 122 | + GTTree *ancestorTree = nil; |
| 123 | + |
| 124 | + // Merge |
| 125 | + GTIndex *index = [localTree merge:remoteTree ancestor:ancestorTree error:error]; |
| 126 | + if (!index) { |
| 127 | + return NO; |
| 128 | + } |
| 129 | + |
| 130 | + // Check for conflict |
| 131 | + if (index.hasConflicts) { |
| 132 | + NSMutableArray <NSString *>*files = [NSMutableArray array]; |
| 133 | + [index enumerateConflictedFilesWithError:error usingBlock:^(GTIndexEntry * _Nonnull ancestor, GTIndexEntry * _Nonnull ours, GTIndexEntry * _Nonnull theirs, BOOL * _Nonnull stop) { |
| 134 | + [files addObject:ours.path]; |
| 135 | + }]; |
| 136 | + |
| 137 | + if (error != NULL) { |
| 138 | + NSDictionary *userInfo = @{GTPullMergeConflictedFiles: files}; |
| 139 | + *error = [NSError git_errorFor:GIT_ECONFLICT description:@"Merge conflict" userInfo:userInfo failureReason:nil]; |
| 140 | + } |
| 141 | + |
| 142 | + // Write conflicts |
| 143 | + git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT; |
| 144 | + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; |
| 145 | + checkout_opts.checkout_strategy = GIT_CHECKOUT_ALLOW_CONFLICTS; |
| 146 | + |
| 147 | + git_annotated_commit *annotatedCommit; |
| 148 | + [self annotatedCommit:&annotatedCommit fromCommit:remoteCommit error:error]; |
| 149 | + |
| 150 | + git_merge(self.git_repository, (const git_annotated_commit **)&annotatedCommit, 1, &merge_opts, &checkout_opts); |
| 151 | + |
| 152 | + return NO; |
| 153 | + } |
| 154 | + |
| 155 | + GTTree *newTree = [index writeTreeToRepository:self error:error]; |
| 156 | + if (!newTree) { |
| 157 | + return NO; |
| 158 | + } |
| 159 | + |
| 160 | + // Create merge commit |
| 161 | + NSString *message = [NSString stringWithFormat:@"Merge branch '%@'", localBranch.shortName]; |
| 162 | + NSArray *parents = @[ localCommit, remoteCommit ]; |
| 163 | + |
| 164 | + // FIXME: This is stepping on the local tree |
| 165 | + GTCommit *mergeCommit = [self createCommitWithTree:newTree message:message parents:parents updatingReferenceNamed:localBranch.name error:error]; |
| 166 | + if (!mergeCommit) { |
| 167 | + return NO; |
| 168 | + } |
| 169 | + |
| 170 | + BOOL success = [self checkoutReference:localBranch.reference strategy:GTCheckoutStrategyForce error:error progressBlock:nil]; |
| 171 | + return success; |
| 172 | + } |
| 173 | + |
| 174 | + return NO; |
| 175 | +} |
| 176 | + |
| 177 | +- (BOOL)annotatedCommit:(git_annotated_commit **)annotatedCommit fromCommit:(GTCommit *)fromCommit error:(NSError **)error { |
| 178 | + int gitError = git_annotated_commit_lookup(annotatedCommit, self.git_repository, fromCommit.OID.git_oid); |
| 179 | + if (gitError != GIT_OK) { |
| 180 | + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to lookup annotated commit for %@", fromCommit]; |
| 181 | + return NO; |
| 182 | + } |
| 183 | + |
| 184 | + return YES; |
| 185 | +} |
| 186 | + |
70 | 187 | - (BOOL)analyzeMerge:(GTMergeAnalysis *)analysis fromBranch:(GTBranch *)fromBranch error:(NSError **)error {
|
71 | 188 | NSParameterAssert(analysis != NULL);
|
72 | 189 | NSParameterAssert(fromBranch != nil);
|
|
0 commit comments