Skip to content

Commit 93162fd

Browse files
committed
Merge pull request #440 from phatblat/ben/pr/push2
API changes for push in libgit2 0.22
2 parents 2f9a457 + b3e79e0 commit 93162fd

File tree

4 files changed

+330
-0
lines changed

4 files changed

+330
-0
lines changed

ObjectiveGit/GTRepository+RemoteOperations.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,34 @@ extern NSString *const GTRepositoryRemoteOptionsCredentialProvider;
4545
/// Retruns an array with GTFetchHeadEntry objects
4646
- (NSArray *)fetchHeadEntriesWithError:(NSError **)error;
4747

48+
#pragma mark - Push
49+
50+
/// Push a single branch to a remote.
51+
///
52+
/// branch - The branch to push. Must not be nil.
53+
/// remote - The remote to push to. Must not be nil.
54+
/// options - Options applied to the push operation. Can be NULL.
55+
/// Recognized options are:
56+
/// `GTRepositoryRemoteOptionsCredentialProvider`
57+
/// error - The error if one occurred. Can be NULL.
58+
/// progressBlock - An optional callback for monitoring progress.
59+
///
60+
/// Returns YES if the push was successful, NO otherwise (and `error`, if provided,
61+
/// will point to an error describing what happened).
62+
- (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;
63+
64+
/// Push an array of branches to a remote.
65+
///
66+
/// branches - An array of branches to push. Must not be nil.
67+
/// remote - The remote to push to. Must not be nil.
68+
/// options - Options applied to the push operation. Can be NULL.
69+
/// Recognized options are:
70+
/// `GTRepositoryRemoteOptionsCredentialProvider`
71+
/// error - The error if one occurred. Can be NULL.
72+
/// progressBlock - An optional callback for monitoring progress.
73+
///
74+
/// Returns YES if the push was successful, NO otherwise (and `error`, if provided,
75+
/// will point to an error describing what happened).
76+
- (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;
77+
4878
@end

ObjectiveGit/GTRepository+RemoteOperations.m

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#import "GTOID.h"
1616
#import "GTRemote.h"
1717
#import "GTSignature.h"
18+
#import "NSArray+StringArray.h"
1819
#import "NSError+Git.h"
1920

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

163+
#pragma mark - Push (Public)
164+
165+
- (BOOL)pushBranch:(GTBranch *)branch toRemote:(GTRemote *)remote withOptions:(NSDictionary *)options error:(NSError **)error progress:(GTRemotePushTransferProgressBlock)progressBlock {
166+
NSParameterAssert(branch != nil);
167+
NSParameterAssert(remote != nil);
168+
169+
return [self pushBranches:@[ branch ] toRemote:remote withOptions:options error:error progress:progressBlock];
170+
}
171+
172+
- (BOOL)pushBranches:(NSArray *)branches toRemote:(GTRemote *)remote withOptions:(NSDictionary *)options error:(NSError **)error progress:(GTRemotePushTransferProgressBlock)progressBlock {
173+
NSParameterAssert(branches != nil);
174+
NSParameterAssert(branches.count != 0);
175+
NSParameterAssert(remote != nil);
176+
177+
NSMutableArray *refspecs = nil;
178+
// Build refspecs for the passed in branches
179+
refspecs = [NSMutableArray arrayWithCapacity:branches.count];
180+
for (GTBranch *branch in branches) {
181+
// Default remote reference for when branch doesn't exist on remote - create with same short name
182+
NSString *remoteBranchReference = [NSString stringWithFormat:@"refs/heads/%@", branch.shortName];
183+
184+
BOOL success = NO;
185+
GTBranch *trackingBranch = [branch trackingBranchWithError:error success:&success];
186+
187+
if (success && trackingBranch != nil) {
188+
// Use remote branch short name from trackingBranch, which could be different
189+
// (e.g. refs/heads/master:refs/heads/my_master)
190+
remoteBranchReference = [NSString stringWithFormat:@"refs/heads/%@", trackingBranch.shortName];
191+
}
192+
193+
[refspecs addObject:[NSString stringWithFormat:@"refs/heads/%@:%@", branch.shortName, remoteBranchReference]];
194+
}
195+
196+
return [self pushRefspecs:refspecs toRemote:remote withOptions:options error:error progress:progressBlock];
197+
}
198+
199+
#pragma mark - Push (Private)
200+
201+
- (BOOL)pushRefspecs:(NSArray *)refspecs toRemote:(GTRemote *)remote withOptions:(NSDictionary *)options error:(NSError **)error progress:(GTRemotePushTransferProgressBlock)progressBlock {
202+
int gitError;
203+
GTCredentialProvider *credProvider = options[GTRepositoryRemoteOptionsCredentialProvider];
204+
205+
GTRemoteConnectionInfo connectionInfo = {
206+
.credProvider = { .credProvider = credProvider },
207+
.direction = GIT_DIRECTION_PUSH,
208+
.pushProgressBlock = progressBlock,
209+
};
210+
211+
git_remote_callbacks remote_callbacks = GIT_REMOTE_CALLBACKS_INIT;
212+
remote_callbacks.credentials = (credProvider != nil ? GTCredentialAcquireCallback : NULL),
213+
remote_callbacks.transfer_progress = GTRemoteFetchTransferProgressCallback,
214+
remote_callbacks.payload = &connectionInfo,
215+
216+
gitError = git_remote_set_callbacks(remote.git_remote, &remote_callbacks);
217+
if (gitError != GIT_OK) {
218+
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to set callbacks on remote"];
219+
return NO;
220+
}
221+
222+
gitError = git_remote_connect(remote.git_remote, GIT_DIRECTION_PUSH);
223+
if (gitError != GIT_OK) {
224+
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to connect remote"];
225+
return NO;
226+
}
227+
@onExit {
228+
git_remote_disconnect(remote.git_remote);
229+
// Clear out callbacks by overwriting with an effectively empty git_remote_callbacks struct
230+
git_remote_set_callbacks(remote.git_remote, &((git_remote_callbacks)GIT_REMOTE_CALLBACKS_INIT));
231+
};
232+
233+
git_push_options push_options = GIT_PUSH_OPTIONS_INIT;
234+
235+
gitError = git_push_init_options(&push_options, GIT_PUSH_OPTIONS_VERSION);
236+
if (gitError != GIT_OK) {
237+
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to init push options"];
238+
return NO;
239+
}
240+
241+
const git_strarray git_refspecs = refspecs.git_strarray;
242+
243+
gitError = git_remote_upload(remote.git_remote, &git_refspecs, &push_options);
244+
if (gitError != GIT_OK) {
245+
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Push upload to remote failed"];
246+
return NO;
247+
}
248+
249+
gitError = git_remote_update_tips(remote.git_remote, self.userSignatureForNow.git_signature, NULL);
250+
if (gitError != GIT_OK) {
251+
if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Update tips failed"];
252+
return NO;
253+
}
254+
255+
return YES;
256+
}
257+
162258
@end

ObjectiveGitFramework.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@
307307
DD3D9513182A81E1004AF532 /* GTBlame.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3D9511182A81E1004AF532 /* GTBlame.m */; };
308308
DD3D951C182AB25C004AF532 /* GTBlameHunk.h in Headers */ = {isa = PBXBuildFile; fileRef = DD3D951A182AB25C004AF532 /* GTBlameHunk.h */; settings = {ATTRIBUTES = (Public, ); }; };
309309
DD3D951D182AB25C004AF532 /* GTBlameHunk.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3D951B182AB25C004AF532 /* GTBlameHunk.m */; };
310+
F8E4A2911A170CA6006485A8 /* GTRemotePushSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = F8E4A2901A170CA6006485A8 /* GTRemotePushSpec.m */; };
310311
/* End PBXBuildFile section */
311312

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

