Skip to content

Commit

Permalink
Merge pull request #464 from libgit2/pull
Browse files Browse the repository at this point in the history
Pull
  • Loading branch information
joshaber committed Sep 9, 2015
2 parents 6e5a9d9 + dae0b10 commit 90dacb6
Show file tree
Hide file tree
Showing 12 changed files with 629 additions and 66 deletions.
4 changes: 4 additions & 0 deletions ObjectiveGit/GTRemote.m
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ - (NSUInteger)hash {
return self.name.hash ^ self.URLString.hash;
}

- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p> name: %@, URLString: %@", NSStringFromClass([self class]), self, self.name, self.URLString];
}

#pragma mark API

+ (BOOL)isValidRemoteName:(NSString *)name {
Expand Down
56 changes: 56 additions & 0 deletions ObjectiveGit/GTRepository+Pull.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// GTRepository+Pull.h
// ObjectiveGitFramework
//
// Created by Ben Chatelain on 6/17/15.
// Copyright © 2015 GitHub, Inc. All rights reserved.
//

#import "GTRepository.h"
#import "git2/merge.h"

NS_ASSUME_NONNULL_BEGIN

/// An enum describing the result of the merge analysis.
/// See `git_merge_analysis_t`.
typedef NS_ENUM(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)

#pragma mark - Pull

/// Pull a single branch from a remote.
///
/// branch - The branch to pull.
/// remote - The remote to pull from.
/// options - Options applied to the fetch operation.
/// Recognized options are:
/// `GTRepositoryRemoteOptionsCredentialProvider`
/// error - The error if one occurred. Can be NULL.
/// progressBlock - An optional callback for monitoring progress.
///
/// Returns YES if the pull was successful, NO otherwise (and `error`, if provided,
/// 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
168 changes: 168 additions & 0 deletions ObjectiveGit/GTRepository+Pull.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//
// GTRepository+Pull.m
// ObjectiveGitFramework
//
// Created by Ben Chatelain on 6/17/15.
// Copyright © 2015 GitHub, Inc. All rights reserved.
//

#import "GTRepository+Pull.h"

#import "GTCommit.h"
#import "GTIndex.h"
#import "GTOID.h"
#import "GTRemote.h"
#import "GTReference.h"
#import "GTRepository+Committing.h"
#import "GTRepository+RemoteOperations.h"
#import "GTTree.h"
#import "NSError+Git.h"
#import "git2/errors.h"

@implementation GTRepository (Pull)

#pragma mark - Pull

- (BOOL)pullBranch:(GTBranch *)branch fromRemote:(GTRemote *)remote withOptions:(NSDictionary *)options error:(NSError **)error progress:(GTRemoteFetchTransferProgressBlock)progressBlock {
NSParameterAssert(branch != nil);
NSParameterAssert(remote != nil);

GTRepository *repo = branch.repository;

if (![self fetchRemote:remote withOptions:options error:error progress:progressBlock]) {
return NO;
}

// Get tracking branch after fetch so that it is up-to-date and doesn't need to be refreshed from disk
GTBranch *trackingBranch;
if (branch.branchType == GTBranchTypeLocal) {
BOOL success = NO;
trackingBranch = [branch trackingBranchWithError:error success:&success];
if (!success) {
if (error != NULL) *error = [NSError git_errorFor:GIT_ERROR description:@"Tracking branch not found for %@", branch.name];
return NO;
}
else if (!trackingBranch) {
// Error should already be provided by libgit2
return NO;
}
}
else {
// When given a remote branch, use it as the tracking branch
trackingBranch = branch;
}

// Check if merge is necessary
GTBranch *localBranch = [repo currentBranchWithError:error];
if (!localBranch) {
return NO;
}

GTCommit *localCommit = [localBranch targetCommitWithError:error];
if (!localCommit) {
return NO;
}

GTCommit *remoteCommit = [trackingBranch 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:trackingBranch 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", remote.name, trackingBranch.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) {
if (error != NULL) *error = [NSError git_errorFor:GIT_ECONFLICT description:@"Merge conflict, pull aborted"];
return NO;
}

GTTree *newTree = [index writeTreeToRepository:repo 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 = [repo 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)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;

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 comit for %@", fromCommit];
return NO;
}

// Allow fast-forward or normal merge
git_merge_preference_t preference = GIT_MERGE_PREFERENCE_NONE;

// Merge analysis
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
9 changes: 8 additions & 1 deletion ObjectiveGit/GTRepository.m
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ @interface GTRepository ()
@implementation GTRepository

- (NSString *)description {
if (self.isBare) {
return [NSString stringWithFormat:@"<%@: %p> (bare) gitDirectoryURL: %@", self.class, self, self.gitDirectoryURL];
}
return [NSString stringWithFormat:@"<%@: %p> fileURL: %@", self.class, self, self.fileURL];
}

Expand Down Expand Up @@ -366,7 +369,11 @@ - (GTReference *)headReferenceWithError:(NSError **)error {
git_reference *headRef;
int gitError = git_repository_head(&headRef, self.git_repository);
if (gitError != GIT_OK) {
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to get HEAD"];
NSString *unborn = @"";
if (gitError == GIT_EUNBORNBRANCH) {
unborn = @" (unborn)";
}
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to get HEAD%@", unborn];
return nil;
}

Expand Down
1 change: 1 addition & 0 deletions ObjectiveGit/ObjectiveGit.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ FOUNDATION_EXPORT const unsigned char ObjectiveGitVersionString[];
#import <ObjectiveGit/GTRepository+References.h>
#import <ObjectiveGit/GTRepository+RemoteOperations.h>
#import <ObjectiveGit/GTRepository+Reset.h>
#import <ObjectiveGit/GTRepository+Pull.h>
#import <ObjectiveGit/GTEnumerator.h>
#import <ObjectiveGit/GTCommit.h>
#import <ObjectiveGit/GTCredential.h>
Expand Down
Loading

0 comments on commit 90dacb6

Please sign in to comment.