diff --git a/Classes/GTTree.h b/Classes/GTTree.h index 1ae19ddf6..3dd872322 100644 --- a/Classes/GTTree.h +++ b/Classes/GTTree.h @@ -33,6 +33,11 @@ @class GTTreeEntry; @class GTIndex; +typedef enum GTTreeEnumerationOptions { + GTTreeEnumerationOptionPre = GIT_TREEWALK_PRE, // Walk the tree in pre-order (subdirectories come first) + GTTreeEnumerationOptionPost = GIT_TREEWALK_POST, // Walk the tree in post-order (subdirectories come last) +} GTTreeEnumerationOptions; + @interface GTTree : GTObject // The number of entries in the tree. @@ -55,6 +60,22 @@ // returns a GTTreeEntry or nil if there is nothing with the specified name - (GTTreeEntry *)entryWithName:(NSString *)name; +// Enumerates the contents of the tree +// +// options - One of `GTTreeEnumerationOptionPre` (for pre-order walks) or +// `GTTreeEnumerationOptionPost` (for post-order walks). +// error - The error if one occurred. +// block - A block that will be called back with a path to the root of the tree +// and the current entry. Cannot be nil. +// Return a negative value to stop the walk, a positive one to skip the +// descendents of the entry, or 0 to continue the enumeration. +// +// returns YES if the enumeration completed successfully, NO otherwise +- (BOOL)enumerateContentsWithOptions:(GTTreeEnumerationOptions)options error:(NSError **)error block:(int(^)(NSString *root, GTTreeEntry *entry))block; + +// Returns the contents of the tree, as an array of GTTreeEntries +- (NSArray *)contents; + // Merges the given tree into the receiver in memory and produces the result as // an index. // diff --git a/Classes/GTTree.m b/Classes/GTTree.m index ba495e700..d7f5d5d08 100644 --- a/Classes/GTTree.m +++ b/Classes/GTTree.m @@ -32,6 +32,15 @@ #import "GTRepository.h" #import "GTIndex.h" #import "NSError+Git.h" +#import "GTTreeEntry+Private.h" + +typedef int(^GTTreeEnumerationBlock)(NSString *root, GTTreeEntry *entry); + +typedef struct GTTreeEnumerationStruct { + __unsafe_unretained GTTree *myself; + __unsafe_unretained GTTreeEnumerationBlock block; + __unsafe_unretained NSMutableDictionary *directoryStructure; +} GTTreeEnumerationStruct; @implementation GTTree @@ -61,6 +70,55 @@ - (git_tree *)git_tree { return (git_tree *)self.git_object; } +#pragma mark Contents + +static int treewalk_cb(const char *root, const git_tree_entry *git_entry, void *payload) { + GTTreeEnumerationStruct *enumStruct = (GTTreeEnumerationStruct *)payload; + NSString *rootString = @(root); + GTTreeEntry *parentEntry = enumStruct->directoryStructure[rootString]; + GTTree *parentTree = parentEntry ? parentEntry.tree : enumStruct->myself; + + GTTreeEntry *entry = [[GTTreeEntry alloc] initWithEntry:git_entry parentTree:parentTree]; + if ([entry type] == GTObjectTypeTree) { + NSString *path = [rootString stringByAppendingPathComponent:entry.name]; + enumStruct->directoryStructure[path] = entry; + } + return enumStruct->block(rootString, entry); +} + + +- (BOOL)enumerateContentsWithOptions:(GTTreeEnumerationOptions)option error:(NSError **)error block:(GTTreeEnumerationBlock)block { + NSParameterAssert(block != nil); + + NSMutableDictionary *structure = [[NSMutableDictionary alloc] initWithCapacity:[self entryCount]]; + + GTTreeEnumerationStruct enumStruct = { + .myself = self, + .block = block, + .directoryStructure = structure, + }; + + int gitError = git_tree_walk(self.git_tree, (git_treewalk_mode)option, treewalk_cb, &enumStruct); + if (gitError != GIT_OK) { + if (error != NULL) *error = [NSError git_errorFor:gitError withAdditionalDescription:@"Failed to enumerate tree"]; + } + return gitError != GIT_OK; +} + +- (NSArray *)contents { + NSError *error = nil; + __block NSMutableArray *_contents = [NSMutableArray array]; + int gitError = [self enumerateContentsWithOptions:GTTreeEnumerationOptionPre error:&error block:^int(NSString *root, GTTreeEntry *entry) { + [_contents addObject:entry]; + return [entry type] == GTObjectTypeTree ? 1 : 0; + }]; + if (gitError < GIT_OK) { + NSLog(@"%@", error); + return nil; + } + return _contents; +} + #pragma mark Merging - (GTIndex *)merge:(GTTree *)otherTree ancestor:(GTTree *)ancestorTree error:(NSError **)error { diff --git a/Classes/GTTreeEntry+Private.h b/Classes/GTTreeEntry+Private.h new file mode 100644 index 000000000..578dfa554 --- /dev/null +++ b/Classes/GTTreeEntry+Private.h @@ -0,0 +1,13 @@ +// +// GTTreeEntry+Private.h +// ObjectiveGitFramework +// +// Created by Etienne on 10/07/13. +// Copyright (c) 2013 GitHub, Inc. All rights reserved. +// + +#import + +@interface GTTreeEntry () +- (GTObjectType)type; +@end diff --git a/Classes/GTTreeEntry.m b/Classes/GTTreeEntry.m index 1af0be721..cb01a9230 100644 --- a/Classes/GTTreeEntry.m +++ b/Classes/GTTreeEntry.m @@ -40,8 +40,25 @@ @interface GTTreeEntry () @implementation GTTreeEntry +#pragma mark NSObject + - (NSString *)description { - return [NSString stringWithFormat:@"<%@: %p> name: %@, sha: %@ attributes: %lu", NSStringFromClass([self class]), self, [self name], [self sha], (unsigned long)[self attributes]]; + return [NSString stringWithFormat:@"<%@: %p> name: %@, type: %@, sha: %@, attributes: %lu", NSStringFromClass(self.class), self, self.name, self.typeString, self.sha, (unsigned long)self.attributes]; +} + +- (NSUInteger)hash { + return [self.sha hash]; +} + +- (BOOL)isEqual:(id)object { + if ([object isKindOfClass:[self class]]) { + return [self isEqualToEntry:object]; + } + return [super isEqual:object]; +} + +- (BOOL)isEqualToEntry:(GTTreeEntry *)treeEntry { + return git_tree_entry_cmp(self.git_tree_entry, treeEntry.git_tree_entry) == 0 ? YES : NO; } #pragma mark API @@ -70,6 +87,14 @@ - (NSString *)sha { return [NSString git_stringWithOid:git_tree_entry_id(self.git_tree_entry)]; } +- (GTObjectType)type { + return (GTObjectType)git_tree_entry_type(self.git_tree_entry); +} + +- (NSString *)typeString { + return @(git_object_type2string(git_tree_entry_type(self.git_tree_entry))); +} + - (GTRepository *)repository { return self.tree.repository; } diff --git a/ObjectiveGitFramework.xcodeproj/project.pbxproj b/ObjectiveGitFramework.xcodeproj/project.pbxproj index 7bda5de99..58bcb0c78 100644 --- a/ObjectiveGitFramework.xcodeproj/project.pbxproj +++ b/ObjectiveGitFramework.xcodeproj/project.pbxproj @@ -89,6 +89,7 @@ 30FDC08116835A8100654BF0 /* GTDiffLine.m in Sources */ = {isa = PBXBuildFile; fileRef = 30FDC07E16835A8100654BF0 /* GTDiffLine.m */; }; 30FDC08216835A8100654BF0 /* GTDiffLine.m in Sources */ = {isa = PBXBuildFile; fileRef = 30FDC07E16835A8100654BF0 /* GTDiffLine.m */; }; 3E0A23E5159E0FDB00A6068F /* GTObjectDatabase.h in Headers */ = {isa = PBXBuildFile; fileRef = 55C8054C13861F34004DCB0F /* GTObjectDatabase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4D26799F178DAF31002A2795 /* GTTreeEntry+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D26799D178DAF31002A2795 /* GTTreeEntry+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 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 */; }; @@ -332,6 +333,7 @@ 30FDC07D16835A8100654BF0 /* GTDiffLine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTDiffLine.h; sourceTree = ""; }; 30FDC07E16835A8100654BF0 /* GTDiffLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTDiffLine.m; sourceTree = ""; }; 32DBCF5E0370ADEE00C91783 /* ObjectiveGitFramework_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjectiveGitFramework_Prefix.pch; sourceTree = ""; }; + 4D26799D178DAF31002A2795 /* GTTreeEntry+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTTreeEntry+Private.h"; 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 = ""; }; @@ -675,6 +677,7 @@ BD6B040F131496B8001909D0 /* GTTree.h */, BD6B0410131496B8001909D0 /* GTTree.m */, BD6B0415131496CC001909D0 /* GTTreeEntry.h */, + 4D26799D178DAF31002A2795 /* GTTreeEntry+Private.h */, BD6B0416131496CC001909D0 /* GTTreeEntry.m */, 5BE612861745EE3300266D8C /* GTTreeBuilder.h */, 5BE612871745EE3300266D8C /* GTTreeBuilder.m */, @@ -839,6 +842,7 @@ 6A74CA3616A942C000E1A3C5 /* GTConfiguration+Private.h in Headers */, 30B1E7EF1703522100D0814D /* NSDate+GTTimeAdditions.h in Headers */, D09C2E371755F16200065E36 /* GTSubmodule.h in Headers */, + 4D2679A0178DAF31002A2795 /* GTTreeEntry+Private.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -879,6 +883,7 @@ 8821547617147A5200D76B76 /* GTReflogEntry.h in Headers */, 30B1E7EE1703522100D0814D /* NSDate+GTTimeAdditions.h in Headers */, D09C2E361755F16200065E36 /* GTSubmodule.h in Headers */, + 4D26799F178DAF31002A2795 /* GTTreeEntry+Private.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ObjectiveGitTests/GTTreeSpec.m b/ObjectiveGitTests/GTTreeSpec.m index 1c95a370f..7aac3f4c4 100644 --- a/ObjectiveGitTests/GTTreeSpec.m +++ b/ObjectiveGitTests/GTTreeSpec.m @@ -35,4 +35,19 @@ expect(entry.sha).to.equal(@"1385f264afb75a56a5bec74243be9b367ba4ca08"); }); +it(@"should give quick access to its contents", ^{ + NSArray *treeContents = tree.contents; + expect(treeContents).notTo.beNil(); + expect(treeContents.count).to.equal(3); + GTTreeEntry *readme = [tree entryWithName:@"README"]; + GTTreeEntry *newTxt = [tree entryWithName:@"new.txt"]; + GTTreeEntry *subdir = [tree entryWithName:@"subdir"]; + expect(readme).notTo.beNil(); + expect(newTxt).notTo.beNil(); + expect(subdir).notTo.beNil(); + expect(treeContents).to.contain(readme); + expect(treeContents).to.contain(newTxt); + expect(treeContents).to.contain(subdir); +}); + SpecEnd