572574
/* Begin PBXFrameworksBuildPhase section */
@@ -717,6 +719,7 @@
717719
88F05AA816011FFD00B7AD1D /* GTObjectSpec.m */,
718720
D00F6815175D373C004DB9D6 /* GTReferenceSpec.m */,
719721
88215482171499BE00D76B76 /* GTReflogSpec.m */,
722+
F8E4A2901A170CA6006485A8 /* GTRemotePushSpec.m */,
720723
4DBA4A3117DA73CE006CD5F5 /* GTRemoteSpec.m */,
721724
200578C418932A82001C06C3 /* GTBlameSpec.m */,
722725
D0AC906B172F941F00347DC4 /* GTRepositorySpec.m */,
@@ -1265,6 +1268,7 @@
12651268
88E353061982EA6B0051001F /* GTRepositoryAttributesSpec.m in Sources */,
12661269
88234B2618F2FE260039972E /* GTRepositoryResetSpec.m in Sources */,
12671270
5BE612931745EEBC00266D8C /* GTTreeBuilderSpec.m in Sources */,
1271+
F8E4A2911A170CA6006485A8 /* GTRemotePushSpec.m in Sources */,
12681272
D06D9E011755D10000558C17 /* GTEnumeratorSpec.m in Sources */,
12691273
D03B7C411756AB370034A610 /* GTSubmoduleSpec.m in Sources */,
12701274
D00F6816175D373C004DB9D6 /* GTReferenceSpec.m in Sources */,

