|
| 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 |
0 commit comments