Skip to content

Commit

Permalink
Merge pull request #440 from phatblat/ben/pr/push2
Browse files Browse the repository at this point in the history
API changes for push in libgit2 0.22
  • Loading branch information
joshaber committed Feb 10, 2015
2 parents 2f9a457 + b3e79e0 commit 93162fd
Show file tree
Hide file tree
Showing 4 changed files with 330 additions and 0 deletions.
30 changes: 30 additions & 0 deletions ObjectiveGit/GTRepository+RemoteOperations.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,34 @@ extern NSString *const GTRepositoryRemoteOptionsCredentialProvider;
/// Retruns an array with GTFetchHeadEntry objects
- (NSArray *)fetchHeadEntriesWithError:(NSError **)error;

#pragma mark - Push

/// Push a single branch to a remote.
///
/// branch - The branch to push. Must not be nil.
/// remote - The remote to push to. Must not be nil.
/// options - Options applied to the push operation. Can be NULL.
/// Recognized options are:
/// `GTRepositoryRemoteOptionsCredentialProvider`
/// error - The error if one occurred. Can be NULL.
/// progressBlock - An optional callback for monitoring progress.
///
/// Returns YES if the push was successful, NO otherwise (and `error`, if provided,
/// will point to an error describing what happened).
- (BOOL)pushBranch:(GTBranch *)branch toRemote:(GTRemote *)remote withOptions:(NSDictionary *)options error:(NSError **)error progress:(void (^)(unsigned int current, unsigned int total, size_t bytes, BOOL *stop))progressBlock;

/// Push an array of branches to a remote.
///
/// branches - An array of branches to push. Must not be nil.
/// remote - The remote to push to. Must not be nil.
/// options - Options applied to the push operation. Can be NULL.
/// Recognized options are:
/// `GTRepositoryRemoteOptionsCredentialProvider`
/// error - The error if one occurred. Can be NULL.
/// progressBlock - An optional callback for monitoring progress.
///
/// Returns YES if the push was successful, NO otherwise (and `error`, if provided,
/// will point to an error describing what happened).
- (BOOL)pushBranches:(NSArray *)branches toRemote:(GTRemote *)remote withOptions:(NSDictionary *)options error:(NSError **)error progress:(void (^)(unsigned int current, unsigned int total, size_t bytes, BOOL *stop))progressBlock;

@end
96 changes: 96 additions & 0 deletions ObjectiveGit/GTRepository+RemoteOperations.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#import "GTOID.h"
#import "GTRemote.h"
#import "GTSignature.h"
#import "NSArray+StringArray.h"
#import "NSError+Git.h"

#import "git2/errors.h"
Expand Down Expand Up @@ -159,4 +160,99 @@ - (NSArray *)fetchHeadEntriesWithError:(NSError **)error {
return entries;
}

#pragma mark - Push (Public)

- (BOOL)pushBranch:(GTBranch *)branch toRemote:(GTRemote *)remote withOptions:(NSDictionary *)options error:(NSError **)error progress:(GTRemotePushTransferProgressBlock)progressBlock {
NSParameterAssert(branch != nil);
NSParameterAssert(remote != nil);

return [self pushBranches:@[ branch ] toRemote:remote withOptions:options error:error progress:progressBlock];
}

- (BOOL)pushBranches:(NSArray *)branches toRemote:(GTRemote *)remote withOptions:(NSDictionary *)options error:(NSError **)error progress:(GTRemotePushTransferProgressBlock)progressBlock {
NSParameterAssert(branches != nil);
NSParameterAssert(branches.count != 0);
NSParameterAssert(remote != nil);

NSMutableArray *refspecs = nil;
// Build refspecs for the passed in branches
refspecs = [NSMutableArray arrayWithCapacity:branches.count];
for (GTBranch *branch in branches) {
// Default remote reference for when branch doesn't exist on remote - create with same short name
NSString *remoteBranchReference = [NSString stringWithFormat:@"refs/heads/%@", branch.shortName];

BOOL success = NO;
GTBranch *trackingBranch = [branch trackingBranchWithError:error success:&success];

if (success && trackingBranch != nil) {
// Use remote branch short name from trackingBranch, which could be different
// (e.g. refs/heads/master:refs/heads/my_master)
remoteBranchReference = [NSString stringWithFormat:@"refs/heads/%@", trackingBranch.shortName];
}

[refspecs addObject:[NSString stringWithFormat:@"refs/heads/%@:%@", branch.shortName, remoteBranchReference]];
}

return [self pushRefspecs:refspecs toRemote:remote withOptions:options error:error progress:progressBlock];
}

#pragma mark - Push (Private)

