Skip to content

Commit

Permalink
Major refactor of PBGitRepositoryWatcher
Browse files Browse the repository at this point in the history
This separates the FS event handling for the 'git' and 'working' directories; and fixes several bugs along the way.

#244
  • Loading branch information
rowanj committed Aug 19, 2013
1 parent ce7b34c commit ff99ac2
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 92 deletions.
11 changes: 10 additions & 1 deletion Classes/git/PBGitRepositoryWatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,19 @@ extern NSString *PBGitRepositoryEventNotification;
extern NSString *kPBGitRepositoryEventTypeUserInfoKey;
extern NSString *kPBGitRepositoryEventPathsUserInfoKey;

typedef void(^PBGitRepositoryWatcherCallbackBlock)(NSArray *changedFiles);

@interface PBGitRepositoryWatcher : NSObject {
FSEventStreamRef eventStream;
FSEventStreamRef gitDirEventStream;
FSEventStreamRef workDirEventStream;
PBGitRepositoryWatcherCallbackBlock gitDirChangedBlock;
PBGitRepositoryWatcherCallbackBlock workDirChangedBlock;
NSDate *gitDirTouchDate;
NSDate *indexTouchDate;

NSString *gitDir;
NSString *workDir;

__strong PBGitRepositoryWatcher* ownRef;
BOOL _running;
}
Expand Down
243 changes: 152 additions & 91 deletions Classes/git/PBGitRepositoryWatcher.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@
NSString *kPBGitRepositoryEventTypeUserInfoKey = @"kPBGitRepositoryEventTypeUserInfoKey";
NSString *kPBGitRepositoryEventPathsUserInfoKey = @"kPBGitRepositoryEventPathsUserInfoKey";


@interface PBGitRepositoryWatcher ()

@property (nonatomic, strong) NSMutableDictionary *statusCache;

- (void) _handleEventCallback:(NSArray *)eventPaths;
- (void) handleGitDirEventCallback:(NSArray *)eventPaths;
- (void) handleWorkDirEventCallback:(NSArray *)eventPaths;

@end

