Skip to content

Commit ff99ac2

Browse files
committed
Major refactor of PBGitRepositoryWatcher
This separates the FS event handling for the 'git' and 'working' directories; and fixes several bugs along the way. #244
1 parent ce7b34c commit ff99ac2

File tree

2 files changed

+162
-92
lines changed

2 files changed

+162
-92
lines changed

Classes/git/PBGitRepositoryWatcher.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,19 @@ extern NSString *PBGitRepositoryEventNotification;
2323
extern NSString *kPBGitRepositoryEventTypeUserInfoKey;
2424
extern NSString *kPBGitRepositoryEventPathsUserInfoKey;
2525

26+
typedef void(^PBGitRepositoryWatcherCallbackBlock)(NSArray *changedFiles);
27+
2628
@interface PBGitRepositoryWatcher : NSObject {
27-
FSEventStreamRef eventStream;
29+
FSEventStreamRef gitDirEventStream;
30+
FSEventStreamRef workDirEventStream;
31+
PBGitRepositoryWatcherCallbackBlock gitDirChangedBlock;
32+
PBGitRepositoryWatcherCallbackBlock workDirChangedBlock;
2833
NSDate *gitDirTouchDate;
2934
NSDate *indexTouchDate;
35+
36+
NSString *gitDir;
37+
NSString *workDir;
38+
3039
__strong PBGitRepositoryWatcher* ownRef;
3140
BOOL _running;
3241
}

Classes/git/PBGitRepositoryWatcher.m

Lines changed: 152 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717
NSString *kPBGitRepositoryEventTypeUserInfoKey = @"kPBGitRepositoryEventTypeUserInfoKey";
1818
NSString *kPBGitRepositoryEventPathsUserInfoKey = @"kPBGitRepositoryEventPathsUserInfoKey";
1919

20+
2021
@interface PBGitRepositoryWatcher ()
2122

2223
@property (nonatomic, strong) NSMutableDictionary *statusCache;
2324

24-
- (void) _handleEventCallback:(NSArray *)eventPaths;
25+
- (void) handleGitDirEventCallback:(NSArray *)eventPaths;
26+
- (void) handleWorkDirEventCallback:(NSArray *)eventPaths;
27+
2528
@end
2629

