// // PBGitHistoryList.m // GitX // // Created by Nathan Kinsinger on 2/20/10. // Copyright 2010 Nathan Kinsinger. All rights reserved. // #import "PBGitHistoryList.h" #import "PBGitRepository.h" #import "PBGitRevList.h" #import "PBGitGrapher.h" #import "PBGitHistoryGrapher.h" @interface PBGitHistoryList () - (void) resetGraphing; - (PBGitHistoryGrapher *) grapher; - (NSInvocationOperation *) operationForCommits:(NSArray *)newCommits; - (void) updateProjectHistoryForRev:(PBGitRevSpecifier *)rev; - (void) updateHistoryForRev:(PBGitRevSpecifier *)rev; @end @implementation PBGitHistoryList @synthesize projectRevList; @synthesize commits; @synthesize isUpdating; @synthesize updatedGraph; @dynamic projectCommits; #pragma mark - #pragma mark Public - (id) initWithRepository:(PBGitRepository *)repo { commits = [NSMutableArray array]; repository = repo; lastBranchFilter = -1; [repository addObserver:self forKeyPath:@"currentBranch" options:0 context:@"currentBranch"]; [repository addObserver:self forKeyPath:@"currentBranchFilter" options:0 context:@"currentBranch"]; [repository addObserver:self forKeyPath:@"hasChanged" options:0 context:@"repositoryHasChanged"]; shouldReloadProjectHistory = YES; projectRevList = [[PBGitRevList alloc] initWithRepository:repository rev:[PBGitRevSpecifier allBranchesRevSpec] shouldGraph:NO]; return self; } - (void) forceUpdate { if ([repository.currentBranch isSimpleRef]) shouldReloadProjectHistory = YES; [self updateHistory]; } - (void) updateHistory { PBGitRevSpecifier *rev = repository.currentBranch; if (!rev) return; if ([rev isSimpleRef]) [self updateProjectHistoryForRev:rev]; else [self updateHistoryForRev:rev]; } - (NSArray *) projectCommits { return [projectRevList.commits copy]; } #pragma mark - #pragma mark History Grapher delegate methods - (void) addCommitsFromArray:(NSArray *)array { if (!array || [array count] == 0) return; if (resetCommits) { self.commits = [NSMutableArray array]; resetCommits = NO; } NSRange range = NSMakeRange([commits count], [array count]); NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:range]; [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"commits"]; [commits addObjectsFromArray:array]; [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"commits"]; } - (void) finishedGraphing { if (!currentRevList.isParsing && ([[graphQueue operations] count] == 0)) { self.isUpdating = NO; [self performSelector:@selector(setUpdatedGraph:) withObject:[NSDate date] afterDelay:0]; } } #pragma mark - #pragma mark Private - (void) resetGraphing { resetCommits = YES; self.isUpdating = YES; [graphQueue setSuspended:YES]; if (graphQueue) [graphQueue removeObserver:self forKeyPath:@"operations"]; graphQueue = [[NSOperationQueue alloc] init]; [graphQueue addObserver:self forKeyPath:@"operations" options:0 context:@"operations"]; lastOperation = nil; grapher = [self grapher]; } - (NSInvocationOperation *) operationForCommits:(NSArray *)newCommits { NSInvocationOperation *graphOperation = [[NSInvocationOperation alloc] initWithTarget:grapher selector:@selector(graphCommits:) object:newCommits]; if (lastOperation) [graphOperation addDependency:lastOperation]; lastOperation = graphOperation; return graphOperation; } - (NSSet *) baseCommitsForLocalRefs { NSMutableSet *baseCommitSHAs = [NSMutableSet set]; NSDictionary *refs = repository.refs; for (NSString *sha in refs) for (PBGitRef *ref in [refs objectForKey:sha]) if ([ref isBranch] || [ref isTag]) [baseCommitSHAs addObject:sha]; return baseCommitSHAs; } - (NSSet *) baseCommitsForRemoteRefs { NSMutableSet *baseCommitSHAs = [NSMutableSet set]; NSDictionary *refs = repository.refs; PBGitRef *remoteRef = [[repository.currentBranch ref] remoteRef]; for (NSString *sha in refs) for (PBGitRef *ref in [refs objectForKey:sha]) if ([remoteRef isEqualToRef:[ref remoteRef]]) [baseCommitSHAs addObject:sha]; return baseCommitSHAs; } - (NSSet *) baseCommits { if ((repository.currentBranchFilter == kGitXSelectedBranchFilter) || (repository.currentBranchFilter == kGitXAllBranchesFilter)) { if (lastSHA) return [NSMutableSet setWithObject:lastSHA]; else if ([repository.currentBranch isSimpleRef]) { PBGitRef *currentRef = [repository.currentBranch ref]; NSString *sha = [repository shaForRef:currentRef]; if (sha) return [NSMutableSet setWithObject:sha]; } } else if (repository.currentBranchFilter == kGitXLocalRemoteBranchesFilter) { if ([[repository.currentBranch ref] isRemote]) return [self baseCommitsForRemoteRefs]; else return [self baseCommitsForLocalRefs]; } return [NSMutableSet set]; } - (PBGitHistoryGrapher *) grapher { BOOL viewAllBranches = (repository.currentBranchFilter == kGitXAllBranchesFilter); return [[PBGitHistoryGrapher alloc] initWithBaseCommits:[self baseCommits] viewAllBranches:viewAllBranches delegate:self]; } - (void) setCurrentRevList:(PBGitRevList *)parser { if (currentRevList == parser) return; if (currentRevList) { [currentRevList removeObserver:self forKeyPath:@"commits"]; [currentRevList removeObserver:self forKeyPath:@"isParsing"]; } currentRevList = parser; [currentRevList addObserver:self forKeyPath:@"commits" options:NSKeyValueObservingOptionNew context:@"commitsUpdated"]; [currentRevList addObserver:self forKeyPath:@"isParsing" options:0 context:@"revListParsing"]; } - (BOOL) isAllBranchesOnlyUpdate { return (lastBranchFilter == kGitXAllBranchesFilter) && (repository.currentBranchFilter == kGitXAllBranchesFilter); } - (BOOL) isLocalRemoteOnlyUpdate:(PBGitRevSpecifier *)rev { if ((lastBranchFilter == kGitXLocalRemoteBranchesFilter) && (repository.currentBranchFilter == kGitXLocalRemoteBranchesFilter)) { if (!lastRemoteRef && ![[rev ref] isRemote]) return YES; if ([lastRemoteRef isEqualToRef:[[rev ref] remoteRef]]) return YES; } return NO; } - (BOOL) selectedBranchNeedsNewGraph:(PBGitRevSpecifier *)rev { if (![rev isSimpleRef]) return YES; if ([self isAllBranchesOnlyUpdate] || [self isLocalRemoteOnlyUpdate:rev]) { lastRemoteRef = [[rev ref] remoteRef]; lastSHA = nil; self.isUpdating = NO; self.updatedGraph = [NSDate date]; return NO; } NSString *revSHA = [repository shaForRef:[rev ref]]; if ([revSHA isEqualToString:lastSHA] && (lastBranchFilter == repository.currentBranchFilter)) return NO; lastBranchFilter = repository.currentBranchFilter; lastRemoteRef = [[rev ref] remoteRef]; lastSHA = revSHA; return YES; } - (BOOL) haveRefsBeenModified { [repository reloadRefs]; NSMutableSet *currentRefSHAs = [NSMutableSet setWithArray:[repository.refs allKeys]]; [currentRefSHAs minusSet:lastRefSHAs]; lastRefSHAs = [NSSet setWithArray:[repository.refs allKeys]]; return [currentRefSHAs count] != 0; } #pragma mark updating history - (void) updateProjectHistoryForRev:(PBGitRevSpecifier *)rev { [self setCurrentRevList:projectRevList]; if ([self haveRefsBeenModified]) shouldReloadProjectHistory = YES; if (![self selectedBranchNeedsNewGraph:rev] && !shouldReloadProjectHistory) return; [self resetGraphing]; if (shouldReloadProjectHistory) { shouldReloadProjectHistory = NO; lastBranchFilter = -1; lastRemoteRef = nil; lastSHA = nil; [projectRevList loadRevisons]; return; } [graphQueue addOperation:[self operationForCommits:projectRevList.commits]]; } - (void) updateHistoryForRev:(PBGitRevSpecifier *)rev { PBGitRevList *otherRevListParser = [[PBGitRevList alloc] initWithRepository:repository rev:rev shouldGraph:YES]; [self setCurrentRevList:otherRevListParser]; [self resetGraphing]; lastBranchFilter = -1; lastRemoteRef = nil; lastSHA = nil; [otherRevListParser loadRevisons]; } #pragma mark - #pragma mark Key Value Observing - (void) removeObservers { [repository removeObserver:self forKeyPath:@"currentBranch"]; [repository removeObserver:self forKeyPath:@"hasChanged"]; if (currentRevList) { [currentRevList removeObserver:self forKeyPath:@"commits"]; [currentRevList removeObserver:self forKeyPath:@"isParsing"]; } if (graphQueue) [graphQueue removeObserver:self forKeyPath:@"operations"]; } - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([@"currentBranch" isEqualToString:context]) { [self updateHistory]; return; } if ([@"repositoryHasChanged" isEqualToString:context]) { [self forceUpdate]; return; } if ([@"commitsUpdated" isEqualToString:context]) { NSInteger changeKind = [(NSNumber *)[change objectForKey:NSKeyValueChangeKindKey] intValue]; if (changeKind == NSKeyValueChangeInsertion) { NSArray *newCommits = [change objectForKey:NSKeyValueChangeNewKey]; if ([repository.currentBranch isSimpleRef]) [graphQueue addOperation:[self operationForCommits:newCommits]]; else [self addCommitsFromArray:newCommits]; } return; } if ([@"revListParsing" isEqualToString:context] || [@"operations" isEqualToString:context]) { [self finishedGraphing]; return; } [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } @end