Skip to content

Commit

Permalink
Merge pull request #559 from libgit2/piet/write-merge-conflicted-files
Browse files Browse the repository at this point in the history
Write merge conflicted files when pulling
  • Loading branch information
joshaber committed Mar 3, 2016
2 parents 5978a65 + 28afdb8 commit b52842f
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 150 deletions.
67 changes: 67 additions & 0 deletions ObjectiveGit/GTRepository+Merging.h
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
212 changes: 212 additions & 0 deletions ObjectiveGit/GTRepository+Merging.m
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
24 changes: 0 additions & 24 deletions ObjectiveGit/GTRepository+Pull.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,9 @@
//

#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,
};

typedef void (^GTRemoteFetchTransferProgressBlock)(const git_transfer_progress *progress, BOOL *stop);

@interface GTRepository (Pull)
Expand All @@ -44,16 +30,6 @@ typedef void (^GTRemoteFetchTransferProgressBlock)(const git_transfer_progress *
/// will point to an error describing what happened).
- (BOOL)pullBranch:(GTBranch *)branch fromRemote:(GTRemote *)remote withOptions:(nullable NSDictionary *)options error:(NSError **)error progress:(nullable GTRemoteFetchTransferProgressBlock)progressBlock;

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

0 comments on commit b52842f

Please sign in to comment.