|
| 1 | +// |
| 2 | +// GTRepository+Pull.m |
| 3 | +// ObjectiveGitFramework |
| 4 | +// |
| 5 | +// Created by Ben Chatelain on 6/17/15. |
| 6 | +// Copyright © 2015 GitHub, Inc. All rights reserved. |
| 7 | +// |
| 8 | + |
| 9 | +#import "GTRepository+Pull.h" |
| 10 | + |
| 11 | +#import "GTCommit.h" |
| 12 | +#import "GTIndex.h" |
| 13 | +#import "GTOID.h" |
| 14 | +#import "GTRemote.h" |
| 15 | +#import "GTReference.h" |
| 16 | +#import "GTRepository+Committing.h" |
| 17 | +#import "GTRepository+RemoteOperations.h" |
| 18 | +#import "GTTree.h" |
| 19 | +#import "NSError+Git.h" |
| 20 | +#import "git2/errors.h" |
| 21 | + |
| 22 | +@implementation GTRepository (Pull) |
| 23 | + |
| 24 | +#pragma mark - Pull |
| 25 | + |
| 26 | +- (BOOL)pullBranch:(GTBranch *)branch fromRemote:(GTRemote *)remote withOptions:(NSDictionary *)options error:(NSError **)error progress:(GTRemoteFetchTransferProgressBlock)progressBlock { |
| 27 | + NSParameterAssert(branch != nil); |
| 28 | + NSParameterAssert(remote != nil); |
| 29 | + |
| 30 | + GTRepository *repo = branch.repository; |
| 31 | + |
| 32 | + if (![self fetchRemote:remote withOptions:options error:error progress:progressBlock]) { |
| 33 | + return NO; |
| 34 | + } |
| 35 | + |
| 36 | + // Get tracking branch after fetch so that it is up-to-date and doesn't need to be refreshed from disk |
| 37 | + GTBranch *trackingBranch; |
| 38 | + if (branch.branchType == GTBranchTypeLocal) { |
| 39 | + BOOL success = NO; |
| 40 | + trackingBranch = [branch trackingBranchWithError:error success:&success]; |
| 41 | + if (!success) { |
| 42 | + if (error != NULL) *error = [NSError git_errorFor:GIT_ERROR description:@"Tracking branch not found for %@", branch.name]; |
| 43 | + return NO; |
| 44 | + } |
| 45 | + else if (!trackingBranch) { |
| 46 | + // Error should already be provided by libgit2 |
| 47 | + return NO; |
| 48 | + } |
| 49 | + } |
| 50 | + else { |
| 51 | + // When given a remote branch, use it as the tracking branch |
| 52 | + trackingBranch = branch; |
| 53 | + } |
| 54 | + |
| 55 | + // Check if merge is necessary |
| 56 | + GTBranch *localBranch = [repo currentBranchWithError:error]; |
| 57 | + if (!localBranch) { |
| 58 | + return NO; |
| 59 | + } |
| 60 | + |
| 61 | + GTCommit *localCommit = [localBranch targetCommitWithError:error]; |
| 62 | + if (!localCommit) { |
| 63 | + return NO; |
| 64 | + } |
| 65 | + |
| 66 | + GTCommit *remoteCommit = [trackingBranch targetCommitWithError:error]; |
| 67 | + if (!remoteCommit) { |
| 68 | + return NO; |
| 69 | + } |
| 70 | + |
| 71 | + if ([localCommit.SHA isEqualToString:remoteCommit.SHA]) { |
| 72 | + // Local and remote tracking branch are already in sync |
| 73 | + return YES; |
| 74 | + } |
| 75 | + |
| 76 | + GTMergeAnalysis analysis = GTMergeAnalysisNone; |
| 77 | + BOOL success = [self analyzeMerge:&analysis fromBranch:trackingBranch error:error]; |
| 78 | + if (!success) { |
| 79 | + return NO; |
| 80 | + } |
| 81 | + |
| 82 | + if (analysis & GTMergeAnalysisUpToDate) { |
| 83 | + // Nothing to do |
| 84 | + return YES; |
| 85 | + } else if (analysis & GTMergeAnalysisFastForward || |
| 86 | + analysis & GTMergeAnalysisUnborn) { |
| 87 | + // Fast-forward branch |
| 88 | + NSString *message = [NSString stringWithFormat:@"merge %@/%@: Fast-forward", remote.name, trackingBranch.name]; |
| 89 | + GTReference *reference = [localBranch.reference referenceByUpdatingTarget:remoteCommit.SHA message:message error:error]; |
| 90 | + BOOL checkoutSuccess = [self checkoutReference:reference strategy:GTCheckoutStrategyForce error:error progressBlock:nil]; |
| 91 | + |
| 92 | + return checkoutSuccess; |
| 93 | + } else if (analysis & GTMergeAnalysisNormal) { |
| 94 | + // Do normal merge |
| 95 | + GTTree *localTree = localCommit.tree; |
| 96 | + GTTree *remoteTree = remoteCommit.tree; |
| 97 | + |
| 98 | + // TODO: Find common ancestor |
| 99 | + GTTree *ancestorTree = nil; |
| 100 | + |
| 101 | + // Merge |
| 102 | + GTIndex *index = [localTree merge:remoteTree ancestor:ancestorTree error:error]; |
| 103 | + if (!index) { |
| 104 | + return NO; |
| 105 | + } |
| 106 | + |
| 107 | + // Check for conflict |
| 108 | + if (index.hasConflicts) { |
| 109 | + if (error != NULL) *error = [NSError git_errorFor:GIT_ECONFLICT description:@"Merge conflict, pull aborted"]; |
| 110 | + return NO; |
| 111 | + } |
| 112 | + |
| 113 | + GTTree *newTree = [index writeTreeToRepository:repo error:error]; |
| 114 | + if (!newTree) { |
| 115 | + return NO; |
| 116 | + } |
| 117 | + |
| 118 | + // Create merge commit |
| 119 | + NSString *message = [NSString stringWithFormat:@"Merge branch '%@'", localBranch.shortName]; |
| 120 | + NSArray *parents = @[ localCommit, remoteCommit ]; |
| 121 | + |
| 122 | + // FIXME: This is stepping on the local tree |
| 123 | + GTCommit *mergeCommit = [repo createCommitWithTree:newTree message:message parents:parents updatingReferenceNamed:localBranch.name error:error]; |
| 124 | + if (!mergeCommit) { |
| 125 | + return NO; |
| 126 | + } |
| 127 | + |
| 128 | + BOOL success = [self checkoutReference:localBranch.reference strategy:GTCheckoutStrategyForce error:error progressBlock:nil]; |
| 129 | + return success; |
| 130 | + } |
| 131 | + |
| 132 | + return NO; |
| 133 | +} |
| 134 | + |
| 135 | +- (BOOL)analyzeMerge:(GTMergeAnalysis *)analysis fromBranch:(GTBranch *)fromBranch error:(NSError **)error { |
| 136 | + NSParameterAssert(analysis != NULL); |
| 137 | + NSParameterAssert(fromBranch != nil); |
| 138 | + |
| 139 | + GTCommit *fromCommit = [fromBranch targetCommitWithError:error]; |
| 140 | + if (!fromCommit) { |
| 141 | + return NO; |
| 142 | + } |
| 143 | + |
| 144 | + git_annotated_commit *annotatedCommit; |
| 145 | + |
| 146 | + int gitError = git_annotated_commit_lookup(&annotatedCommit, self.git_repository, fromCommit.OID.git_oid); |
| 147 | + if (gitError != GIT_OK) { |
| 148 | + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to lookup annotated comit for %@", fromCommit]; |
| 149 | + return NO; |
| 150 | + } |
| 151 | + |
| 152 | + // Allow fast-forward or normal merge |
| 153 | + git_merge_preference_t preference = GIT_MERGE_PREFERENCE_NONE; |
| 154 | + |
| 155 | + // Merge analysis |
| 156 | + gitError = git_merge_analysis((git_merge_analysis_t *)analysis, &preference, self.git_repository, (const git_annotated_commit **) &annotatedCommit, 1); |
| 157 | + if (gitError != GIT_OK) { |
| 158 | + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to analyze merge"]; |
| 159 | + return NO; |
| 160 | + } |
| 161 | + |
| 162 | + // Cleanup |
| 163 | + git_annotated_commit_free(annotatedCommit); |
| 164 | + |
| 165 | + return YES; |
| 166 | +} |
| 167 | + |
| 168 | +@end |
0 commit comments