- (BOOL)pushRefspecs:(NSArray *)refspecs toRemote:(GTRemote *)remote withOptions:(NSDictionary *)options error:(NSError **)error progress:(GTRemotePushTransferProgressBlock)progressBlock {
int gitError;
GTCredentialProvider *credProvider = options[GTRepositoryRemoteOptionsCredentialProvider];

GTRemoteConnectionInfo connectionInfo = {
.credProvider = { .credProvider = credProvider },
.direction = GIT_DIRECTION_PUSH,
.pushProgressBlock = progressBlock,
};

git_remote_callbacks remote_callbacks = GIT_REMOTE_CALLBACKS_INIT;
remote_callbacks.credentials = (credProvider != nil ? GTCredentialAcquireCallback : NULL),
remote_callbacks.transfer_progress = GTRemoteFetchTransferProgressCallback,
remote_callbacks.payload = &connectionInfo,

gitError = git_remote_set_callbacks(remote.git_remote, &remote_callbacks);
if (gitError != GIT_OK) {
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to set callbacks on remote"];
return NO;
}

gitError = git_remote_connect(remote.git_remote, GIT_DIRECTION_PUSH);
if (gitError != GIT_OK) {
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to connect remote"];
return NO;
}
@onExit {
git_remote_disconnect(remote.git_remote);
// Clear out callbacks by overwriting with an effectively empty git_remote_callbacks struct
git_remote_set_callbacks(remote.git_remote, &((git_remote_callbacks)GIT_REMOTE_CALLBACKS_INIT));
};

git_push_options push_options = GIT_PUSH_OPTIONS_INIT;

gitError = git_push_init_options(&push_options, GIT_PUSH_OPTIONS_VERSION);
if (gitError != GIT_OK) {
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to init push options"];
return NO;
}

const git_strarray git_refspecs = refspecs.git_strarray;

gitError = git_remote_upload(remote.git_remote, &git_refspecs, &push_options);
if (gitError != GIT_OK) {
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Push upload to remote failed"];
return NO;
}

gitError = git_remote_update_tips(remote.git_remote, self.userSignatureForNow.git_signature, NULL);
if (gitError != GIT_OK) {
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Update tips failed"];
return NO;
}

return YES;
}

@end
4 changes: 4 additions & 0 deletions ObjectiveGitFramework.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@
DD3D9513182A81E1004AF532 /* GTBlame.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3D9511182A81E1004AF532 /* GTBlame.m */; };
DD3D951C182AB25C004AF532 /* GTBlameHunk.h in Headers */ = {isa = PBXBuildFile; fileRef = DD3D951A182AB25C004AF532 /* GTBlameHunk.h */; settings = {ATTRIBUTES = (Public, ); }; };
DD3D951D182AB25C004AF532 /* GTBlameHunk.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3D951B182AB25C004AF532 /* GTBlameHunk.m */; };
F8E4A2911A170CA6006485A8 /* GTRemotePushSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = F8E4A2901A170CA6006485A8 /* GTRemotePushSpec.m */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -567,6 +568,7 @@
DD3D951B182AB25C004AF532 /* GTBlameHunk.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTBlameHunk.m; sourceTree = "<group>"; };
E46931A7172740D300F2077D /* update_libgit2 */ = {isa = PBXFileReference; lastKnownFileType = text; name = update_libgit2; path = script/update_libgit2; sourceTree = "<group>"; };
E46931A8172740D300F2077D /* update_libgit2_ios */ = {isa = PBXFileReference; lastKnownFileType = text; name = update_libgit2_ios; path = script/update_libgit2_ios; sourceTree = "<group>"; };
F8E4A2901A170CA6006485A8 /* GTRemotePushSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTRemotePushSpec.m; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -717,6 +719,7 @@
88F05AA816011FFD00B7AD1D /* GTObjectSpec.m */,
D00F6815175D373C004DB9D6 /* GTReferenceSpec.m */,
88215482171499BE00D76B76 /* GTReflogSpec.m */,
F8E4A2901A170CA6006485A8 /* GTRemotePushSpec.m */,
4DBA4A3117DA73CE006CD5F5 /* GTRemoteSpec.m */,
200578C418932A82001C06C3 /* GTBlameSpec.m */,
D0AC906B172F941F00347DC4 /* GTRepositorySpec.m */,
Expand Down Expand Up @@ -1265,6 +1268,7 @@
88E353061982EA6B0051001F /* GTRepositoryAttributesSpec.m in Sources */,
88234B2618F2FE260039972E /* GTRepositoryResetSpec.m in Sources */,
5BE612931745EEBC00266D8C /* GTTreeBuilderSpec.m in Sources */,
F8E4A2911A170CA6006485A8 /* GTRemotePushSpec.m in Sources */,
D06D9E011755D10000558C17 /* GTEnumeratorSpec.m in Sources */,
D03B7C411756AB370034A610 /* GTSubmoduleSpec.m in Sources */,
D00F6816175D373C004DB9D6 /* GTReferenceSpec.m in Sources */,
Expand Down
200 changes: 200 additions & 0 deletions ObjectiveGitTests/GTRemotePushSpec.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//
// GTRemotePushSpec.m
// ObjectiveGitFramework
//
// Created by Ben Chatelain on 11/14/2014.
// Copyright (c) 2014 GitHub, Inc. All rights reserved.
//

