Skip to content

API changes for push in libgit2 0.22 #440

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 10, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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