ObjectiveGitTests/GTRemotePushSpec.m

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
//
2+
// GTRemotePushSpec.m
3+
// ObjectiveGitFramework
4+
//
5+
// Created by Ben Chatelain on 11/14/2014.
6+
// Copyright (c) 2014 GitHub, Inc. All rights reserved.
7+
//
8+
9+
#import <Nimble/Nimble.h>
10+
#import <ObjectiveGit/ObjectiveGit.h>
11+
#import <Quick/Quick.h>
12+
13+
#import "QuickSpec+GTFixtures.h"
14+
15+
// Helper to quickly create commits
16+
GTCommit *(^createCommitInRepository)(NSString *, NSData *, NSString *, GTRepository *) = ^ GTCommit * (NSString *message, NSData *fileData, NSString *fileName, GTRepository *repo) {
17+
GTTreeBuilder *treeBuilder = [[GTTreeBuilder alloc] initWithTree:nil repository:repo error:nil];
18+
[treeBuilder addEntryWithData:fileData fileName:fileName fileMode:GTFileModeBlob error:nil];
19+
20+
GTTree *testTree = [treeBuilder writeTree:nil];
21+
22+
// We need the parent commit to make the new one
23+
GTReference *headReference = [repo headReferenceWithError:nil];
24+
25+
GTEnumerator *commitEnum = [[GTEnumerator alloc] initWithRepository:repo error:nil];
26+
[commitEnum pushSHA:[headReference targetSHA] error:nil];
27+
GTCommit *parent = [commitEnum nextObject];
28+
29+
GTCommit *testCommit = [repo createCommitWithTree:testTree message:message parents:@[ parent ] updatingReferenceNamed:headReference.name error:nil];
30+
expect(testCommit).notTo(beNil());
31+
32+
return testCommit;
33+
};
34+
35+
GTBranch *(^localBranchWithName)(NSString *, GTRepository *) = ^ GTBranch * (NSString *branchName, GTRepository *repo) {
36+
NSString *reference = [GTBranch.localNamePrefix stringByAppendingString:branchName];
37+
NSArray *branches = [repo branchesWithPrefix:reference error:NULL];
38+
expect(branches).notTo(beNil());
39+
expect(@(branches.count)).to(equal(@1));
40+
expect(((GTBranch *)branches[0]).shortName).to(equal(branchName));
41+
42+
return branches[0];
43+
};
44+
45+
#pragma mark - GTRemotePushSpec
46+
47+
QuickSpecBegin(GTRemotePushSpec)
48+
49+
describe(@"pushing", ^{
50+
__block GTRepository *notBareRepo;
51+
52+
beforeEach(^{
53+
notBareRepo = self.bareFixtureRepository;
54+
expect(notBareRepo).notTo(beNil());
55+
// This repo is not really "bare" according to libgit2
56+
expect(@(notBareRepo.isBare)).to(beFalsy());
57+
});
58+
59+
describe(@"to remote", ^{ // via local transport
60+
__block NSURL *remoteRepoURL;
61+
__block NSURL *localRepoURL;
62+
__block GTRepository *remoteRepo;
63+
__block GTRepository *localRepo;
64+
__block GTRemote *remote;
65+
__block NSError *error;
66+
67+
beforeEach(^{
68+
// Make a bare clone to serve as the remote
69+
remoteRepoURL = [notBareRepo.gitDirectoryURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"bare_remote_repo.git"];
70+
NSDictionary *options = @{ GTRepositoryCloneOptionsBare: @1 };
71+
remoteRepo = [GTRepository cloneFromURL:notBareRepo.gitDirectoryURL toWorkingDirectory:remoteRepoURL options:options error:&error transferProgressBlock:NULL checkoutProgressBlock:NULL];
72+
expect(error).to(beNil());
73+
expect(remoteRepo).notTo(beNil());
74+
expect(@(remoteRepo.isBare)).to(beTruthy()); // that's better
75+
76+
localRepoURL = [remoteRepoURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"local_push_repo"];
77+
expect(localRepoURL).notTo(beNil());
78+
79+
// Local clone for testing pushes
80+
localRepo = [GTRepository cloneFromURL:remoteRepoURL toWorkingDirectory:localRepoURL options:nil error:&error transferProgressBlock:NULL checkoutProgressBlock:NULL];
81+
82+
expect(error).to(beNil());
83+
expect(localRepo).notTo(beNil());
84+
85+
GTConfiguration *configuration = [localRepo configurationWithError:&error];
86+
expect(error).to(beNil());
87+
expect(configuration).notTo(beNil());
88+
89+
expect(@(configuration.remotes.count)).to(equal(@1));
90+
91+
remote = configuration.remotes[0];
92+
expect(remote.name).to(equal(@"origin"));
93+
});
94+
95+
afterEach(^{
96+
[NSFileManager.defaultManager removeItemAtURL:remoteRepoURL error:&error];
97+
expect(error).to(beNil());
98+
[NSFileManager.defaultManager removeItemAtURL:localRepoURL error:&error];
99+
expect(error).to(beNil());
100+
error = NULL;
101+
});
102+
103+
context(@"when the local and remote branches are in sync", ^{
104+
it(@"should push no commits", ^{
105+
GTBranch *masterBranch = localBranchWithName(@"master", localRepo);
106+
expect(@([masterBranch numberOfCommitsWithError:NULL])).to(equal(@3));
107+
108+
GTBranch *remoteMasterBranch = localBranchWithName(@"master", remoteRepo);
109+
expect(@([remoteMasterBranch numberOfCommitsWithError:NULL])).to(equal(@3));
110+
111+
// Push
112+
__block BOOL transferProgressed = NO;
113+
BOOL result = [localRepo pushBranch:masterBranch toRemote:remote withOptions:nil error:&error progress:^(unsigned int current, unsigned int total, size_t bytes, BOOL *stop) {
114+
transferProgressed = YES;
115+
}];
116+
expect(error).to(beNil());
117+
expect(@(result)).to(beTruthy());
118+
expect(@(transferProgressed)).to(beFalsy()); // Local transport doesn't currently call progress callbacks
119+
120+
// Same number of commits after push, refresh branch first
121+
remoteMasterBranch = localBranchWithName(@"master", remoteRepo);
122+
expect(@([remoteMasterBranch numberOfCommitsWithError:NULL])).to(equal(@3));
123+
});
124+
});
125+
126+
it(@"can push one commit", ^{
127+
// Create a new commit in the local repo
128+
NSString *testData = @"Test";
129+
NSString *fileName = @"test.txt";
130+
GTCommit *testCommit = createCommitInRepository(@"Test commit", [testData dataUsingEncoding:NSUTF8StringEncoding], fileName, localRepo);
131+
expect(testCommit).notTo(beNil());
132+
133+
// Refetch master branch to ensure the commit count is accurate
134+
GTBranch *masterBranch = localBranchWithName(@"master", localRepo);
135+
expect(@([masterBranch numberOfCommitsWithError:NULL])).to(equal(@4));
136+
137+
// Number of commits on tracking branch before push
138+
BOOL success = NO;
139+
GTBranch *localTrackingBranch = [masterBranch trackingBranchWithError:&error success:&success];
140+
expect(error).to(beNil());
141+
expect(@(success)).to(beTruthy());
142+
expect(@([localTrackingBranch numberOfCommitsWithError:NULL])).to(equal(@3));
143+
144+
// Number of commits on remote before push
145+
GTBranch *remoteMasterBranch = localBranchWithName(@"master", remoteRepo);
146+
expect(@([remoteMasterBranch numberOfCommitsWithError:NULL])).to(equal(@3));
147+
148+
// Push
149+
__block BOOL transferProgressed = NO;
150+
BOOL result = [localRepo pushBranch:masterBranch toRemote:remote withOptions:nil error:&error progress:^(unsigned int current, unsigned int total, size_t bytes, BOOL *stop) {
151+
transferProgressed = YES;
152+
}];
153+
expect(error).to(beNil());
154+
expect(@(result)).to(beTruthy());
155+
expect(@(transferProgressed)).to(beFalsy()); // Local transport doesn't currently call progress callbacks
156+
157+
// Number of commits on tracking branch after push
158+
localTrackingBranch = [masterBranch trackingBranchWithError:&error success:&success];
159+
expect(error).to(beNil());
160+
expect(@(success)).to(beTruthy());
161+
expect(@([localTrackingBranch numberOfCommitsWithError:NULL])).to(equal(@4));
162+
163+
// Refresh remote master branch to ensure the commit count is accurate
164+
remoteMasterBranch = localBranchWithName(@"master", remoteRepo);
165+
166+
// Number of commits in remote repo after push
167+
expect(@([remoteMasterBranch numberOfCommitsWithError:NULL])).to(equal(@4));
168+
169+
// Verify commit is in remote
170+
GTCommit *pushedCommit = [remoteRepo lookUpObjectByOID:testCommit.OID objectType:GTObjectTypeCommit error:&error];
171+
expect(error).to(beNil());
172+
expect(pushedCommit).notTo(beNil());
173+
expect(pushedCommit.OID).to(equal(testCommit.OID));
174+
175+
GTTreeEntry *entry = [[pushedCommit tree] entryWithName:fileName];
176+
expect(entry).notTo(beNil());
177+
178+
GTBlob *fileData = (GTBlob *)[entry GTObject:&error];
179+
expect(error).to(beNil());
180+
expect(fileData).notTo(beNil());
181+
expect(fileData.content).to(equal(testData));
182+
});
183+
184+
it(@"can push two branches", ^{
185+
// refs/heads/master on local
186+
GTBranch *branch1 = localBranchWithName(@"master", localRepo);
187+
188+
// Create refs/heads/new_master on local
189+
[localRepo createReferenceNamed:@"refs/heads/new_master" fromReference:branch1.reference committer:localRepo.userSignatureForNow message:@"Create new_master branch" error:&error];
190+
GTBranch *branch2 = localBranchWithName(@"new_master", localRepo);
191+
192+
BOOL result = [localRepo pushBranches:@[ branch1, branch2 ] toRemote:remote withOptions:nil error:&error progress:NULL];
193+
expect(error).to(beNil());
194+
expect(@(result)).to(beTruthy());
195+
});
196+
});
197+
198+
});
199+
200+
QuickSpecEnd

0 commit comments

Comments
 (0)