#import <Nimble/Nimble.h>
#import <ObjectiveGit/ObjectiveGit.h>
#import <Quick/Quick.h>

#import "QuickSpec+GTFixtures.h"

// Helper to quickly create commits
GTCommit *(^createCommitInRepository)(NSString *, NSData *, NSString *, GTRepository *) = ^ GTCommit * (NSString *message, NSData *fileData, NSString *fileName, GTRepository *repo) {
GTTreeBuilder *treeBuilder = [[GTTreeBuilder alloc] initWithTree:nil repository:repo error:nil];
[treeBuilder addEntryWithData:fileData fileName:fileName fileMode:GTFileModeBlob error:nil];

GTTree *testTree = [treeBuilder writeTree:nil];

// We need the parent commit to make the new one
GTReference *headReference = [repo headReferenceWithError:nil];

GTEnumerator *commitEnum = [[GTEnumerator alloc] initWithRepository:repo error:nil];
[commitEnum pushSHA:[headReference targetSHA] error:nil];
GTCommit *parent = [commitEnum nextObject];

GTCommit *testCommit = [repo createCommitWithTree:testTree message:message parents:@[ parent ] updatingReferenceNamed:headReference.name error:nil];
expect(testCommit).notTo(beNil());

return testCommit;
};

GTBranch *(^localBranchWithName)(NSString *, GTRepository *) = ^ GTBranch * (NSString *branchName, GTRepository *repo) {
NSString *reference = [GTBranch.localNamePrefix stringByAppendingString:branchName];
NSArray *branches = [repo branchesWithPrefix:reference error:NULL];
expect(branches).notTo(beNil());
expect(@(branches.count)).to(equal(@1));
expect(((GTBranch *)branches[0]).shortName).to(equal(branchName));

return branches[0];
};

#pragma mark - GTRemotePushSpec

QuickSpecBegin(GTRemotePushSpec)