void PBGitRepositoryWatcherCallback(ConstFSEventStreamRef streamRef,
Expand All @@ -30,22 +33,20 @@ void PBGitRepositoryWatcherCallback(ConstFSEventStreamRef streamRef,
void *_eventPaths,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[]){
PBGitRepositoryWatcher *watcher = (__bridge PBGitRepositoryWatcher*)clientCallBackInfo;
PBGitRepositoryWatcherCallbackBlock block = (__bridge PBGitRepositoryWatcherCallbackBlock)clientCallBackInfo;

NSMutableArray *changePaths = [[NSMutableArray alloc] init];
NSArray *eventPaths = (__bridge NSArray*)_eventPaths;
for (int i = 0; i < numEvents; ++i) {
NSString *path = [eventPaths objectAtIndex:i];
if ([path hasSuffix:@".lock"]) {
continue;
}
PBGitRepositoryWatcherEventPath *ep = [[PBGitRepositoryWatcherEventPath alloc] init];
ep.path = path;
ep.path = [path stringByStandardizingPath];
ep.flag = eventFlags[i];
[changePaths addObject:ep];

}
if (changePaths.count) {
[watcher _handleEventCallback:changePaths];
if (block && changePaths.count) {
block(changePaths);
}
}

Expand All @@ -55,39 +56,79 @@ @implementation PBGitRepositoryWatcher

- (id) initWithRepository:(PBGitRepository *)theRepository {
self = [super init];
if (!self)
if (!self) {
return nil;

}

__weak PBGitRepositoryWatcher* weakSelf = self;
repository = theRepository;
FSEventStreamContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};

NSString *indexPath = repository.gtRepo.gitDirectoryURL.path;
if (!indexPath) {
return nil;
{
gitDir = [repository.gtRepo.gitDirectoryURL.path stringByStandardizingPath];
if (!gitDir) {
return nil;
}
gitDirChangedBlock = ^(NSArray *changeEvents){
NSMutableArray *filteredEvents = [NSMutableArray new];
for (PBGitRepositoryWatcherEventPath *event in changeEvents) {
// exclude all changes to .lock files
if ([event.path hasSuffix:@".lock"]) {
continue;
}
[filteredEvents addObject:event];
}
if (filteredEvents.count) {
[weakSelf handleGitDirEventCallback:filteredEvents];
}
};
FSEventStreamContext gitDirWatcherContext = {0, (__bridge void *)(gitDirChangedBlock), NULL, NULL, NULL};
gitDirEventStream = FSEventStreamCreate(kCFAllocatorDefault, PBGitRepositoryWatcherCallback, &gitDirWatcherContext,
(__bridge CFArrayRef)@[gitDir],
kFSEventStreamEventIdSinceNow, 1.0,
kFSEventStreamCreateFlagUseCFTypes |
kFSEventStreamCreateFlagIgnoreSelf |
kFSEventStreamCreateFlagFileEvents);

}
NSString *workDir = repository.gtRepo.isBare ? nil : repository.gtRepo.fileURL.path;
NSArray *paths = nil;
if (workDir) {
paths = @[indexPath, workDir];
} else {
paths = @[indexPath];
{
workDir = repository.gtRepo.isBare ? nil : [repository.gtRepo.fileURL.path stringByStandardizingPath];
if (workDir) {
workDirChangedBlock = ^(NSArray *changeEvents){
NSMutableArray *filteredEvents = [NSMutableArray new];
PBGitRepositoryWatcher *watcher = weakSelf;
if (!watcher) {
return;
}
for (PBGitRepositoryWatcherEventPath *event in changeEvents) {
// exclude anything under the .git dir
if ([event.path hasPrefix:watcher->gitDir]) {
continue;
}
[filteredEvents addObject:event];
}
if (filteredEvents.count) {
[watcher handleWorkDirEventCallback:filteredEvents];
}
};
FSEventStreamContext workDirWatcherContext = {0, (__bridge void *)(workDirChangedBlock), NULL, NULL, NULL};
workDirEventStream = FSEventStreamCreate(kCFAllocatorDefault, PBGitRepositoryWatcherCallback, &workDirWatcherContext,
(__bridge CFArrayRef)@[workDir],
kFSEventStreamEventIdSinceNow, 1.0,
kFSEventStreamCreateFlagUseCFTypes |
kFSEventStreamCreateFlagIgnoreSelf |
kFSEventStreamCreateFlagFileEvents);
}
}

self.statusCache = [NSMutableDictionary new];

// Create and activate event stream
eventStream = FSEventStreamCreate(kCFAllocatorDefault, PBGitRepositoryWatcherCallback, &context,
(__bridge CFArrayRef)paths,
kFSEventStreamEventIdSinceNow, 1.0,
kFSEventStreamCreateFlagUseCFTypes |
kFSEventStreamCreateFlagIgnoreSelf |
kFSEventStreamCreateFlagFileEvents);
self.statusCache = [NSMutableDictionary new];

if ([PBGitDefaults useRepositoryWatcher])
[self start];
return self;
}

- (NSDate *) _fileModificationDateAtPath:(NSString *)path {
- (NSDate *) fileModificationDateAtPath:(NSString *)path {
NSError* error;
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:path
error:&error];
Expand All @@ -99,11 +140,12 @@ - (NSDate *) _fileModificationDateAtPath:(NSString *)path {
return [attrs objectForKey:NSFileModificationDate];
}

- (BOOL) _indexChanged {
- (BOOL) indexChanged {
if (self.repository.isBareRepository) {
return NO;
}
NSDate *newTouchDate = [self _fileModificationDateAtPath:[repository.gitURL.path stringByAppendingPathComponent:@"index"]];

NSDate *newTouchDate = [self fileModificationDateAtPath:[gitDir stringByAppendingPathComponent:@"index"]];
if (![newTouchDate isEqual:indexTouchDate]) {
indexTouchDate = newTouchDate;
return YES;
Expand All @@ -112,7 +154,7 @@ - (BOOL) _indexChanged {
return NO;
}

- (BOOL) _gitDirectoryChanged {
- (BOOL) gitDirectoryChanged {

for (NSURL* fileURL in [[NSFileManager defaultManager] contentsOfDirectoryAtURL:repository.gitURL
includingPropertiesForKeys:[NSArray arrayWithObject:NSURLContentModificationDateKey]
Expand Down Expand Up @@ -140,79 +182,88 @@ - (BOOL) _gitDirectoryChanged {
return NO;
}

- (void) _handleEventCallback:(NSArray *)eventPaths {
- (void) handleGitDirEventCallback:(NSArray *)eventPaths
{
PBGitRepositoryWatcherEventType event = 0x0;

if ([self _indexChanged])
{
// NSLog(@"Watcher found an index change");
if ([self indexChanged]) {
event |= PBGitRepositoryWatcherEventTypeIndex;
}

NSString* ourRepo_ns = repository.gitURL.path;
// libgit2 API results for directories end with a '/'
if (![ourRepo_ns hasSuffix:@"/"])
ourRepo_ns = [NSString stringWithFormat:@"%@/", ourRepo_ns];



NSMutableArray *paths = [NSMutableArray array];

for (PBGitRepositoryWatcherEventPath *eventPath in eventPaths) {
// .git dir
if ([[eventPath.path stringByStandardizingPath] isEqual:[repository.gitURL.path stringByStandardizingPath]]) {
if ([self _gitDirectoryChanged] || eventPath.flag != kFSEventStreamEventFlagNone) {
if ([eventPath.path isEqualToString:gitDir]) {
if ([self gitDirectoryChanged] || eventPath.flag != kFSEventStreamEventFlagNone) {
event |= PBGitRepositoryWatcherEventTypeGitDirectory;
[paths addObject:eventPath.path];
// NSLog(@"Watcher: git dir change in %@", eventPath.path);
}
}

// ignore objects dir ... ?
else if ([eventPath.path rangeOfString:[gitDir stringByAppendingPathComponent:@"objects"]].location != NSNotFound) {
continue;
}
// index is already covered
else if ([eventPath.path rangeOfString:[gitDir stringByAppendingPathComponent:@"index"]].location != NSNotFound) {
continue;
}
// subdirs of .git dir
else if ([eventPath.path rangeOfString:repository.gitURL.path].location != NSNotFound) {
// ignore changes to lock files
if ([eventPath.path hasSuffix:@".lock"])
{
// NSLog(@"Watcher: ignoring change to lock file: %@", eventPath.path);
continue;
}
else if ([eventPath.path rangeOfString:gitDir].location != NSNotFound) {
event |= PBGitRepositoryWatcherEventTypeGitDirectory;
[paths addObject:eventPath.path];
// NSLog(@"Watcher: git dir subdir change in %@", eventPath.path);
}
}

if(event != 0x0){
NSDictionary *eventInfo = @{kPBGitRepositoryEventTypeUserInfoKey:@(event),
kPBGitRepositoryEventPathsUserInfoKey:paths};

else {
unsigned int fileStatus = 0;
NSString *repoPrefix = self.repository.fileURL.path;
// TODO: fix exception
NSString *eventRepoRelativePath = [eventPath.path substringFromIndex:(repoPrefix.length + 1)];
int gitError = git_status_file(&fileStatus, self.repository.gtRepo.git_repository, eventRepoRelativePath.UTF8String);
if (gitError == GIT_OK) {
if (fileStatus & GIT_STATUS_IGNORED) {
// NSLog(@"ignoring change to ignored file: %@", eventPath.path);
} else {
NSNumber *oldStatus = self.statusCache[eventPath.path];
NSNumber *newStatus = @(fileStatus);
if (![oldStatus isEqualTo:newStatus]) {
// NSLog(@"file changed status: %@", eventPath.path);
[paths addObject:eventPath.path];
event |= PBGitRepositoryWatcherEventTypeWorkingDirectory;
} else if (fileStatus & GIT_STATUS_WT_MODIFIED) {
// NSLog(@"modified file touched: %@", eventPath.path);
event |= PBGitRepositoryWatcherEventTypeWorkingDirectory;
[paths addObject:eventPath.path];
}
self.statusCache[eventPath.path] = newStatus;
[[NSNotificationCenter defaultCenter] postNotificationName:PBGitRepositoryEventNotification object:repository userInfo:eventInfo];
}
}

- (void)handleWorkDirEventCallback:(NSArray *)eventPaths
{
PBGitRepositoryWatcherEventType event = 0x0;

NSMutableArray *paths = [NSMutableArray array];
for (PBGitRepositoryWatcherEventPath *eventPath in eventPaths) {
unsigned int fileStatus = 0;
if (![eventPath.path hasPrefix:workDir]) {
continue;
}
if ([eventPath.path isEqualToString:workDir]) {
event |= PBGitRepositoryWatcherEventTypeWorkingDirectory;
[paths addObject:eventPath.path];
continue;
}
NSString *eventRepoRelativePath = [eventPath.path substringFromIndex:(workDir.length + 1)];
int gitError = git_status_file(&fileStatus, self.repository.gtRepo.git_repository, eventRepoRelativePath.UTF8String);
if (gitError == GIT_OK) {
if (fileStatus & GIT_STATUS_IGNORED) {
// NSLog(@"ignoring change to ignored file: %@", eventPath.path);
} else {
NSNumber *oldStatus = self.statusCache[eventPath.path];
NSNumber *newStatus = @(fileStatus);
if (![oldStatus isEqualTo:newStatus]) {
// NSLog(@"file changed status: %@", eventPath.path);
[paths addObject:eventPath.path];
event |= PBGitRepositoryWatcherEventTypeWorkingDirectory;
} else if (fileStatus & GIT_STATUS_WT_MODIFIED) {
// NSLog(@"modified file touched: %@", eventPath.path);
event |= PBGitRepositoryWatcherEventTypeWorkingDirectory;
[paths addObject:eventPath.path];
}
self.statusCache[eventPath.path] = newStatus;
}
}
}

if(event != 0x0){
// NSLog(@"PBGitRepositoryWatcher firing notification for repository %@ with flag %lu", repository, event);
NSDictionary *eventInfo = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithUnsignedInt:event], kPBGitRepositoryEventTypeUserInfoKey,
paths, kPBGitRepositoryEventPathsUserInfoKey,
NULL];

NSDictionary *eventInfo = @{kPBGitRepositoryEventTypeUserInfoKey:@(event),
kPBGitRepositoryEventPathsUserInfoKey:paths};

[[NSNotificationCenter defaultCenter] postNotificationName:PBGitRepositoryEventNotification object:repository userInfo:eventInfo];
}
}
Expand All @@ -222,20 +273,30 @@ - (void) start {
return;

// set initial state
[self _gitDirectoryChanged];
[self _indexChanged];
[self gitDirectoryChanged];
[self indexChanged];
ownRef = self; // The callback has no reference to us, so we need to stay alive as long as it may be called
FSEventStreamScheduleWithRunLoop(eventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
FSEventStreamStart(eventStream);
FSEventStreamScheduleWithRunLoop(gitDirEventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
FSEventStreamStart(gitDirEventStream);

if (workDirEventStream) {
FSEventStreamScheduleWithRunLoop(workDirEventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
FSEventStreamStart(workDirEventStream);
}

_running = YES;
}

- (void) stop {
if (!_running)
return;

FSEventStreamStop(eventStream);
FSEventStreamUnscheduleFromRunLoop(eventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
if (workDirEventStream) {
FSEventStreamStop(workDirEventStream);
FSEventStreamUnscheduleFromRunLoop(workDirEventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
}
FSEventStreamStop(gitDirEventStream);
FSEventStreamUnscheduleFromRunLoop(gitDirEventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
ownRef = nil; // Now that we can't be called anymore, we can allow ourself to be -dealloc'd
_running = NO;
}
Expand Down

0 comments on commit ff99ac2

Please sign in to comment.