diff --git a/Classes/Categories/NSError+Git.h b/Classes/Categories/NSError+Git.h index d33324f16..b8b2195a6 100644 --- a/Classes/Categories/NSError+Git.h +++ b/Classes/Categories/NSError+Git.h @@ -68,4 +68,22 @@ extern NSString * const GTGitErrorDomain; /// Returns a non-nil NSError. + (NSError *)git_errorFor:(int)code description:(NSString *)desc, ... NS_FORMAT_FUNCTION(2, 3); + +/// Describes the given libgit2 error code, using `desc` as the error's +/// description, and a failure reason from `reason` and the arguments that +/// follow. +/// +/// The created error will also have an `NSUnderlyingErrorKey` that contains the +/// result of +git_errorFor: on the same error code. +/// +/// code - The error code returned from libgit2. +/// desc - The description to use in the created NSError. This may be nil. +/// userInfo - A dictionary of additional values to insert in the NSError userInfo. +/// This may be nil. +/// reason - A format string to use for the created NSError's failure reason. +/// This may be nil. +/// ... - Format arguments to insert into `reason`. +/// +/// Returns a non-nil NSError. ++ (NSError *)git_errorFor:(int)code description:(NSString *)desc userInfo:(NSDictionary *)userInfo failureReason:(NSString *)reason, ... NS_FORMAT_FUNCTION(4, 5); @end diff --git a/Classes/Categories/NSError+Git.m b/Classes/Categories/NSError+Git.m index 681e10034..300fd1bed 100644 --- a/Classes/Categories/NSError+Git.m +++ b/Classes/Categories/NSError+Git.m @@ -67,6 +67,26 @@ + (NSError *)git_errorFor:(int)code description:(NSString *)desc failureReason:( return [NSError errorWithDomain:GTGitErrorDomain code:code userInfo:userInfo]; } ++ (NSError *)git_errorFor:(int)code description:(NSString *)desc userInfo:(NSDictionary *)additionalUserInfo failureReason:(NSString *)reason, ... { + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:additionalUserInfo]; + + if (desc != nil) userInfo[NSLocalizedDescriptionKey] = desc; + if (reason != nil) { + va_list args; + va_start(args, reason); + + NSString *formattedReason = [[NSString alloc] initWithFormat:reason arguments:args]; + va_end(args); + + userInfo[NSLocalizedFailureReasonErrorKey] = formattedReason; + } + + NSError *underError = [self git_errorFor:code]; + if (underError != nil) userInfo[NSUnderlyingErrorKey] = underError; + + return [NSError errorWithDomain:GTGitErrorDomain code:code userInfo:userInfo]; +} + + (NSError *)git_errorFor:(int)code { NSDictionary *userInfo = nil; diff --git a/Classes/GTBranch.h b/Classes/GTBranch.h index ad1b6f84a..d2d7a56c8 100644 --- a/Classes/GTBranch.h +++ b/Classes/GTBranch.h @@ -27,6 +27,7 @@ @class GTCommit; @class GTReference; +@class GTRemote; @class GTRepository; typedef NS_ENUM(NSInteger, GTBranchType) { diff --git a/Classes/GTBranch.m b/Classes/GTBranch.m index 764cbdfca..f1707cebe 100644 --- a/Classes/GTBranch.m +++ b/Classes/GTBranch.m @@ -28,6 +28,7 @@ #import "GTEnumerator.h" #import "GTRepository.h" #import "GTCommit.h" +#import "GTRemote.h" #import "NSError+Git.h" @implementation GTBranch diff --git a/Classes/GTConfiguration.m b/Classes/GTConfiguration.m index 569e09886..a50bc3c75 100644 --- a/Classes/GTConfiguration.m +++ b/Classes/GTConfiguration.m @@ -128,7 +128,7 @@ - (NSArray *)remotes { git_remote *remote = NULL; if (git_remote_load(&remote, repository.git_repository, name) == 0) { - [remotes addObject:[[GTRemote alloc] initWithGitRemote:remote]]; + [remotes addObject:[[GTRemote alloc] initWithGitRemote:remote inRepository:repository]]; } } diff --git a/Classes/GTFetchHeadEntry.h b/Classes/GTFetchHeadEntry.h new file mode 100644 index 000000000..3a926c862 --- /dev/null +++ b/Classes/GTFetchHeadEntry.h @@ -0,0 +1,38 @@ +// +// GTFetchHeadEntry.h +// ObjectiveGitFramework +// +// Created by Pablo Bendersky on 8/14/14. +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// + +#import + +@class GTRepository; +@class GTOID; +@class GTReference; + +/// A class representing an entry on the FETCH_HEAD file, as returned by the callback of git_repository_fetchhead_foreach. +@interface GTFetchHeadEntry : NSObject + +/// The reference of this fetch entry. +@property (nonatomic, readonly, strong) GTReference *reference; + +/// The remote URL where this entry was originally fetched from. +@property (nonatomic, readonly, copy) NSString *remoteURLString; + +/// The target OID of this fetch entry (what we need to merge with) +@property (nonatomic, readonly, copy) GTOID *targetOID; + +/// Flag indicating if we need to merge this entry or not. +@property (nonatomic, getter = isMerge, readonly) BOOL merge; + +/// Initializes a GTFetchHeadEntry. +/// +/// reference - Reference on the repository. Cannot be nil. +/// remoteURLString - URL String where this was originally fetched from. Cannot be nil. +/// targetOID - Target OID. Cannot be nil. +/// merge - Indicates if this is pending a merge. +- (instancetype)initWithReference:(GTReference *)reference remoteURLString:(NSString *)remoteURLString targetOID:(GTOID *)targetOID isMerge:(BOOL)merge; + +@end diff --git a/Classes/GTFetchHeadEntry.m b/Classes/GTFetchHeadEntry.m new file mode 100644 index 000000000..04a75c28a --- /dev/null +++ b/Classes/GTFetchHeadEntry.m @@ -0,0 +1,36 @@ +// +// GTFetchHeadEntry.m +// ObjectiveGitFramework +// +// Created by Pablo Bendersky on 8/14/14. +// Copyright (c) 2014 GitHub, Inc. All rights reserved. +// + +#import "GTFetchHeadEntry.h" +#import "GTOID.h" + +@implementation GTFetchHeadEntry + +- (instancetype)initWithReference:(GTReference *)reference remoteURLString:(NSString *)remoteURLString targetOID:(GTOID *)targetOID isMerge:(BOOL)merge { + NSParameterAssert(reference != nil); + NSParameterAssert(remoteURLString != nil); + NSParameterAssert(targetOID != nil); + + self = [super init]; + if (self == nil) return nil; + + _reference = reference; + _remoteURLString = [remoteURLString copy]; + _targetOID = [targetOID copy]; + _merge = merge; + + return self; +} + +#pragma mark NSObject + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p>{ reference: %@, remoteURL: %@, targetOID: %@, merge: %i }", self.class, self, self.reference, self.remoteURLString, self.targetOID, (int)self.merge]; +} + +@end diff --git a/Classes/GTRemote.h b/Classes/GTRemote.h index 423982ae1..6bad1af0b 100644 --- a/Classes/GTRemote.h +++ b/Classes/GTRemote.h @@ -8,18 +8,27 @@ #import "git2.h" +@class GTRepository; +@class GTOID; +@class GTReference; +@class GTCredentialProvider; + +extern NSString * const GTRemoteRenameProblematicRefSpecs; + +// Auto Tag settings. See `git_remote_autotag_option_t`. +typedef enum { + GTRemoteDownloadTagsAuto = GIT_REMOTE_DOWNLOAD_TAGS_AUTO, + GTRemoteDownloadTagsNone = GIT_REMOTE_DOWNLOAD_TAGS_NONE, + GTRemoteDownloadTagsAll = GIT_REMOTE_DOWNLOAD_TAGS_ALL, +} GTRemoteAutoTagOption; + /// A class representing a remote for a git repository. /// -/// Analagous to `git_remote` in libgit2. +/// Analogous to `git_remote` in libgit2. @interface GTRemote : NSObject -/// Initializes a new GTRemote to represent an underlying `git_remote`. -/// -/// remote - The underlying `git_remote` object. -- (id)initWithGitRemote:(git_remote *)remote; - -/// The underlying `git_remote` object. -- (git_remote *)git_remote __attribute__((objc_returns_inner_pointer)); +/// The repository owning this remote. +@property (nonatomic, readonly, strong) GTRepository *repository; /// The name of the remote. @property (nonatomic, readonly, copy) NSString *name; @@ -27,12 +36,78 @@ /// The URL string for the remote. @property (nonatomic, readonly, copy) NSString *URLString; +/// The push URL for the remote, if provided. +@property (nonatomic, copy) NSString *pushURLString; + +/// Whether the remote is connected or not. +@property (nonatomic, readonly, getter=isConnected) BOOL connected; + +/// Whether the remote updates FETCH_HEAD when fetched. +/// Defaults to YES. +@property (nonatomic) BOOL updatesFetchHead; + +/// The auto-tag setting for the remote. +@property (nonatomic) GTRemoteAutoTagOption autoTag; + /// The fetch refspecs for this remote. /// /// This array will contain NSStrings of the form /// `+refs/heads/*:refs/remotes/REMOTE/*`. @property (nonatomic, readonly, copy) NSArray *fetchRefspecs; +/// The push refspecs for this remote. +/// +/// This array will contain NSStrings of the form +/// `+refs/heads/*:refs/remotes/REMOTE/*`. +@property (nonatomic, readonly, copy) NSArray *pushRefspecs; + +/// Tests if a URL is supported (e.g. it's a supported URL scheme) ++ (BOOL)isSupportedURLString:(NSString *)URLString; + +/// Tests if a URL is valid (e.g. it actually makes sense as a URL) ++ (BOOL)isValidURLString:(NSString *)URLString; + +/// Tests if a name is valid ++ (BOOL)isValidRemoteName:(NSString *)name; + +/// Create a new remote in a repository. +/// +/// name - The name for the new remote. Cannot be nil. +/// URLString - The origin URL for the remote. Cannot be nil. +/// repo - The repository the remote should be created in. Cannot be nil. +/// error - Will be set if an error occurs. +/// +/// Returns a new remote, or nil if an error occurred ++ (instancetype)createRemoteWithName:(NSString *)name URLString:(NSString *)URLString inRepository:(GTRepository *)repo error:(NSError **)error; + +/// Load a remote from a repository. +/// +/// name - The name for the new remote. Cannot be nil. +/// repo - The repository the remote should be looked up in. Cannot be nil. +/// error - Will be set if an error occurs. +/// +/// Returns the loaded remote, or nil if an error occurred. ++ (instancetype)remoteWithName:(NSString *)name inRepository:(GTRepository *)repo error:(NSError **)error; + +/// Initialize a remote from a `git_remote`. +/// +/// remote - The underlying `git_remote` object. Cannot be nil. +/// repo - The repository the remote belongs to. Cannot be nil. +- (instancetype)initWithGitRemote:(git_remote *)remote inRepository:(GTRepository *)repo; + +/// The underlying `git_remote` object. +- (git_remote *)git_remote __attribute__((objc_returns_inner_pointer)); + +/// Rename the remote. +/// +/// name - The new name for the remote. Cannot be nil. +/// error - Will be set if an error occurs. If there was an error renaming some +/// refspecs, their names will be available as an arry under the +/// `GTRemoteRenameProblematicRefSpecs` key. +/// +/// Return YES if successful, NO otherwise. +- (BOOL)rename:(NSString *)name error:(NSError **)error; + /// Updates the URL string for this remote. /// /// URLString - The URLString to update to. May not be nil. diff --git a/Classes/GTRemote.m b/Classes/GTRemote.m index a7a3d9778..2ffae77ad 100644 --- a/Classes/GTRemote.m +++ b/Classes/GTRemote.m @@ -7,28 +7,66 @@ // #import "GTRemote.h" +#import "GTRepository.h" +#import "GTOID.h" +#import "GTCredential+Private.h" +#import "GTBranch.h" #import "NSError+Git.h" #import "NSArray+StringArray.h" #import "EXTScope.h" +NSString * const GTRemoteRenameProblematicRefSpecs = @"GTRemoteRenameProblematicRefSpecs"; + @interface GTRemote () @property (nonatomic, readonly, assign) git_remote *git_remote; - @end @implementation GTRemote #pragma mark Lifecycle -- (id)initWithGitRemote:(git_remote *)remote { ++ (instancetype)createRemoteWithName:(NSString *)name URLString:(NSString *)URLString inRepository:(GTRepository *)repo error:(NSError **)error { + NSParameterAssert(name != nil); + NSParameterAssert(URLString != nil); + NSParameterAssert(repo != nil); + + git_remote *remote; + int gitError = git_remote_create(&remote, repo.git_repository, name.UTF8String, URLString.UTF8String); + if (gitError != GIT_OK) { + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Remote creation failed" failureReason:@"Failed to create a remote named \"%@\" for \"%@\"", name, URLString]; + + return nil; + } + + return [[self alloc] initWithGitRemote:remote inRepository:repo]; +} + ++ (instancetype)remoteWithName:(NSString *)name inRepository:(GTRepository *)repo error:(NSError **)error { + NSParameterAssert(name != nil); + NSParameterAssert(repo != nil); + + git_remote *remote; + int gitError = git_remote_load(&remote, repo.git_repository, name.UTF8String); + if (gitError != GIT_OK) { + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Remote loading failed" failureReason:nil]; + + return nil; + } + + return [[self alloc] initWithGitRemote:remote inRepository:repo]; +} + +- (instancetype)initWithGitRemote:(git_remote *)remote inRepository:(GTRepository *)repo { NSParameterAssert(remote != NULL); + NSParameterAssert(repo != nil); self = [super init]; if (self == nil) return nil; _git_remote = remote; + _repository = repo; return self; } @@ -50,6 +88,26 @@ - (NSUInteger)hash { return self.name.hash ^ self.URLString.hash; } +#pragma mark API + ++ (BOOL)isValidURLString:(NSString *)URLString { + NSParameterAssert(URLString != nil); + + return git_remote_valid_url(URLString.UTF8String) == GIT_OK; +} + ++ (BOOL)isSupportedURLString:(NSString *)URLString { + NSParameterAssert(URLString != nil); + + return git_remote_supported_url(URLString.UTF8String) == GIT_OK; +} + ++ (BOOL)isValidRemoteName:(NSString *)name { + NSParameterAssert(name != nil); + + return git_remote_is_valid_name(name.UTF8String) == GIT_OK; +} + #pragma mark Properties - (NSString *)name { @@ -66,6 +124,61 @@ - (NSString *)URLString { return @(URLString); } +- (void)setURLString:(NSString *)URLString { + git_remote_set_url(self.git_remote, URLString.UTF8String); +} + +- (NSString *)pushURLString { + const char *pushURLString = git_remote_pushurl(self.git_remote); + if (pushURLString == NULL) return nil; + + return @(pushURLString); +} + +- (void)setPushURLString:(NSString *)pushURLString { + git_remote_set_pushurl(self.git_remote, pushURLString.UTF8String); +} + +- (BOOL)updatesFetchHead { + return git_remote_update_fetchhead(self.git_remote) != 0; +} + +- (void)setUpdatesFetchHead:(BOOL)updatesFetchHead { + git_remote_set_update_fetchhead(self.git_remote, updatesFetchHead); +} + +- (GTRemoteAutoTagOption)autoTag { + return (GTRemoteAutoTagOption)git_remote_autotag(self.git_remote); +} + +- (void)setAutoTag:(GTRemoteAutoTagOption)autoTag { + git_remote_set_autotag(self.git_remote, (git_remote_autotag_option_t)autoTag); +} + +- (BOOL)isConnected { + return git_remote_connected(self.git_remote) != 0; +} + +#pragma mark Renaming + +- (BOOL)rename:(NSString *)name error:(NSError **)error { + NSParameterAssert(name != nil); + + git_strarray problematic_refspecs; + + int gitError = git_remote_rename(&problematic_refspecs, self.git_remote, name.UTF8String); + if (gitError != GIT_OK) { + NSArray *problematicRefspecs = [NSArray git_arrayWithStrarray:problematic_refspecs]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:problematicRefspecs forKey:GTRemoteRenameProblematicRefSpecs]; + + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to rename remote" userInfo:userInfo failureReason:@"Couldn't rename remote %@ to %@", self.name, name]; + } + + git_strarray_free(&problematic_refspecs); + + return gitError == GIT_OK; +} + - (NSArray *)fetchRefspecs { __block git_strarray refspecs; int gitError = git_remote_get_fetch_refspecs(&refspecs, self.git_remote); @@ -79,6 +192,18 @@ - (NSArray *)fetchRefspecs { } +- (NSArray *)pushRefspecs { + __block git_strarray refspecs; + int gitError = git_remote_get_push_refspecs(&refspecs, self.git_remote); + if (gitError != GIT_OK) return nil; + + @onExit { + git_strarray_free(&refspecs); + }; + + return [NSArray git_arrayWithStrarray:refspecs]; +} + #pragma mark Update the remote - (BOOL)saveRemote:(NSError **)error { diff --git a/Classes/GTRepository+RemoteOperations.h b/Classes/GTRepository+RemoteOperations.h new file mode 100644 index 000000000..029745234 --- /dev/null +++ b/Classes/GTRepository+RemoteOperations.h @@ -0,0 +1,46 @@ +// +// GTRepository+RemoteOperations.h +// ObjectiveGitFramework +// +// Created by Etienne on 18/11/13. +// Copyright (c) 2013 GitHub, Inc. All rights reserved. +// + +#import + +/// A `GTCredentialProvider`, that will be used to authenticate against the remote. +extern NSString *const GTRepositoryRemoteOptionsCredentialProvider; + +@class GTFetchHeadEntry; + +@interface GTRepository (RemoteOperations) + +/// Fetch a remote. +/// +/// remote - The remote to fetch from. +/// options - Options applied to the fetch operation. +/// Recognized options are : +/// `GTRepositoryRemoteOptionsCredentialProvider` +/// error - The error if one occurred. Can be NULL. +/// +/// Returns YES if the fetch was successful, NO otherwise (and `error`, if provided, +/// will point to an error describing what happened). +- (BOOL)fetchRemote:(GTRemote *)remote withOptions:(NSDictionary *)options error:(NSError **)error progress:(void (^)(const git_transfer_progress *stats, BOOL *stop))progressBlock; + +/// Enumerate all available fetch head entries. +/// +/// error - The error if one ocurred. Can be NULL. +/// block - A block to execute for each FETCH_HEAD entry. `fetchHeadEntry` will be the current +/// fetch head entry. Setting `stop` to YES will cause enumeration to stop after the block returns. +/// +/// Returns YES if the operation succedded, NO otherwise. +- (BOOL)enumerateFetchHeadEntriesWithError:(NSError **)error usingBlock:(void (^)(GTFetchHeadEntry *fetchHeadEntry, BOOL *stop))block; + +/// Convenience method for -enumerateFetchHeadEntriesWithError:usingBlock: that retuns an NSArray with all the fetch head entries. +/// +/// error - The error if one ocurred. Can be NULL. +/// +/// Retruns an array with GTFetchHeadEntry objects +- (NSArray *)fetchHeadEntriesWithError:(NSError **)error; + +@end diff --git a/Classes/GTRepository+RemoteOperations.m b/Classes/GTRepository+RemoteOperations.m new file mode 100644 index 000000000..dcf096811 --- /dev/null +++ b/Classes/GTRepository+RemoteOperations.m @@ -0,0 +1,130 @@ +// +// GTRepository+RemoteOperations.m +// ObjectiveGitFramework +// +// Created by Etienne on 18/11/13. +// Copyright (c) 2013 GitHub, Inc. All rights reserved. +// + +#import "GTRepository+RemoteOperations.h" + +#import "GTCredential.h" +#import "GTCredential+Private.h" +#import "EXTScope.h" + +NSString *const GTRepositoryRemoteOptionsCredentialProvider = @"GTRepositoryRemoteOptionsCredentialProvider"; + +@implementation GTRepository (RemoteOperations) + +#pragma mark - +#pragma mark Common Remote code + +typedef void (^GTRemoteFetchTransferProgressBlock)(const git_transfer_progress *stats, BOOL *stop); + +typedef struct { + GTCredentialAcquireCallbackInfo credProvider; + __unsafe_unretained GTRemoteFetchTransferProgressBlock fetchProgressBlock; + __unsafe_unretained GTRemoteFetchTransferProgressBlock pushProgressBlock; + git_direction direction; +} GTRemoteConnectionInfo; + +int GTRemoteFetchTransferProgressCallback(const git_transfer_progress *stats, void *payload) { + GTRemoteConnectionInfo *info = payload; + BOOL stop = NO; + + if (info->fetchProgressBlock != nil) { + info->fetchProgressBlock(stats, &stop); + } + + return (stop == YES ? GIT_EUSER : 0); +} + +#pragma mark - +#pragma mark Fetch + +- (BOOL)fetchRemote:(GTRemote *)remote withOptions:(NSDictionary *)options error:(NSError **)error progress:(GTRemoteFetchTransferProgressBlock)progressBlock { + GTCredentialProvider *credProvider = options[GTRepositoryRemoteOptionsCredentialProvider]; + GTRemoteConnectionInfo connectionInfo = { + .credProvider = {credProvider}, + .direction = GIT_DIRECTION_FETCH, + .fetchProgressBlock = progressBlock, + }; + git_remote_callbacks remote_callbacks = { + .version = GIT_REMOTE_CALLBACKS_VERSION, + .credentials = (credProvider != nil ? GTCredentialAcquireCallback : NULL), + .transfer_progress = GTRemoteFetchTransferProgressCallback, + .payload = &connectionInfo, + }; + + int 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_fetch(remote.git_remote, self.userSignatureForNow.git_signature, NULL); + if (gitError != GIT_OK) { + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to fetch from remote"]; + return NO; + } + + return YES; +} + +#pragma mark - +#pragma mark Fetch Head enumeration + +typedef void (^GTRemoteEnumerateFetchHeadEntryBlock)(GTFetchHeadEntry *entry, BOOL *stop); + +typedef struct { + __unsafe_unretained GTRepository *repository; + __unsafe_unretained GTRemoteEnumerateFetchHeadEntryBlock enumerationBlock; +} GTEnumerateHeadEntriesPayload; + +int GTFetchHeadEntriesCallback(const char *ref_name, const char *remote_url, const git_oid *oid, unsigned int is_merge, void *payload) { + GTEnumerateHeadEntriesPayload *entriesPayload = payload; + + GTRepository *repository = entriesPayload->repository; + GTRemoteEnumerateFetchHeadEntryBlock enumerationBlock = entriesPayload->enumerationBlock; + + GTReference *reference = [GTReference referenceByLookingUpReferencedNamed:@(ref_name) inRepository:repository error:NULL]; + + GTFetchHeadEntry *entry = [[GTFetchHeadEntry alloc] initWithReference:reference remoteURLString:@(remote_url) targetOID:[GTOID oidWithGitOid:oid] isMerge:(BOOL)is_merge]; + + BOOL stop = NO; + + enumerationBlock(entry, &stop); + + return (stop == YES ? GIT_EUSER : 0); +} + +- (BOOL)enumerateFetchHeadEntriesWithError:(NSError **)error usingBlock:(void (^)(GTFetchHeadEntry *fetchHeadEntry, BOOL *stop))block { + NSParameterAssert(block != nil); + + GTEnumerateHeadEntriesPayload payload = { + .repository = self, + .enumerationBlock = block, + }; + int gitError = git_repository_fetchhead_foreach(self.git_repository, GTFetchHeadEntriesCallback, &payload); + + if (gitError != GIT_OK) { + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to get fetchhead entries"]; + return NO; + } + + return YES; +} + +- (NSArray *)fetchHeadEntriesWithError:(NSError **)error { + NSMutableArray *entries = [NSMutableArray array]; + + [self enumerateFetchHeadEntriesWithError:error usingBlock:^(GTFetchHeadEntry *fetchHeadEntry, BOOL *stop) { + [entries addObject:fetchHeadEntry]; + + *stop = NO; + }]; + + return entries; +} + +@end diff --git a/Classes/GTRepository.h b/Classes/GTRepository.h index bf8941ce2..2888694c1 100644 --- a/Classes/GTRepository.h +++ b/Classes/GTRepository.h @@ -47,6 +47,7 @@ @class GTSubmodule; @class GTTag; @class GTTree; +@class GTRemote; /// Checkout strategies used by the various -checkout... methods /// See git_checkout_strategy_t @@ -215,6 +216,13 @@ extern NSString *const GTRepositoryCloneOptionsCloneLocal; - (NSArray *)remoteBranchesWithError:(NSError **)error; - (NSArray *)branchesWithPrefix:(NSString *)prefix error:(NSError **)error; +/// List all remotes in the repository +/// +/// error - will be filled if an error occurs +/// +/// returns an array of NSStrings holding the names of the remotes, or nil if an error occurred +- (NSArray *)remoteNamesWithError:(NSError **)error; + /// Convenience method to return all tags in the repository - (NSArray *)allTagsWithError:(NSError **)error; diff --git a/Classes/GTRepository.m b/Classes/GTRepository.m index b863efbf6..273cad270 100644 --- a/Classes/GTRepository.m +++ b/Classes/GTRepository.m @@ -183,8 +183,7 @@ static int transferProgressCallback(const git_transfer_progress *progress, void } struct GTClonePayload { - // credProvider must be first for compatibility with GTCredentialAcquireCallbackInfo - __unsafe_unretained GTCredentialProvider *credProvider; + GTCredentialAcquireCallbackInfo credProvider; __unsafe_unretained GTTransferProgressBlock transferProgressBlock; }; @@ -226,12 +225,14 @@ + (id)cloneFromURL:(NSURL *)originURL toWorkingDirectory:(NSURL *)workdirURL opt cloneOptions.checkout_opts = checkoutOptions; } - struct GTClonePayload payload; + GTCredentialProvider *provider = options[GTRepositoryCloneOptionsCredentialProvider]; + struct GTClonePayload payload = { + .credProvider = {provider}, + }; + cloneOptions.remote_callbacks.version = GIT_REMOTE_CALLBACKS_VERSION; - GTCredentialProvider *provider = options[GTRepositoryCloneOptionsCredentialProvider]; if (provider) { - payload.credProvider = provider; cloneOptions.remote_callbacks.credentials = GTCredentialAcquireCallback; } @@ -382,7 +383,7 @@ - (NSArray *)branchesWithPrefix:(NSString *)prefix error:(NSError **)error { GTBranch *branch = [[GTBranch alloc] initWithReference:ref repository:self]; if (branch == nil) continue; - + [branches addObject:branch]; } @@ -413,6 +414,21 @@ - (NSArray *)allBranchesWithError:(NSError **)error { return allBranches; } +- (NSArray *)remoteNamesWithError:(NSError **)error { + git_strarray array; + int gitError = git_remote_list(&array, self.git_repository); + if (gitError < GIT_OK) { + if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to list all remotes."]; + return nil; + } + + NSArray *remoteNames = [NSArray git_arrayWithStrarray:array]; + + git_strarray_free(&array); + + return remoteNames; +} + struct GTRepositoryTagEnumerationInfo { __unsafe_unretained GTRepository *myself; __unsafe_unretained GTRepositoryTagEnumerationBlock block; @@ -595,7 +611,7 @@ - (GTCommit *)mergeBaseBetweenFirstOID:(GTOID *)firstOID secondOID:(GTOID *)seco if (error != NULL) *error = [NSError git_errorFor:errorCode description:@"Failed to find merge base between commits %@ and %@.", firstOID.SHA, secondOID.SHA]; return nil; } - + return [self lookUpObjectByGitOid:&mergeBase objectType:GTObjectTypeCommit error:error]; } @@ -756,57 +772,57 @@ static int checkoutNotifyCallback(git_checkout_notify_t why, const char *path, c - (BOOL)moveHEADToReference:(GTReference *)reference error:(NSError **)error { NSParameterAssert(reference != nil); - + int gitError = git_repository_set_head(self.git_repository, reference.name.UTF8String, [self userSignatureForNow].git_signature, NULL); if (gitError != GIT_OK) { if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to move HEAD to reference %@", reference.name]; } - + return gitError == GIT_OK; } - (BOOL)moveHEADToCommit:(GTCommit *)commit error:(NSError **)error { NSParameterAssert(commit != nil); - + int gitError = git_repository_set_head_detached(self.git_repository, commit.OID.git_oid, [self userSignatureForNow].git_signature, NULL); if (gitError != GIT_OK) { if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to move HEAD to commit %@", commit.SHA]; } - + return gitError == GIT_OK; } - (BOOL)performCheckoutWithStrategy:(GTCheckoutStrategyType)strategy notifyFlags:(GTCheckoutNotifyFlags)notifyFlags error:(NSError **)error progressBlock:(GTCheckoutProgressBlock)progressBlock notifyBlock:(GTCheckoutNotifyBlock)notifyBlock { - + git_checkout_options checkoutOptions = GIT_CHECKOUT_OPTIONS_INIT; - + checkoutOptions.checkout_strategy = strategy; checkoutOptions.progress_cb = checkoutProgressCallback; checkoutOptions.progress_payload = (__bridge void *)progressBlock; - + checkoutOptions.notify_cb = checkoutNotifyCallback; checkoutOptions.notify_flags = notifyFlags; checkoutOptions.notify_payload = (__bridge void *)notifyBlock; - + int gitError = git_checkout_head(self.git_repository, &checkoutOptions); if (gitError < GIT_OK) { if (error != NULL) *error = [NSError git_errorFor:gitError description:@"Failed to checkout tree."]; } - + return gitError == GIT_OK; } - (BOOL)checkoutCommit:(GTCommit *)targetCommit strategy:(GTCheckoutStrategyType)strategy notifyFlags:(GTCheckoutNotifyFlags)notifyFlags error:(NSError **)error progressBlock:(GTCheckoutProgressBlock)progressBlock notifyBlock:(GTCheckoutNotifyBlock)notifyBlock { BOOL success = [self moveHEADToCommit:targetCommit error:error]; if (success == NO) return NO; - + return [self performCheckoutWithStrategy:strategy notifyFlags:notifyFlags error:error progressBlock:progressBlock notifyBlock:notifyBlock]; } - (BOOL)checkoutReference:(GTReference *)targetReference strategy:(GTCheckoutStrategyType)strategy notifyFlags:(GTCheckoutNotifyFlags)notifyFlags error:(NSError **)error progressBlock:(GTCheckoutProgressBlock)progressBlock notifyBlock:(GTCheckoutNotifyBlock)notifyBlock { BOOL success = [self moveHEADToReference:targetReference error:error]; if (success == NO) return NO; - + return [self performCheckoutWithStrategy:strategy notifyFlags:notifyFlags error:error progressBlock:progressBlock notifyBlock:notifyBlock]; } diff --git a/Classes/ObjectiveGit.h b/Classes/ObjectiveGit.h index f2bca21a7..11dbdc1fe 100644 --- a/Classes/ObjectiveGit.h +++ b/Classes/ObjectiveGit.h @@ -30,6 +30,7 @@ #import #import #import +#import #import #import #import @@ -58,6 +59,7 @@ #import #import #import +#import #import #import diff --git a/ObjectiveGitFramework.xcodeproj/project.pbxproj b/ObjectiveGitFramework.xcodeproj/project.pbxproj index f0cb92a50..3344198c2 100644 --- a/ObjectiveGitFramework.xcodeproj/project.pbxproj +++ b/ObjectiveGitFramework.xcodeproj/project.pbxproj @@ -151,8 +151,11 @@ 4D79C0EE17DF9F4D00997DE4 /* GTCredential.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D79C0EC17DF9F4D00997DE4 /* GTCredential.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4D79C0EF17DF9F4D00997DE4 /* GTCredential.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D79C0ED17DF9F4D00997DE4 /* GTCredential.m */; }; 4D79C0F717DFAA7100997DE4 /* GTCredential+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D79C0F617DFAA7100997DE4 /* GTCredential+Private.h */; }; + 4DBA4A3217DA73CE006CD5F5 /* GTRemoteSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DBA4A3117DA73CE006CD5F5 /* GTRemoteSpec.m */; }; 4DE864351794A37E00371A65 /* GTRepository+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DE864341794A37E00371A65 /* GTRepository+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 4DE864361794A37E00371A65 /* GTRepository+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DE864341794A37E00371A65 /* GTRepository+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 4DFFB15B183AA8D600D1565E /* GTRepository+RemoteOperations.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DFFB159183AA8D600D1565E /* GTRepository+RemoteOperations.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4DFFB15C183AA8D600D1565E /* GTRepository+RemoteOperations.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DFFB15A183AA8D600D1565E /* GTRepository+RemoteOperations.m */; }; 55C8054F13861FE7004DCB0F /* GTObjectDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 55C8054D13861F34004DCB0F /* GTObjectDatabase.m */; }; 55C8055013861FE7004DCB0F /* GTObjectDatabase.m in Sources */ = {isa = PBXBuildFile; fileRef = 55C8054D13861F34004DCB0F /* GTObjectDatabase.m */; }; 55C8057A13875578004DCB0F /* NSString+Git.m in Sources */ = {isa = PBXBuildFile; fileRef = 55C8057313874CDF004DCB0F /* NSString+Git.m */; }; @@ -173,6 +176,12 @@ 6A74CA3616A942C000E1A3C5 /* GTConfiguration+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 883CD6AE1600F01000F57354 /* GTConfiguration+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 6A74CA3716A942C800E1A3C5 /* GTRemote.h in Headers */ = {isa = PBXBuildFile; fileRef = 883CD6A91600EBC600F57354 /* GTRemote.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6A74CA3816A9432A00E1A3C5 /* ObjectiveGit.m in Sources */ = {isa = PBXBuildFile; fileRef = 88F05AC51601209A00B7AD1D /* ObjectiveGit.m */; }; + 6E98A3CB199A892C0048E067 /* GTRepository+RemoteOperations.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DFFB15A183AA8D600D1565E /* GTRepository+RemoteOperations.m */; }; + 6E98A3CC199A89300048E067 /* GTRepository+RemoteOperations.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DFFB159183AA8D600D1565E /* GTRepository+RemoteOperations.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6EEB51A1199D62B9001D72C0 /* GTFetchHeadEntry.h in Headers */ = {isa = PBXBuildFile; fileRef = 6EEB519F199D62B9001D72C0 /* GTFetchHeadEntry.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6EEB51A2199D62B9001D72C0 /* GTFetchHeadEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = 6EEB51A0199D62B9001D72C0 /* GTFetchHeadEntry.m */; }; + 6EEB51A3199D62CC001D72C0 /* GTFetchHeadEntry.h in Headers */ = {isa = PBXBuildFile; fileRef = 6EEB519F199D62B9001D72C0 /* GTFetchHeadEntry.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6EEB51A4199D62D3001D72C0 /* GTFetchHeadEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = 6EEB51A0199D62B9001D72C0 /* GTFetchHeadEntry.m */; }; 79262F1013C697C100A4B1EA /* git2 in Copy git2 Headers */ = {isa = PBXBuildFile; fileRef = 79262F0E13C697BE00A4B1EA /* git2 */; }; 79262F8B13C69B1600A4B1EA /* git2.h in Headers */ = {isa = PBXBuildFile; fileRef = 79262F8A13C69B1600A4B1EA /* git2.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8803DA871313145700E6E818 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 8803DA861313145700E6E818 /* libz.dylib */; }; @@ -307,7 +316,6 @@ DD3D9520182AB3C4004AF532 /* GTBlameHunk.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3D951B182AB25C004AF532 /* GTBlameHunk.m */; }; E9FFC6BF1577CC8300A9E736 /* GTConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 88EB7E4C14AEBA600046FEA4 /* GTConfiguration.m */; }; E9FFC6C01577CC8A00A9E736 /* GTConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 88EB7E4B14AEBA600046FEA4 /* GTConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; - F6ED8DA1180E713200A32D40 /* GTRemoteSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = F6ED8DA0180E713200A32D40 /* GTRemoteSpec.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -414,7 +422,10 @@ 4D79C0ED17DF9F4D00997DE4 /* GTCredential.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTCredential.m; sourceTree = ""; }; 4D79C0F617DFAA7100997DE4 /* GTCredential+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTCredential+Private.h"; sourceTree = ""; }; 4D8DADBE181A7D9F001B1202 /* libgit2 */ = {isa = PBXFileReference; lastKnownFileType = folder; name = libgit2; path = External/libgit2; sourceTree = ""; }; + 4DBA4A3117DA73CE006CD5F5 /* GTRemoteSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTRemoteSpec.m; sourceTree = ""; }; 4DE864341794A37E00371A65 /* GTRepository+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTRepository+Private.h"; sourceTree = ""; }; + 4DFFB159183AA8D600D1565E /* GTRepository+RemoteOperations.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTRepository+RemoteOperations.h"; sourceTree = ""; }; + 4DFFB15A183AA8D600D1565E /* GTRepository+RemoteOperations.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTRepository+RemoteOperations.m"; sourceTree = ""; }; 55C8054C13861F34004DCB0F /* GTObjectDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTObjectDatabase.h; sourceTree = ""; }; 55C8054D13861F34004DCB0F /* GTObjectDatabase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = GTObjectDatabase.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 55C8057213874CDF004DCB0F /* NSString+Git.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Git.h"; sourceTree = ""; }; @@ -426,6 +437,8 @@ 6A1F2FD417C6A8F3003DFADE /* libssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libssl.a; path = "External/ios-openssl/lib/libssl.a"; sourceTree = ""; }; 6A3C60A017D5987600382DFF /* update_libssh2_ios */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = update_libssh2_ios; path = script/update_libssh2_ios; sourceTree = SOURCE_ROOT; }; 6A502B8617D6892D00BAF4A5 /* libssh2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libssh2.a; path = "External/libssh2-ios/lib/libssh2.a"; sourceTree = ""; }; + 6EEB519F199D62B9001D72C0 /* GTFetchHeadEntry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTFetchHeadEntry.h; sourceTree = ""; }; + 6EEB51A0199D62B9001D72C0 /* GTFetchHeadEntry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTFetchHeadEntry.m; sourceTree = ""; }; 79262F0E13C697BE00A4B1EA /* git2 */ = {isa = PBXFileReference; lastKnownFileType = folder; name = git2; path = External/libgit2/include/git2; sourceTree = ""; }; 79262F8A13C69B1600A4B1EA /* git2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = git2.h; path = External/libgit2/include/git2.h; sourceTree = ""; }; 8803DA861313145700E6E818 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; @@ -559,7 +572,6 @@ DD3D951B182AB25C004AF532 /* GTBlameHunk.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTBlameHunk.m; sourceTree = ""; }; E46931A7172740D300F2077D /* update_libgit2 */ = {isa = PBXFileReference; lastKnownFileType = text; name = update_libgit2; path = script/update_libgit2; sourceTree = ""; }; E46931A8172740D300F2077D /* update_libgit2_ios */ = {isa = PBXFileReference; lastKnownFileType = text; name = update_libgit2_ios; path = script/update_libgit2_ios; sourceTree = ""; }; - F6ED8DA0180E713200A32D40 /* GTRemoteSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTRemoteSpec.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -731,9 +743,9 @@ 88F05AA816011FFD00B7AD1D /* GTObjectTest.m */, D00F6815175D373C004DB9D6 /* GTReferenceSpec.m */, 88215482171499BE00D76B76 /* GTReflogSpec.m */, + 4DBA4A3117DA73CE006CD5F5 /* GTRemoteSpec.m */, 200578C418932A82001C06C3 /* GTBlameSpec.m */, D0AC906B172F941F00347DC4 /* GTRepositorySpec.m */, - F6ED8DA0180E713200A32D40 /* GTRemoteSpec.m */, 4D12323F178E009E0048F785 /* GTRepositoryCommittingSpec.m */, D03B7C401756AB370034A610 /* GTSubmoduleSpec.m */, 2089E43B17D9A58000F451DA /* GTTagSpec.m */, @@ -790,6 +802,8 @@ D015F7C917F695E800AD5E1F /* GTRepository+Stashing.m */, 88746CC217FA1C950005888A /* GTRepository+Committing.h */, 88746CC317FA1C950005888A /* GTRepository+Committing.m */, + 4DFFB159183AA8D600D1565E /* GTRepository+RemoteOperations.h */, + 4DFFB15A183AA8D600D1565E /* GTRepository+RemoteOperations.m */, BDD8AE6D13131B8800CB5D40 /* GTEnumerator.h */, BDD8AE6E13131B8800CB5D40 /* GTEnumerator.m */, BD6C22A71314625800992935 /* GTObject.h */, @@ -853,6 +867,8 @@ D0CE551F18B6C58F008EB8E0 /* GTFilterList.m */, 88E352FE1982E9160051001F /* GTRepository+Attributes.h */, 88E352FF1982E9160051001F /* GTRepository+Attributes.m */, + 6EEB519F199D62B9001D72C0 /* GTFetchHeadEntry.h */, + 6EEB51A0199D62B9001D72C0 /* GTFetchHeadEntry.m */, ); path = Classes; sourceTree = ""; @@ -970,6 +986,7 @@ 5BE612891745EE3400266D8C /* GTTreeBuilder.h in Headers */, D021DF501806899200934E32 /* NSArray+StringArray.h in Headers */, 04DB4654133AB5E200D9C624 /* GTTreeEntry.h in Headers */, + 6E98A3CC199A89300048E067 /* GTRepository+RemoteOperations.h in Headers */, 04DB4655133AB5E200D9C624 /* GTBlob.h in Headers */, 04DB4656133AB5E200D9C624 /* GTTag.h in Headers */, 04DB4657133AB5E200D9C624 /* GTIndex.h in Headers */, @@ -1003,6 +1020,7 @@ 20702DB118A1F38A009FB457 /* GTBlameHunk.h in Headers */, 6A74CA3616A942C000E1A3C5 /* GTConfiguration+Private.h in Headers */, 30B1E7EF1703522100D0814D /* NSDate+GTTimeAdditions.h in Headers */, + 6EEB51A3199D62CC001D72C0 /* GTFetchHeadEntry.h in Headers */, D09C2E371755F16200065E36 /* GTSubmodule.h in Headers */, D015F7CB17F695E800AD5E1F /* GTRepository+Stashing.h in Headers */, 4DE864361794A37E00371A65 /* GTRepository+Private.h in Headers */, @@ -1026,11 +1044,13 @@ 88BC0E5018EF4F3600C7D0E6 /* GTRepository+Reset.h in Headers */, BDFAF9C3131C1845000508BC /* GTIndex.h in Headers */, BDFAF9C9131C1868000508BC /* GTIndexEntry.h in Headers */, + 6EEB51A1199D62B9001D72C0 /* GTFetchHeadEntry.h in Headers */, BD441E08131ED0C300187010 /* GTReference.h in Headers */, 88F6D9D91320451F00CC0BA8 /* ObjectiveGit.h in Headers */, 88F6D9FA1320467100CC0BA8 /* GTCommit.h in Headers */, 88F6D9FB1320467500CC0BA8 /* GTObject.h in Headers */, AA046112134F4D2000DF526B /* GTOdbObject.h in Headers */, + 4DFFB15B183AA8D600D1565E /* GTRepository+RemoteOperations.h in Headers */, BDB2B1301386F34300C88D55 /* GTObjectDatabase.h in Headers */, 88F6D9FC1320467800CC0BA8 /* GTSignature.h in Headers */, 88F50F59132054D800584FBE /* GTBranch.h in Headers */, @@ -1251,6 +1271,7 @@ 6A74CA3416A942AA00E1A3C5 /* NSData+Git.m in Sources */, 6A74CA3216A9429700E1A3C5 /* GTRemote.m in Sources */, 04DB4660133AB5EB00D9C624 /* NSError+Git.m in Sources */, + 6E98A3CB199A892C0048E067 /* GTRepository+RemoteOperations.m in Sources */, 04DB4661133AB5EB00D9C624 /* GTRepository.m in Sources */, 04DB4664133AB5EB00D9C624 /* GTEnumerator.m in Sources */, 398F8AA7183111260071359D /* GTCredential.m in Sources */, @@ -1278,6 +1299,7 @@ 30A3D6571667F11C00C49A39 /* GTDiff.m in Sources */, 3011D86E1668E48500CE3409 /* GTDiffFile.m in Sources */, 3011D8741668E78500CE3409 /* GTDiffHunk.m in Sources */, + 6EEB51A4199D62D3001D72C0 /* GTFetchHeadEntry.m in Sources */, 3011D87A1668F29600CE3409 /* GTDiffDelta.m in Sources */, DD3D951E182AB3BD004AF532 /* GTBlame.m in Sources */, 880EE66418AE700500B82455 /* GTFilter.m in Sources */, @@ -1336,8 +1358,8 @@ D03B7C411756AB370034A610 /* GTSubmoduleSpec.m in Sources */, D00F6816175D373C004DB9D6 /* GTReferenceSpec.m in Sources */, D040AF70177B9779001AD9EB /* GTOIDSpec.m in Sources */, - F6ED8DA1180E713200A32D40 /* GTRemoteSpec.m in Sources */, D040AF78177B9A9E001AD9EB /* GTSignatureSpec.m in Sources */, + 4DBA4A3217DA73CE006CD5F5 /* GTRemoteSpec.m in Sources */, 4D123240178E009E0048F785 /* GTRepositoryCommittingSpec.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1374,11 +1396,13 @@ 88EB7E4E14AEBA600046FEA4 /* GTConfiguration.m in Sources */, 883CD6AC1600EBC600F57354 /* GTRemote.m in Sources */, 30DCBA7317B4791A009B0EBD /* NSArray+StringArray.m in Sources */, + 4DFFB15C183AA8D600D1565E /* GTRepository+RemoteOperations.m in Sources */, 88F05AC61601209A00B7AD1D /* ObjectiveGit.m in Sources */, 30A3D6561667F11C00C49A39 /* GTDiff.m in Sources */, 3011D86D1668E48500CE3409 /* GTDiffFile.m in Sources */, 3011D8731668E78500CE3409 /* GTDiffHunk.m in Sources */, 3011D8791668F29600CE3409 /* GTDiffDelta.m in Sources */, + 6EEB51A2199D62B9001D72C0 /* GTFetchHeadEntry.m in Sources */, 30FDC08116835A8100654BF0 /* GTDiffLine.m in Sources */, 886E622C18AEBF75000611A0 /* GTFilterSource.m in Sources */, DD3D9513182A81E1004AF532 /* GTBlame.m in Sources */, diff --git a/ObjectiveGitTests/GTIndexSpec.m b/ObjectiveGitTests/GTIndexSpec.m index f9b11f288..d9da6af36 100644 --- a/ObjectiveGitTests/GTIndexSpec.m +++ b/ObjectiveGitTests/GTIndexSpec.m @@ -15,10 +15,9 @@ __block GTIndex *index; beforeEach(^{ - repository = self.bareFixtureRepository; + repository = self.testAppFixtureRepository; - NSURL *indexURL = [repository.gitDirectoryURL URLByAppendingPathComponent:@"index"]; - index = [GTIndex indexWithFileURL:indexURL repository:self.bareFixtureRepository error:NULL]; + index = [repository indexWithError:NULL]; expect(index).notTo.beNil(); BOOL success = [index refresh:NULL]; @@ -26,7 +25,7 @@ }); it(@"should count the entries", ^{ - expect(index.entryCount).to.equal(2); + expect(index.entryCount).to.equal(24); }); it(@"should clear all entries", ^{ @@ -37,16 +36,14 @@ it(@"should read entry properties", ^{ GTIndexEntry *entry = [index entryAtIndex:0]; expect(entry).notTo.beNil(); - expect(entry.path).to.equal(@"README"); + expect(entry.path).to.equal(@".gitignore"); expect(entry.staged).to.beFalsy(); }); it(@"should write to the repository and return a tree", ^{ - GTRepository *repository = self.bareFixtureRepository; - GTIndex *index = [repository indexWithError:NULL]; GTTree *tree = [index writeTree:NULL]; expect(tree).notTo.beNil(); - expect(tree.entryCount).to.equal(2); + expect(tree.entryCount).to.equal(23); expect(tree.repository).to.equal(repository); }); @@ -55,13 +52,13 @@ NSArray *branches = [repository allBranchesWithError:NULL]; GTCommit *masterCommit = [branches[0] targetCommitAndReturnError:NULL]; GTCommit *packedCommit = [branches[1] targetCommitAndReturnError:NULL]; - + expect(masterCommit).notTo.beNil(); expect(packedCommit).notTo.beNil(); - + GTIndex *index = [masterCommit.tree merge:packedCommit.tree ancestor:NULL error:NULL]; GTTree *mergedTree = [index writeTreeToRepository:repository error:NULL]; - + expect(index).notTo.beNil(); expect(mergedTree).notTo.beNil(); expect(mergedTree.entryCount).to.equal(5); @@ -104,7 +101,7 @@ it(@"should correctly find no conflicts", ^{ expect(index.hasConflicts).to.beFalsy(); }); - + it(@"should immediately return YES when enumerating no conflicts", ^{ __block BOOL blockRan = NO; BOOL enumerationResult = [index enumerateConflictedFilesWithError:NULL usingBlock:^(GTIndexEntry *ancestor, GTIndexEntry *ours, GTIndexEntry *theirs, BOOL *stop) { @@ -113,17 +110,17 @@ expect(enumerationResult).to.beTruthy(); expect(blockRan).to.beFalsy(); }); - + it(@"should correctly report conflicts", ^{ index = [self.conflictedFixtureRepository indexWithError:NULL]; expect(index).notTo.beNil(); expect(index.hasConflicts).to.beTruthy(); }); - + it(@"should enumerate conflicts successfully", ^{ index = [self.conflictedFixtureRepository indexWithError:NULL]; expect(index).notTo.beNil(); - + NSError *err = NULL; __block NSUInteger count = 0; NSArray *expectedPaths = @[ @"TestAppDelegate.h", @"main.m" ]; @@ -131,7 +128,7 @@ expect(ours.path).to.equal(expectedPaths[count]); count ++; }]; - + expect(enumerationResult).to.beTruthy(); expect(err).to.beNil(); expect(count).to.equal(2); @@ -148,18 +145,18 @@ expect(index).toNot.beNil(); expect([index.repository statusForFile:fileName success:NULL error:NULL]).to.equal(GTFileStatusModifiedInWorktree); }); - + it(@"should update the Index", ^{ BOOL success = [index updatePathspecs:@[ fileName ] error:NULL passingTest:^(NSString *matchedPathspec, NSString *path, BOOL *stop) { expect(matchedPathspec).to.equal(fileName); expect(path).to.equal(fileName); return YES; }]; - + expect(success).to.beTruthy(); expect([index.repository statusForFile:fileName success:NULL error:NULL]).to.equal(GTFileStatusModifiedInIndex); }); - + it(@"should skip a specific file", ^{ BOOL success = [index updatePathspecs:NULL error:NULL passingTest:^(NSString *matchedPathspec, NSString *path, BOOL *stop) { if ([path.lastPathComponent isEqualToString:fileName]) { @@ -168,11 +165,11 @@ return YES; } }]; - + expect(success).to.beTruthy(); expect([index.repository statusForFile:fileName success:NULL error:NULL]).to.equal(GTFileStatusModifiedInWorktree); }); - + it(@"should stop be able to stop early", ^{ NSString *otherFileName = @"TestAppDelegate.h"; [@"WELP" writeToFile:[self.testAppFixtureRepository.fileURL.path stringByAppendingPathComponent:otherFileName] atomically:NO encoding:NSUTF8StringEncoding error:NULL]; @@ -183,7 +180,7 @@ } return YES; }]; - + expect(success).to.beTruthy(); expect([index.repository statusForFile:fileName success:NULL error:NULL]).to.equal(GTFileStatusModifiedInIndex); expect([index.repository statusForFile:otherFileName success:NULL error:NULL]).equal(GTFileStatusModifiedInWorktree); diff --git a/ObjectiveGitTests/GTRemoteSpec.m b/ObjectiveGitTests/GTRemoteSpec.m index 47bd2b45b..8e271a004 100644 --- a/ObjectiveGitTests/GTRemoteSpec.m +++ b/ObjectiveGitTests/GTRemoteSpec.m @@ -56,7 +56,7 @@ expect(remote.fetchRefspecs).to.equal(@[ fetchRefspec ]); NSString *newFetchRefspec = @"+refs/heads/master:refs/remotes/origin/master"; - + __block NSError *error = nil; expect([remote addFetchRefspec:newFetchRefspec error:&error]).to.beTruthy(); expect(error).to.beNil(); @@ -65,8 +65,136 @@ }); }); -afterEach(^{ - [self tearDown]; +describe(@"network operations", ^{ + __block NSURL *repositoryURL; + __block NSURL *fetchingRepoURL; + __block GTRepository *fetchingRepo; + __block NSArray *remoteNames; + __block NSString *remoteName; + + beforeEach(^{ + repository = self.bareFixtureRepository; + expect(repository.isBare).to.beFalsy(); // yeah right + repositoryURL = repository.gitDirectoryURL; + NSURL *fixturesURL = repositoryURL.URLByDeletingLastPathComponent; + fetchingRepoURL = [fixturesURL URLByAppendingPathComponent:@"fetchrepo"]; + + NSError *error = nil; + fetchingRepo = [GTRepository cloneFromURL:repositoryURL toWorkingDirectory:fetchingRepoURL options:nil error:&error transferProgressBlock:nil checkoutProgressBlock:nil]; + expect(fetchingRepo).notTo.beNil(); + expect(error).to.beNil(); + + remoteNames = [fetchingRepo remoteNamesWithError:&error]; + expect(error).to.beNil(); + expect(remoteNames.count).to.beGreaterThanOrEqualTo(1); + + remoteName = remoteNames[0]; + }); + + afterEach(^{ + [NSFileManager.defaultManager removeItemAtURL:fetchingRepoURL error:NULL]; + }); + + describe(@"-remoteWithName:inRepository:error", ^{ + it(@"should return existing remotes", ^{ + NSError *error = nil; + + GTRemote *originRemote = [GTRemote remoteWithName:remoteName inRepository:fetchingRepo error:&error]; + expect(error).to.beNil(); + expect(originRemote).notTo.beNil(); + expect(originRemote.name).to.equal(@"origin"); + expect(originRemote.URLString).to.equal(repositoryURL.path); + }); + + it(@"should fail for non-existent remotes", ^{ + NSError *error = nil; + + GTRemote *originRemote = [GTRemote remoteWithName:@"blork" inRepository:fetchingRepo error:&error]; + expect(error).notTo.beNil(); + expect(originRemote).to.beNil(); + }); + }); + + describe(@"-createRemoteWithName:url:inRepository:error", ^{ + it(@"should allow creating new remotes", ^{ + NSError *error = nil; + GTRemote *remote = [GTRemote createRemoteWithName:@"newremote" URLString:@"git://user@example.com/testrepo.git" inRepository:fetchingRepo error:&error]; + expect(error).to.beNil(); + expect(remote).notTo.beNil(); + + GTRemote *newRemote = [GTRemote remoteWithName:@"newremote" inRepository:fetchingRepo error:&error]; + expect(error).to.beNil(); + expect(newRemote).notTo.beNil(); + expect(newRemote.URLString).to.equal(@"git://user@example.com/testrepo.git"); + }); + }); + + // Helper to quickly create commits + GTCommit *(^createCommitInRepository)(NSString *, NSData *, NSString *, GTRepository *) = ^(NSString *message, NSData *fileData, NSString *fileName, GTRepository *repo) { + GTTreeBuilder *treeBuilder = [[GTTreeBuilder alloc] initWithTree:nil error:nil]; + [treeBuilder addEntryWithData:fileData fileName:fileName fileMode:GTFileModeBlob error:nil]; + + GTTree *testTree = [treeBuilder writeTreeToRepository:repo error: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; + }; + + describe(@"-[GTRepository fetchRemote:withOptions:error:progress:]", ^{ + it(@"allows remotes to be fetched", ^{ + NSError *error = nil; + GTRemote *remote = [GTRemote remoteWithName:remoteName inRepository:fetchingRepo error:nil]; // Tested above + + BOOL result = [fetchingRepo fetchRemote:remote withOptions:nil error:&error progress:nil]; + expect(error).to.beNil(); + expect(result).to.beTruthy(); + }); + + it(@"brings in new commits", ^{ + NSError *error = nil; + + // Create a new commit in the master repo + NSString *testData = @"Test"; + NSString *fileName = @"test.txt"; + + GTCommit *testCommit = createCommitInRepository(@"Test commit", [testData dataUsingEncoding:NSUTF8StringEncoding], fileName, repository); + + // Now issue a fetch from the fetching repo + GTRemote *remote = [GTRemote remoteWithName:remoteName inRepository:fetchingRepo error:nil]; + + __block unsigned int receivedObjects = 0; + __block BOOL transferProgressed = NO; + BOOL success = [fetchingRepo fetchRemote:remote withOptions:nil error:&error progress:^(const git_transfer_progress *stats, BOOL *stop) { + receivedObjects += stats->received_objects; + transferProgressed = YES; + }]; + expect(error).to.beNil(); + expect(success).to.beTruthy(); + expect(transferProgressed).to.beTruthy(); + expect(receivedObjects).to.equal(10); + + GTCommit *fetchedCommit = [fetchingRepo lookUpObjectByOID:testCommit.OID objectType:GTObjectTypeCommit error:&error]; + expect(error).to.beNil(); + expect(fetchedCommit).notTo.beNil(); + + GTTreeEntry *entry = [[fetchedCommit 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); + }); + }); }); SpecEnd diff --git a/ObjectiveGitTests/GTRepositorySpec.m b/ObjectiveGitTests/GTRepositorySpec.m index 8fd21b5cf..c5cffcacf 100644 --- a/ObjectiveGitTests/GTRepositorySpec.m +++ b/ObjectiveGitTests/GTRepositorySpec.m @@ -8,6 +8,7 @@ #import "GTRepository.h" #import "GTRepository+Committing.h" +#import "SPTExample.h" SpecBegin(GTRepository) @@ -115,18 +116,57 @@ it(@"should have set a valid remote URL", ^{ NSError *error = nil; - GTRepository *repo = [GTRepository cloneFromURL:originURL toWorkingDirectory:workdirURL options:nil error:&error transferProgressBlock:transferProgressBlock checkoutProgressBlock:checkoutProgressBlock]; - expect(repo).notTo.beNil(); + repository = [GTRepository cloneFromURL:originURL toWorkingDirectory:workdirURL options:nil error:&error transferProgressBlock:transferProgressBlock checkoutProgressBlock:checkoutProgressBlock]; + expect(repository).notTo.beNil(); expect(error).to.beNil(); - // FIXME: Move that to a method in GTRepository ? - // Or use the new initializers in GTRemote that are waiting in #224 - git_remote *remote; - git_remote_load(&remote, repo.git_repository, "origin"); - GTRemote *originRemote = [[GTRemote alloc] initWithGitRemote:remote]; + GTRemote *originRemote = [GTRemote remoteWithName:@"origin" inRepository:repository error:&error]; + expect(error).to.beNil(); expect(originRemote.URLString).to.equal(originURL.path); }); }); + + describe(@"with remote repositories", ^{ + __block GTCredentialProvider *provider = nil; + NSString *userName = [[NSProcessInfo processInfo] environment][@"GTUserName"]; + NSString *publicKeyPath = [[[NSProcessInfo processInfo] environment][@"GTPublicKey"] stringByStandardizingPath]; + NSString *privateKeyPath = [[[NSProcessInfo processInfo] environment][@"GTPrivateKey"] stringByStandardizingPath]; + NSString *privateKeyPassword = [[NSProcessInfo processInfo] environment][@"GTPrivateKeyPassword"]; + + beforeEach(^{ + // Let's clone libgit2's documentation + originURL = [NSURL URLWithString:@"git@github.com:libgit2/libgit2.github.com.git"]; + }); + + if (!userName || !publicKeyPath || !privateKeyPath || !privateKeyPassword) { + pending(@"should handle normal clones (pending environment)"); + } else { + it(@"should handle clones", ^{ + __block NSError *error = nil; + + provider = [GTCredentialProvider providerWithBlock:^GTCredential *(GTCredentialType type, NSString *URL, NSString *credUserName) { + expect(URL).to.equal(originURL.absoluteString); + expect(type & GTCredentialTypeSSHKey).to.beTruthy(); + GTCredential *cred = nil; + // cred = [GTCredential credentialWithUserName:userName password:password error:&error]; + cred = [GTCredential credentialWithUserName:credUserName publicKeyURL:[NSURL fileURLWithPath:publicKeyPath] privateKeyURL:[NSURL fileURLWithPath:privateKeyPath] passphrase:privateKeyPassword error:&error]; + expect(cred).notTo.beNil(); + expect(error).to.beNil(); + return cred; + }]; + + repository = [GTRepository cloneFromURL:originURL toWorkingDirectory:workdirURL options:@{GTRepositoryCloneOptionsCredentialProvider: provider} error:&error transferProgressBlock:transferProgressBlock checkoutProgressBlock:checkoutProgressBlock]; + expect(repository).notTo.beNil(); + expect(error).to.beNil(); + expect(transferProgressCalled).to.beTruthy(); + expect(checkoutProgressCalled).to.beTruthy(); + + GTRemote *originRemote = [GTRemote remoteWithName:@"origin" inRepository:repository error:&error]; + expect(error).to.beNil(); + expect(originRemote.URLString).to.equal(originURL.absoluteString); + }); + } + }); }); describe(@"-headReferenceWithError:", ^{ @@ -327,6 +367,59 @@ }); }); +describe(@"-remoteNamesWithError:", ^{ + it(@"allows access to remote names", ^{ + NSError *error = nil; + NSArray *remoteNames = [repository remoteNamesWithError:&error]; + expect(error.localizedDescription).to.beNil(); + expect(remoteNames).notTo.beNil(); + }); + + it(@"returns remote names if there are any", ^{ + NSError *error = nil; + NSString *remoteName = @"testremote"; + GTRemote *remote = [GTRemote createRemoteWithName:remoteName URLString:@"git://user@example.com/testrepo" inRepository:repository error:&error]; + expect(error.localizedDescription).to.beNil(); + expect(remote).notTo.beNil(); + + NSArray *remoteNames = [repository remoteNamesWithError:&error]; + expect(error.localizedDescription).to.beNil(); + expect(remoteNames).to.contain(remoteName); + }); +}); + +describe(@"-resetToCommit:withResetType:error:", ^{ + beforeEach(^{ + repository = self.bareFixtureRepository; + }); + + it(@"should move HEAD when used", ^{ + NSError *error = nil; + GTReference *originalHead = [repository headReferenceWithError:NULL]; + NSString *resetTargetSHA = @"8496071c1b46c854b31185ea97743be6a8774479"; + + GTCommit *commit = [repository lookUpObjectBySHA:resetTargetSHA error:NULL]; + expect(commit).notTo.beNil(); + GTCommit *originalHeadCommit = [repository lookUpObjectBySHA:originalHead.targetSHA error:NULL]; + expect(originalHeadCommit).notTo.beNil(); + + BOOL success = [repository resetToCommit:commit resetType:GTRepositoryResetTypeSoft error:&error]; + expect(success).to.beTruthy(); + expect(error).to.beNil(); + + GTReference *head = [repository headReferenceWithError:&error]; + expect(head).notTo.beNil(); + expect(head.targetSHA).to.equal(resetTargetSHA); + + success = [repository resetToCommit:originalHeadCommit resetType:GTRepositoryResetTypeSoft error:&error]; + expect(success).to.beTruthy(); + expect(error).to.beNil(); + + head = [repository headReferenceWithError:&error]; + expect(head.targetSHA).to.equal(originalHead.targetSHA); + }); +}); + describe(@"-lookUpBranchWithName:type:error:", ^{ it(@"should look up a local branch", ^{ NSError *error = nil;