describe(@"pushing", ^{
__block GTRepository *notBareRepo;

beforeEach(^{
notBareRepo = self.bareFixtureRepository;
expect(notBareRepo).notTo(beNil());
// This repo is not really "bare" according to libgit2
expect(@(notBareRepo.isBare)).to(beFalsy());
});

describe(@"to remote", ^{ // via local transport
__block NSURL *remoteRepoURL;
__block NSURL *localRepoURL;
__block GTRepository *remoteRepo;
__block GTRepository *localRepo;
__block GTRemote *remote;
__block NSError *error;

beforeEach(^{
// Make a bare clone to serve as the remote
remoteRepoURL = [notBareRepo.gitDirectoryURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"bare_remote_repo.git"];
NSDictionary *options = @{ GTRepositoryCloneOptionsBare: @1 };
remoteRepo = [GTRepository cloneFromURL:notBareRepo.gitDirectoryURL toWorkingDirectory:remoteRepoURL options:options error:&error transferProgressBlock:NULL checkoutProgressBlock:NULL];
expect(error).to(beNil());
expect(remoteRepo).notTo(beNil());
expect(@(remoteRepo.isBare)).to(beTruthy()); // that's better

localRepoURL = [remoteRepoURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"local_push_repo"];
expect(localRepoURL).notTo(beNil());

// Local clone for testing pushes
localRepo = [GTRepository cloneFromURL:remoteRepoURL toWorkingDirectory:localRepoURL options:nil error:&error transferProgressBlock:NULL checkoutProgressBlock:NULL];

expect(error).to(beNil());
expect(localRepo).notTo(beNil());

GTConfiguration *configuration = [localRepo configurationWithError:&error];
expect(error).to(beNil());
expect(configuration).notTo(beNil());

expect(@(configuration.remotes.count)).to(equal(@1));

remote = configuration.remotes[0];
expect(remote.name).to(equal(@"origin"));
});

afterEach(^{
[NSFileManager.defaultManager removeItemAtURL:remoteRepoURL error:&error];
expect(error).to(beNil());
[NSFileManager.defaultManager removeItemAtURL:localRepoURL error:&error];
expect(error).to(beNil());
error = NULL;
});

context(@"when the local and remote branches are in sync", ^{
it(@"should push no commits", ^{
GTBranch *masterBranch = localBranchWithName(@"master", localRepo);
expect(@([masterBranch numberOfCommitsWithError:NULL])).to(equal(@3));

GTBranch *remoteMasterBranch = localBranchWithName(@"master", remoteRepo);
expect(@([remoteMasterBranch numberOfCommitsWithError:NULL])).to(equal(@3));

// Push
__block BOOL transferProgressed = NO;
BOOL result = [localRepo pushBranch:masterBranch toRemote:remote withOptions:nil error:&error progress:^(unsigned int current, unsigned int total, size_t bytes, BOOL *stop) {
transferProgressed = YES;
}];
expect(error).to(beNil());
expect(@(result)).to(beTruthy());
expect(@(transferProgressed)).to(beFalsy()); // Local transport doesn't currently call progress callbacks

// Same number of commits after push, refresh branch first
remoteMasterBranch = localBranchWithName(@"master", remoteRepo);
expect(@([remoteMasterBranch numberOfCommitsWithError:NULL])).to(equal(@3));
});
});

it(@"can push one commit", ^{
// Create a new commit in the local repo
NSString *testData = @"Test";
NSString *fileName = @"test.txt";
GTCommit *testCommit = createCommitInRepository(@"Test commit", [testData dataUsingEncoding:NSUTF8StringEncoding], fileName, localRepo);
expect(testCommit).notTo(beNil());

// Refetch master branch to ensure the commit count is accurate
GTBranch *masterBranch = localBranchWithName(@"master", localRepo);
expect(@([masterBranch numberOfCommitsWithError:NULL])).to(equal(@4));

// Number of commits on tracking branch before push
BOOL success = NO;
GTBranch *localTrackingBranch = [masterBranch trackingBranchWithError:&error success:&success];
expect(error).to(beNil());
expect(@(success)).to(beTruthy());
expect(@([localTrackingBranch numberOfCommitsWithError:NULL])).to(equal(@3));

// Number of commits on remote before push
GTBranch *remoteMasterBranch = localBranchWithName(@"master", remoteRepo);
expect(@([remoteMasterBranch numberOfCommitsWithError:NULL])).to(equal(@3));

// Push
__block BOOL transferProgressed = NO;
BOOL result = [localRepo pushBranch:masterBranch toRemote:remote withOptions:nil error:&error progress:^(unsigned int current, unsigned int total, size_t bytes, BOOL *stop) {
transferProgressed = YES;
}];
expect(error).to(beNil());
expect(@(result)).to(beTruthy());
expect(@(transferProgressed)).to(beFalsy()); // Local transport doesn't currently call progress callbacks

// Number of commits on tracking branch after push
localTrackingBranch = [masterBranch trackingBranchWithError:&error success:&success];
expect(error).to(beNil());
expect(@(success)).to(beTruthy());
expect(@([localTrackingBranch numberOfCommitsWithError:NULL])).to(equal(@4));

// Refresh remote master branch to ensure the commit count is accurate
remoteMasterBranch = localBranchWithName(@"master", remoteRepo);

// Number of commits in remote repo after push
expect(@([remoteMasterBranch numberOfCommitsWithError:NULL])).to(equal(@4));

// Verify commit is in remote
GTCommit *pushedCommit = [remoteRepo lookUpObjectByOID:testCommit.OID objectType:GTObjectTypeCommit error:&error];
expect(error).to(beNil());
expect(pushedCommit).notTo(beNil());
expect(pushedCommit.OID).to(equal(testCommit.OID));

GTTreeEntry *entry = [[pushedCommit tree] entryWithName:fileName];
expect(entry).notTo(beNil());

GTBlob *fileData = (GTBlob *)[entry GTObject:&error];
expect(error).to(beNil());
expect(fileData).notTo(beNil());
expect(fileData.content).to(equal(testData));
});

it(@"can push two branches", ^{
// refs/heads/master on local
GTBranch *branch1 = localBranchWithName(@"master", localRepo);

// Create refs/heads/new_master on local
[localRepo createReferenceNamed:@"refs/heads/new_master" fromReference:branch1.reference committer:localRepo.userSignatureForNow message:@"Create new_master branch" error:&error];
GTBranch *branch2 = localBranchWithName(@"new_master", localRepo);

BOOL result = [localRepo pushBranches:@[ branch1, branch2 ] toRemote:remote withOptions:nil error:&error progress:NULL];
expect(error).to(beNil());
expect(@(result)).to(beTruthy());
});
});

});

QuickSpecEnd

0 comments on commit 93162fd

Please sign in to comment.