-
Notifications
You must be signed in to change notification settings - Fork 280
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #559 from libgit2/piet/write-merge-conflicted-files
Write merge conflicted files when pulling
- Loading branch information
Showing
7 changed files
with
299 additions
and
150 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// | ||
// GTRepository+Merging.h | ||
// ObjectiveGitFramework | ||
// | ||
// Created by Piet Brauer on 02/03/16. | ||
// Copyright © 2016 GitHub, Inc. All rights reserved. | ||
// | ||
|
||
#import "GTRepository.h" | ||
#import "git2/merge.h" | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
|
||
/// UserInfo key for conflicted files when pulling fails with a merge conflict | ||
extern NSString * const GTPullMergeConflictedFiles; | ||
|
||
/// An enum describing the result of the merge analysis. | ||
/// See `git_merge_analysis_t`. | ||
typedef NS_OPTIONS(NSInteger, GTMergeAnalysis) { | ||
GTMergeAnalysisNone = GIT_MERGE_ANALYSIS_NONE, | ||
GTMergeAnalysisNormal = GIT_MERGE_ANALYSIS_NORMAL, | ||
GTMergeAnalysisUpToDate = GIT_MERGE_ANALYSIS_UP_TO_DATE, | ||
GTMergeAnalysisUnborn = GIT_MERGE_ANALYSIS_UNBORN, | ||
GTMergeAnalysisFastForward = GIT_MERGE_ANALYSIS_FASTFORWARD, | ||
}; | ||
|
||
@interface GTRepository (Merging) | ||
|
||
/// Enumerate all available merge head entries. | ||
/// | ||
/// error - The error if one ocurred. Can be NULL. | ||
/// block - A block to execute for each MERGE_HEAD entry. `mergeHeadEntry` will | ||
/// be the current merge head entry. Setting `stop` to YES will cause | ||
/// enumeration to stop after the block returns. Must not be nil. | ||
/// | ||
/// Returns YES if the operation succedded, NO otherwise. | ||
- (BOOL)enumerateMergeHeadEntriesWithError:(NSError **)error usingBlock:(void (^)(GTOID *mergeHeadEntry, BOOL *stop))block; | ||
|
||
/// Convenience method for -enumerateMergeHeadEntriesWithError:usingBlock: that retuns an NSArray with all the fetch head entries. | ||
/// | ||
/// error - The error if one ocurred. Can be NULL. | ||
/// | ||
/// Retruns a (possibly empty) array with GTOID objects. Will not be nil. | ||
- (NSArray <GTOID *>*)mergeHeadEntriesWithError:(NSError **)error; | ||
|
||
/// Merge Branch into current branch | ||
/// | ||
/// fromBranch - The branch to merge from. | ||
/// error - The error if one occurred. Can be NULL. | ||
/// | ||
/// Returns YES if the merge was successful, NO otherwise (and `error`, if provided, | ||
/// will point to an error describing what happened). | ||
- (BOOL)mergeBranchIntoCurrentBranch:(GTBranch *)fromBranch withError:(NSError **)error; | ||
|
||
/// Analyze which merge to perform. | ||
/// | ||
/// analysis - The resulting analysis. | ||
/// fromBranch - The branch to merge from. | ||
/// error - The error if one occurred. Can be NULL. | ||
/// | ||
/// Returns YES if the analysis was successful, NO otherwise (and `error`, if provided, | ||
/// will point to an error describing what happened). | ||
- (BOOL)analyzeMerge:(GTMergeAnalysis *)analysis fromBranch:(GTBranch *)fromBranch error:(NSError **)error; | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
// | ||
// GTRepository+Merging.m | ||
// ObjectiveGitFramework | ||
// | ||
// Created by Piet Brauer on 02/03/16. | ||
// Copyright © 2016 GitHub, Inc. All rights reserved. | ||
// | ||
|
||
#import "GTRepository+Merging.h" | ||
#import "GTOID.h" | ||
#import "NSError+Git.h" | ||
#import "git2/errors.h" | ||
#import "GTCommit.h" | ||
#import "GTReference.h" | ||
#import "GTRepository+Committing.h" | ||
#import "GTRepository+Pull.h" | ||
#import "GTTree.h" | ||
#import "GTIndex.h" | ||
#import "GTIndexEntry.h" | ||
|
||
typedef void (^GTRemoteFetchTransferProgressBlock)(const git_transfer_progress *stats, BOOL *stop); | ||
|
||
@implementation GTRepository (Merging) | ||
|
||
typedef void (^GTRepositoryEnumerateMergeHeadEntryBlock)(GTOID *entry, BOOL *stop); | ||
|
||
typedef struct { | ||
__unsafe_unretained GTRepositoryEnumerateMergeHeadEntryBlock enumerationBlock; | ||
} GTEnumerateMergeHeadEntriesPayload; | ||
|
||
int GTMergeHeadEntriesCallback(const git_oid *oid, void *payload) { | ||
GTEnumerateMergeHeadEntriesPayload *entriesPayload = payload; | ||
|
||
GTRepositoryEnumerateMergeHeadEntryBlock enumerationBlock = entriesPayload->enumerationBlock; | ||
|
||
GTOID *gtoid = [GTOID oidWithGitOid:oid]; | ||
|
||
BOOL stop = NO; | ||
|
||
enumerationBlock(gtoid, &stop); | ||
|
||
return (stop == YES ? GIT_EUSER : 0); | ||
} | ||
|
||
- (BOOL)enumerateMergeHeadEntriesWithError:(NSError **)error usingBlock:(void (^)(GTOID *mergeHeadEntry, BOOL *stop))block { | ||
NSParameterAssert(block != nil); | ||
|
||
GTEnumerateMergeHeadEntriesPayload payload = { | ||
.enumerationBlock = block, | ||
}; | ||
|
||
int gitError = git_repository_mergehead_foreach(self.git_repository, GTMergeHeadEntriesCallback, &payload); | ||
|
||
if (gitError != GIT_OK) { | ||
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to get mergehead entries"]; | ||
return NO; | ||
} | ||
|
||
return YES; | ||
} | ||
|
||
- (NSArray *)mergeHeadEntriesWithError:(NSError **)error { | ||
NSMutableArray *entries = [NSMutableArray array]; | ||
|
||
[self enumerateMergeHeadEntriesWithError:error usingBlock:^(GTOID *mergeHeadEntry, BOOL *stop) { | ||
[entries addObject:mergeHeadEntry]; | ||
|
||
*stop = NO; | ||
}]; | ||
|
||
return entries; | ||
} | ||
|
||
- (BOOL)mergeBranchIntoCurrentBranch:(GTBranch *)branch withError:(NSError **)error { | ||
// Check if merge is necessary | ||
GTBranch *localBranch = [self currentBranchWithError:error]; | ||
if (!localBranch) { | ||
return NO; | ||
} | ||
|
||
GTCommit *localCommit = [localBranch targetCommitWithError:error]; | ||
if (!localCommit) { | ||
return NO; | ||
} | ||
|
||
GTCommit *remoteCommit = [branch targetCommitWithError:error]; | ||
if (!remoteCommit) { | ||
return NO; | ||
} | ||
|
||
if ([localCommit.SHA isEqualToString:remoteCommit.SHA]) { | ||
// Local and remote tracking branch are already in sync | ||
return YES; | ||
} | ||
|
||
GTMergeAnalysis analysis = GTMergeAnalysisNone; | ||
BOOL success = [self analyzeMerge:&analysis fromBranch:branch error:error]; | ||
if (!success) { | ||
return NO; | ||
} | ||
|
||
if (analysis & GTMergeAnalysisUpToDate) { | ||
// Nothing to do | ||
return YES; | ||
} else if (analysis & GTMergeAnalysisFastForward || | ||
analysis & GTMergeAnalysisUnborn) { | ||
// Fast-forward branch | ||
NSString *message = [NSString stringWithFormat:@"merge %@: Fast-forward", branch.name]; | ||
GTReference *reference = [localBranch.reference referenceByUpdatingTarget:remoteCommit.SHA message:message error:error]; | ||
BOOL checkoutSuccess = [self checkoutReference:reference strategy:GTCheckoutStrategyForce error:error progressBlock:nil]; | ||
|
||
return checkoutSuccess; | ||
} else if (analysis & GTMergeAnalysisNormal) { | ||
// Do normal merge | ||
GTTree *localTree = localCommit.tree; | ||
GTTree *remoteTree = remoteCommit.tree; | ||
|
||
// TODO: Find common ancestor | ||
GTTree *ancestorTree = nil; | ||
|
||
// Merge | ||
GTIndex *index = [localTree merge:remoteTree ancestor:ancestorTree error:error]; | ||
if (!index) { | ||
return NO; | ||
} | ||
|
||
// Check for conflict | ||
if (index.hasConflicts) { | ||
NSMutableArray <NSString *>*files = [NSMutableArray array]; | ||
[index enumerateConflictedFilesWithError:error usingBlock:^(GTIndexEntry * _Nonnull ancestor, GTIndexEntry * _Nonnull ours, GTIndexEntry * _Nonnull theirs, BOOL * _Nonnull stop) { | ||
[files addObject:ours.path]; | ||
}]; | ||
|
||
if (error != NULL) { | ||
NSDictionary *userInfo = @{GTPullMergeConflictedFiles: files}; | ||
*error = [NSError git_errorFor:GIT_ECONFLICT description:@"Merge conflict" userInfo:userInfo failureReason:nil]; | ||
} | ||
|
||
// Write conflicts | ||
git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT; | ||
git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; | ||
checkout_opts.checkout_strategy = GIT_CHECKOUT_ALLOW_CONFLICTS; | ||
|
||
git_annotated_commit *annotatedCommit; | ||
[self annotatedCommit:&annotatedCommit fromCommit:remoteCommit error:error]; | ||
|
||
git_merge(self.git_repository, (const git_annotated_commit **)&annotatedCommit, 1, &merge_opts, &checkout_opts); | ||
|
||
return NO; | ||
} | ||
|
||
GTTree *newTree = [index writeTreeToRepository:self error:error]; | ||
if (!newTree) { | ||
return NO; | ||
} | ||
|
||
// Create merge commit | ||
NSString *message = [NSString stringWithFormat:@"Merge branch '%@'", localBranch.shortName]; | ||
NSArray *parents = @[ localCommit, remoteCommit ]; | ||
|
||
// FIXME: This is stepping on the local tree | ||
GTCommit *mergeCommit = [self createCommitWithTree:newTree message:message parents:parents updatingReferenceNamed:localBranch.name error:error]; | ||
if (!mergeCommit) { | ||
return NO; | ||
} | ||
|
||
BOOL success = [self checkoutReference:localBranch.reference strategy:GTCheckoutStrategyForce error:error progressBlock:nil]; | ||
return success; | ||
} | ||
|
||
return NO; | ||
} | ||
|
||
- (BOOL)annotatedCommit:(git_annotated_commit **)annotatedCommit fromCommit:(GTCommit *)fromCommit error:(NSError **)error { | ||
int gitError = git_annotated_commit_lookup(annotatedCommit, self.git_repository, fromCommit.OID.git_oid); | ||
if (gitError != GIT_OK) { | ||
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to lookup annotated commit for %@", fromCommit]; | ||
return NO; | ||
} | ||
|
||
return YES; | ||
} | ||
|
||
- (BOOL)analyzeMerge:(GTMergeAnalysis *)analysis fromBranch:(GTBranch *)fromBranch error:(NSError **)error { | ||
NSParameterAssert(analysis != NULL); | ||
NSParameterAssert(fromBranch != nil); | ||
|
||
GTCommit *fromCommit = [fromBranch targetCommitWithError:error]; | ||
if (!fromCommit) { | ||
return NO; | ||
} | ||
|
||
git_annotated_commit *annotatedCommit; | ||
[self annotatedCommit:&annotatedCommit fromCommit:fromCommit error:error]; | ||
|
||
// Allow fast-forward or normal merge | ||
git_merge_preference_t preference = GIT_MERGE_PREFERENCE_NONE; | ||
|
||
// Merge analysis | ||
int gitError = git_merge_analysis((git_merge_analysis_t *)analysis, &preference, self.git_repository, (const git_annotated_commit **) &annotatedCommit, 1); | ||
if (gitError != GIT_OK) { | ||
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to analyze merge"]; | ||
return NO; | ||
} | ||
|
||
// Cleanup | ||
git_annotated_commit_free(annotatedCommit); | ||
|
||
return YES; | ||
} | ||
|
||
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.