|  | 
|  | 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