2730
void PBGitRepositoryWatcherCallback(ConstFSEventStreamRef streamRef,
@@ -30,22 +33,20 @@ void PBGitRepositoryWatcherCallback(ConstFSEventStreamRef streamRef,
3033
void *_eventPaths,
3134
const FSEventStreamEventFlags eventFlags[],
3235
const FSEventStreamEventId eventIds[]){
33-
PBGitRepositoryWatcher *watcher = (__bridge PBGitRepositoryWatcher*)clientCallBackInfo;
36+
PBGitRepositoryWatcherCallbackBlock block = (__bridge PBGitRepositoryWatcherCallbackBlock)clientCallBackInfo;
37+
3438
NSMutableArray *changePaths = [[NSMutableArray alloc] init];
3539
NSArray *eventPaths = (__bridge NSArray*)_eventPaths;
3640
for (int i = 0; i < numEvents; ++i) {
3741
NSString *path = [eventPaths objectAtIndex:i];
38-
if ([path hasSuffix:@".lock"]) {
39-
continue;
40-
}
4142
PBGitRepositoryWatcherEventPath *ep = [[PBGitRepositoryWatcherEventPath alloc] init];
42-
ep.path = path;
43+
ep.path = [path stringByStandardizingPath];
4344
ep.flag = eventFlags[i];
4445
[changePaths addObject:ep];
45-
46+
4647
}
47-
if (changePaths.count) {
48-
[watcher _handleEventCallback:changePaths];
48+
if (block && changePaths.count) {
49+
block(changePaths);
4950
}
5051
}
5152

@@ -55,39 +56,79 @@ @implementation PBGitRepositoryWatcher
5556

5657
- (id) initWithRepository:(PBGitRepository *)theRepository {
5758
self = [super init];
58-
if (!self)
59+
if (!self) {
5960
return nil;
60-
61+
}
62+
63+
__weak PBGitRepositoryWatcher* weakSelf = self;
6164
repository = theRepository;
62-
FSEventStreamContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
6365

64-
NSString *indexPath = repository.gtRepo.gitDirectoryURL.path;
65-
if (!indexPath) {
66-
return nil;
66+
{
67+
gitDir = [repository.gtRepo.gitDirectoryURL.path stringByStandardizingPath];
68+
if (!gitDir) {
69+
return nil;
70+
}
71+
gitDirChangedBlock = ^(NSArray *changeEvents){
72+
NSMutableArray *filteredEvents = [NSMutableArray new];
73+
for (PBGitRepositoryWatcherEventPath *event in changeEvents) {
74+
// exclude all changes to .lock files
75+
if ([event.path hasSuffix:@".lock"]) {
76+
continue;
77+
}
78+
[filteredEvents addObject:event];
79+
}
80+
if (filteredEvents.count) {
81+
[weakSelf handleGitDirEventCallback:filteredEvents];
82+
}
83+
};
84+
FSEventStreamContext gitDirWatcherContext = {0, (__bridge void *)(gitDirChangedBlock), NULL, NULL, NULL};
85+
gitDirEventStream = FSEventStreamCreate(kCFAllocatorDefault, PBGitRepositoryWatcherCallback, &gitDirWatcherContext,
86+
(__bridge CFArrayRef)@[gitDir],
87+
kFSEventStreamEventIdSinceNow, 1.0,
88+
kFSEventStreamCreateFlagUseCFTypes |
89+
kFSEventStreamCreateFlagIgnoreSelf |
90+
kFSEventStreamCreateFlagFileEvents);
91+
6792
}
68-
NSString *workDir = repository.gtRepo.isBare ? nil : repository.gtRepo.fileURL.path;
69-
NSArray *paths = nil;
70-
if (workDir) {
71-
paths = @[indexPath, workDir];
72-
} else {
73-
paths = @[indexPath];
93+
{
94+
workDir = repository.gtRepo.isBare ? nil : [repository.gtRepo.fileURL.path stringByStandardizingPath];
95+
if (workDir) {
96+
workDirChangedBlock = ^(NSArray *changeEvents){
97+
NSMutableArray *filteredEvents = [NSMutableArray new];
98+
PBGitRepositoryWatcher *watcher = weakSelf;
99+
if (!watcher) {
100+
return;
101+
}
102+
for (PBGitRepositoryWatcherEventPath *event in changeEvents) {
103+
// exclude anything under the .git dir
104+
if ([event.path hasPrefix:watcher->gitDir]) {
105+
continue;
106+
}
107+
[filteredEvents addObject:event];
108+
}
109+
if (filteredEvents.count) {
110+
[watcher handleWorkDirEventCallback:filteredEvents];
111+
}
112+
};
113+
FSEventStreamContext workDirWatcherContext = {0, (__bridge void *)(workDirChangedBlock), NULL, NULL, NULL};
114+
workDirEventStream = FSEventStreamCreate(kCFAllocatorDefault, PBGitRepositoryWatcherCallback, &workDirWatcherContext,
115+
(__bridge CFArrayRef)@[workDir],
116+
kFSEventStreamEventIdSinceNow, 1.0,
117+
kFSEventStreamCreateFlagUseCFTypes |
118+
kFSEventStreamCreateFlagIgnoreSelf |
119+
kFSEventStreamCreateFlagFileEvents);
120+
}
74121
}
75122

76-
self.statusCache = [NSMutableDictionary new];
77123

78-
// Create and activate event stream
79-
eventStream = FSEventStreamCreate(kCFAllocatorDefault, PBGitRepositoryWatcherCallback, &context,
80-
(__bridge CFArrayRef)paths,
81-
kFSEventStreamEventIdSinceNow, 1.0,
82-
kFSEventStreamCreateFlagUseCFTypes |
83-
kFSEventStreamCreateFlagIgnoreSelf |
84-
kFSEventStreamCreateFlagFileEvents);
124+
self.statusCache = [NSMutableDictionary new];
125+
85126
if ([PBGitDefaults useRepositoryWatcher])
86127
[self start];
87128
return self;
88129
}
89130

90-
- (NSDate *) _fileModificationDateAtPath:(NSString *)path {
131+
- (NSDate *) fileModificationDateAtPath:(NSString *)path {
91132
NSError* error;
92133
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:path
93134
error:&error];
@@ -99,11 +140,12 @@ - (NSDate *) _fileModificationDateAtPath:(NSString *)path {
99140
return [attrs objectForKey:NSFileModificationDate];
100141
}
101142

102-
- (BOOL) _indexChanged {
143+
- (BOOL) indexChanged {
103144
if (self.repository.isBareRepository) {
104145
return NO;
105146
}
106-
NSDate *newTouchDate = [self _fileModificationDateAtPath:[repository.gitURL.path stringByAppendingPathComponent:@"index"]];
147+
148+
NSDate *newTouchDate = [self fileModificationDateAtPath:[gitDir stringByAppendingPathComponent:@"index"]];
107149
if (![newTouchDate isEqual:indexTouchDate]) {
108150
indexTouchDate = newTouchDate;
109151
return YES;
@@ -112,7 +154,7 @@ - (BOOL) _indexChanged {
112154
return NO;
113155
}
114156

115-
- (BOOL) _gitDirectoryChanged {
157+
- (BOOL) gitDirectoryChanged {
116158

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

143-
- (void) _handleEventCallback:(NSArray *)eventPaths {
185+
- (void) handleGitDirEventCallback:(NSArray *)eventPaths
186+
{
144187
PBGitRepositoryWatcherEventType event = 0x0;
145188

146-
if ([self _indexChanged])
147-
{
148-
// NSLog(@"Watcher found an index change");
189+
if ([self indexChanged]) {
149190
event |= PBGitRepositoryWatcherEventTypeIndex;
150191
}
151-
152-
NSString* ourRepo_ns = repository.gitURL.path;
153-
// libgit2 API results for directories end with a '/'
154-
if (![ourRepo_ns hasSuffix:@"/"])
155-
ourRepo_ns = [NSString stringWithFormat:@"%@/", ourRepo_ns];
156-
192+
193+
157194
NSMutableArray *paths = [NSMutableArray array];
158-
159195
for (PBGitRepositoryWatcherEventPath *eventPath in eventPaths) {
160196
// .git dir
161-
if ([[eventPath.path stringByStandardizingPath] isEqual:[repository.gitURL.path stringByStandardizingPath]]) {
162-
if ([self _gitDirectoryChanged] || eventPath.flag != kFSEventStreamEventFlagNone) {
197+
if ([eventPath.path isEqualToString:gitDir]) {
198+
if ([self gitDirectoryChanged] || eventPath.flag != kFSEventStreamEventFlagNone) {
163199
event |= PBGitRepositoryWatcherEventTypeGitDirectory;
164200
[paths addObject:eventPath.path];
165-
// NSLog(@"Watcher: git dir change in %@", eventPath.path);
166201
}
167202
}
168-
203+
// ignore objects dir ... ?
204+
else if ([eventPath.path rangeOfString:[gitDir stringByAppendingPathComponent:@"objects"]].location != NSNotFound) {
205+
continue;
206+
}
207+
// index is already covered
208+
else if ([eventPath.path rangeOfString:[gitDir stringByAppendingPathComponent:@"index"]].location != NSNotFound) {
209+
continue;
210+
}
169211
// subdirs of .git dir
170-
else if ([eventPath.path rangeOfString:repository.gitURL.path].location != NSNotFound) {
171-
// ignore changes to lock files
172-
if ([eventPath.path hasSuffix:@".lock"])
173-
{
174-
// NSLog(@"Watcher: ignoring change to lock file: %@", eventPath.path);
175-
continue;
176-
}
212+
else if ([eventPath.path rangeOfString:gitDir].location != NSNotFound) {
177213
event |= PBGitRepositoryWatcherEventTypeGitDirectory;
178214
[paths addObject:eventPath.path];
179-
// NSLog(@"Watcher: git dir subdir change in %@", eventPath.path);
180215
}
216+
}
217+
218+
if(event != 0x0){
219+
NSDictionary *eventInfo = @{kPBGitRepositoryEventTypeUserInfoKey:@(event),
220+
kPBGitRepositoryEventPathsUserInfoKey:paths};
181221

182-
else {
183-
unsigned int fileStatus = 0;
184-
NSString *repoPrefix = self.repository.fileURL.path;
185-
// TODO: fix exception
186-
NSString *eventRepoRelativePath = [eventPath.path substringFromIndex:(repoPrefix.length + 1)];
187-
int gitError = git_status_file(&fileStatus, self.repository.gtRepo.git_repository, eventRepoRelativePath.UTF8String);
188-
if (gitError == GIT_OK) {
189-
if (fileStatus & GIT_STATUS_IGNORED) {
190-
// NSLog(@"ignoring change to ignored file: %@", eventPath.path);
191-
} else {
192-
NSNumber *oldStatus = self.statusCache[eventPath.path];
193-
NSNumber *newStatus = @(fileStatus);
194-
if (![oldStatus isEqualTo:newStatus]) {
195-
// NSLog(@"file changed status: %@", eventPath.path);
196-
[paths addObject:eventPath.path];
197-
event |= PBGitRepositoryWatcherEventTypeWorkingDirectory;
198-
} else if (fileStatus & GIT_STATUS_WT_MODIFIED) {
199-
// NSLog(@"modified file touched: %@", eventPath.path);
200-
event |= PBGitRepositoryWatcherEventTypeWorkingDirectory;
201-
[paths addObject:eventPath.path];
202-
}
203-
self.statusCache[eventPath.path] = newStatus;
222+
[[NSNotificationCenter defaultCenter] postNotificationName:PBGitRepositoryEventNotification object:repository userInfo:eventInfo];
223+
}
224+
}
225+
226+
- (void)handleWorkDirEventCallback:(NSArray *)eventPaths
227+
{
228+
PBGitRepositoryWatcherEventType event = 0x0;
229+
230+
NSMutableArray *paths = [NSMutableArray array];
231+
for (PBGitRepositoryWatcherEventPath *eventPath in eventPaths) {
232+
unsigned int fileStatus = 0;
233+
if (![eventPath.path hasPrefix:workDir]) {
234+
continue;
235+
}
236+
if ([eventPath.path isEqualToString:workDir]) {
237+
event |= PBGitRepositoryWatcherEventTypeWorkingDirectory;
238+
[paths addObject:eventPath.path];
239+
continue;
240+
}
241+
NSString *eventRepoRelativePath = [eventPath.path substringFromIndex:(workDir.length + 1)];
242+
int gitError = git_status_file(&fileStatus, self.repository.gtRepo.git_repository, eventRepoRelativePath.UTF8String);
243+
if (gitError == GIT_OK) {
244+
if (fileStatus & GIT_STATUS_IGNORED) {
245+
// NSLog(@"ignoring change to ignored file: %@", eventPath.path);
246+
} else {
247+
NSNumber *oldStatus = self.statusCache[eventPath.path];
248+
NSNumber *newStatus = @(fileStatus);
249+
if (![oldStatus isEqualTo:newStatus]) {
250+
// NSLog(@"file changed status: %@", eventPath.path);
251+
[paths addObject:eventPath.path];
252+
event |= PBGitRepositoryWatcherEventTypeWorkingDirectory;
253+
} else if (fileStatus & GIT_STATUS_WT_MODIFIED) {
254+
// NSLog(@"modified file touched: %@", eventPath.path);
255+
event |= PBGitRepositoryWatcherEventTypeWorkingDirectory;
256+
[paths addObject:eventPath.path];
204257
}
258+
self.statusCache[eventPath.path] = newStatus;
205259
}
206260
}
207261
}
208-
262+
209263
if(event != 0x0){
210-
// NSLog(@"PBGitRepositoryWatcher firing notification for repository %@ with flag %lu", repository, event);
211-
NSDictionary *eventInfo = [NSDictionary dictionaryWithObjectsAndKeys:
212-
[NSNumber numberWithUnsignedInt:event], kPBGitRepositoryEventTypeUserInfoKey,
213-
paths, kPBGitRepositoryEventPathsUserInfoKey,
214-
NULL];
215-
264+
NSDictionary *eventInfo = @{kPBGitRepositoryEventTypeUserInfoKey:@(event),
265+
kPBGitRepositoryEventPathsUserInfoKey:paths};
266+
216267
[[NSNotificationCenter defaultCenter] postNotificationName:PBGitRepositoryEventNotification object:repository userInfo:eventInfo];
217268
}
218269
}
@@ -222,20 +273,30 @@ - (void) start {
222273
return;
223274

224275
// set initial state
225-
[self _gitDirectoryChanged];
226-
[self _indexChanged];
276+
[self gitDirectoryChanged];
277+
[self indexChanged];
227278
ownRef = self; // The callback has no reference to us, so we need to stay alive as long as it may be called
228-
FSEventStreamScheduleWithRunLoop(eventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
229-
FSEventStreamStart(eventStream);
279+
FSEventStreamScheduleWithRunLoop(gitDirEventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
280+
FSEventStreamStart(gitDirEventStream);
281+
282+
if (workDirEventStream) {
283+
FSEventStreamScheduleWithRunLoop(workDirEventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
284+
FSEventStreamStart(workDirEventStream);
285+
}
286+
230287
_running = YES;
231288
}
232289

233290
- (void) stop {
234291
if (!_running)
235292
return;
236293

237-
FSEventStreamStop(eventStream);
238-
FSEventStreamUnscheduleFromRunLoop(eventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
294+
if (workDirEventStream) {
295+
FSEventStreamStop(workDirEventStream);
296+
FSEventStreamUnscheduleFromRunLoop(workDirEventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
297+
}
298+
FSEventStreamStop(gitDirEventStream);
299+
FSEventStreamUnscheduleFromRunLoop(gitDirEventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
239300
ownRef = nil; // Now that we can't be called anymore, we can allow ourself to be -dealloc'd
240301
_running = NO;
241302
}

0 commit comments

Comments
 (0)