5 // Created by Pieter de Bie on 9/12/09.
6 // Copyright 2009 Pieter de Bie. All rights reserved.
10 #import "PBGitRepository.h"
11 #import "PBGitBinary.h"
12 #import "PBEasyPipe.h"
13 #import "NSString_RegEx.h"
14 #import "PBChangedFile.h"
16 NSString *PBGitIndexIndexRefreshStatus = @"PBGitIndexIndexRefreshStatus";
17 NSString *PBGitIndexIndexRefreshFailed = @"PBGitIndexIndexRefreshFailed";
18 NSString *PBGitIndexFinishedIndexRefresh = @"PBGitIndexFinishedIndexRefresh";
20 NSString *PBGitIndexCommitStatus = @"PBGitIndexCommitStatus";
21 NSString *PBGitIndexCommitFailed = @"PBGitIndexCommitFailed";
22 NSString *PBGitIndexFinishedCommit = @"PBGitIndexFinishedCommit";
25 @interface PBGitIndex (IndexRefreshMethods)
27 - (NSArray *)linesFromNotification:(NSNotification *)notification;
28 - (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines;
29 - (void)addFilesFromDictionary:(NSMutableDictionary *)dictionary staged:(BOOL)staged tracked:(BOOL)tracked;
31 - (void)indexStepComplete;
33 - (void)indexRefreshFinished:(NSNotification *)notification;
34 - (void)readOtherFiles:(NSNotification *)notification;
35 - (void)readUnstagedFiles:(NSNotification *)notification;
36 - (void)readStagedFiles:(NSNotification *)notification;
40 @interface PBGitIndex ()
42 // Returns the tree to compare the index to, based
43 // on whether amend is set or not.
44 - (NSString *) parentTree;
45 - (void)postCommitUpdate:(NSString *)update;
46 - (void)postCommitFailure:(NSString *)reason;
49 @implementation PBGitIndex
53 - (id)initWithRepository:(PBGitRepository *)theRepository workingDirectory:(NSURL *)theWorkingDirectory
55 if (!(self = [super init]))
58 NSAssert(theWorkingDirectory, @"PBGitIndex requires a working directory");
59 NSAssert(theRepository, @"PBGitIndex requires a repository");
61 repository = theRepository;
62 workingDirectory = theWorkingDirectory;
63 files = [NSMutableArray array];
68 - (NSArray *)indexChanges
73 - (void)setAmend:(BOOL)newAmend
75 if (newAmend == amend)
79 amendEnvironment = nil;
86 // If we amend, we want to keep the author information for the previous commit
87 // We do this by reading in the previous commit, and storing the information
88 // in a dictionary. This dictionary will then later be read by [self commit:]
89 NSString *message = [repository outputForCommand:@"cat-file commit HEAD"];
90 NSArray *match = [message substringsMatchingRegularExpression:@"\nauthor ([^\n]*) <([^\n>]*)> ([0-9]+[^\n]*)\n" count:3 options:0 ranges:nil error:nil];
92 amendEnvironment = [NSDictionary dictionaryWithObjectsAndKeys:[match objectAtIndex:1], @"GIT_AUTHOR_NAME",
93 [match objectAtIndex:2], @"GIT_AUTHOR_EMAIL",
94 [match objectAtIndex:3], @"GIT_AUTHOR_DATE",
100 // If we were already refreshing the index, we don't want
101 // double notifications. As we can't stop the tasks anymore,
102 // just cancel the notifications
104 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
105 [nc removeObserver:self];
107 // Ask Git to refresh the index
108 NSFileHandle *updateHandle = [PBEasyPipe handleForCommand:[PBGitBinary path]
109 withArgs:[NSArray arrayWithObjects:@"update-index", @"-q", @"--unmerged", @"--ignore-missing", @"--refresh", nil]
110 inDir:[workingDirectory path]];
113 selector:@selector(indexRefreshFinished:)
114 name:NSFileHandleReadToEndOfFileCompletionNotification
115 object:updateHandle];
116 [updateHandle readToEndOfFileInBackgroundAndNotify];
120 - (NSString *) parentTree
122 NSString *parent = amend ? @"HEAD^" : @"HEAD";
124 if (![repository parseReference:parent])
125 // We don't have a head ref. Return the empty tree.
126 return @"4b825dc642cb6eb9a060e54bf8d69288fbee4904";
131 - (void)commitWithMessage:(NSString *)commitMessage
133 NSMutableString *commitSubject = [@"commit: " mutableCopy];
134 NSRange newLine = [commitMessage rangeOfString:@"\n"];
135 if (newLine.location == NSNotFound)
136 [commitSubject appendString:commitMessage];
138 [commitSubject appendString:[commitMessage substringToIndex:newLine.location]];
140 NSString *commitMessageFile;
141 commitMessageFile = [repository.fileURL.path stringByAppendingPathComponent:@"COMMIT_EDITMSG"];
143 [commitMessage writeToFile:commitMessageFile atomically:YES encoding:NSUTF8StringEncoding error:nil];
146 [self postCommitUpdate:@"Creating tree"];
147 NSString *tree = [repository outputForCommand:@"write-tree"];
148 if ([tree length] != 40)
149 return [self postCommitFailure:@"Creating tree failed"];
152 NSMutableArray *arguments = [NSMutableArray arrayWithObjects:@"commit-tree", tree, nil];
153 NSString *parent = amend ? @"HEAD^" : @"HEAD";
154 if ([repository parseReference:parent]) {
155 [arguments addObject:@"-p"];
156 [arguments addObject:parent];
159 [self postCommitUpdate:@"Creating commit"];
161 NSString *commit = [repository outputForArguments:arguments
162 inputString:commitMessage
163 byExtendingEnvironment:amendEnvironment
166 if (ret || [commit length] != 40)
167 return [self postCommitFailure:@"Could not create a commit object"];
169 [self postCommitUpdate:@"Running hooks"];
170 if (![repository executeHook:@"pre-commit" output:nil])
171 return [self postCommitFailure:@"Pre-commit hook failed"];
173 if (![repository executeHook:@"commit-msg" withArgs:[NSArray arrayWithObject:commitMessageFile] output:nil])
174 return [self postCommitFailure:@"Commit-msg hook failed"];
176 [self postCommitUpdate:@"Updating HEAD"];
177 [repository outputForArguments:[NSArray arrayWithObjects:@"update-ref", @"-m", commitSubject, @"HEAD", commit, nil]
180 return [self postCommitFailure:@"Could not update HEAD"];
182 [self postCommitUpdate:@"Running post-commit hook"];
184 BOOL success = [repository executeHook:@"post-commit" output:nil];
185 NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:[NSNumber numberWithBool:success] forKey:@"success"];
186 NSString *description;
188 description = [NSString stringWithFormat:@"Successfull created commit %@", commit];
190 description = [NSString stringWithFormat:@"Post-commit hook failed, but successfully created commit %@", commit];
192 [userInfo setObject:description forKey:@"description"];
193 [userInfo setObject:commit forKey:@"sha"];
195 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexFinishedCommit
201 repository.hasChanged = YES;
203 amendEnvironment = nil;
211 - (void)postCommitUpdate:(NSString *)update
213 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexCommitStatus
215 userInfo:[NSDictionary dictionaryWithObject:update forKey:@"description"]];
218 - (void)postCommitFailure:(NSString *)reason
220 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexCommitFailed
222 userInfo:[NSDictionary dictionaryWithObject:reason forKey:@"description"]];
226 - (BOOL)stageFiles:(NSArray *)stageFiles
228 // Input string for update-index
229 // This will be a list of filenames that
230 // should be updated. It's similar to
231 // "git add -- <files>
232 NSMutableString *input = [NSMutableString string];
234 for (PBChangedFile *file in stageFiles) {
235 [input appendFormat:@"%@\0", file.path];
239 [repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"--add", @"--remove", @"-z", @"--stdin", nil]
244 // FIXME: failed notification?
245 NSLog(@"Error when updating index. Retvalue: %i", ret);
249 // TODO: Stop Tracking
250 for (PBChangedFile *file in stageFiles)
252 file.hasUnstagedChanges = NO;
253 file.hasStagedChanges = YES;
255 // TODO: Resume tracking
259 // TODO: Refactor with above. What's a better name for this?
260 - (BOOL)unstageFiles:(NSArray *)unstageFiles
262 NSMutableString *input = [NSMutableString string];
264 for (PBChangedFile *file in unstageFiles) {
265 [input appendString:[file indexInfo]];
269 [repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"-z", @"--index-info", nil]
275 // FIXME: Failed notification
276 NSLog(@"Error when updating index. Retvalue: %i", ret);
280 // TODO: stop tracking
281 for (PBChangedFile *file in unstageFiles)
283 file.hasUnstagedChanges = YES;
284 file.hasStagedChanges = NO;
286 // TODO: resume tracking
291 - (BOOL)applyPatch:(NSString *)hunk stage:(BOOL)stage reverse:(BOOL)reverse;
293 NSMutableArray *array = [NSMutableArray arrayWithObjects:@"apply", nil];
295 [array addObject:@"--cached"];
297 [array addObject:@"--reverse"];
300 NSString *error = [repository outputForArguments:array
304 // FIXME: show this error, rather than just logging it
306 NSLog(@"Error: %@", error);
310 // TODO: Try to be smarter about what to refresh
316 - (NSString *)diffForFile:(PBChangedFile *)file staged:(BOOL)staged contextLines:(NSUInteger)context
318 NSString *parameter = [NSString stringWithFormat:@"-U%u", context];
320 NSString *indexPath = [@":0:" stringByAppendingString:file.path];
322 if (file.status == NEW)
323 return [repository outputForArguments:[NSArray arrayWithObjects:@"show", indexPath, nil]];
325 return [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-index", parameter, @"--cached", [self parentTree], @"--", file.path, nil]];
329 if (file.status == NEW) {
330 NSStringEncoding encoding;
331 NSError *error = nil;
332 NSString *path = [[repository workingDirectory] stringByAppendingPathComponent:file.path];
333 NSString *contents = [NSString stringWithContentsOfFile:path
334 usedEncoding:&encoding
342 return [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-files", parameter, @"--", file.path, nil]];
346 # pragma mark WebKit Accessibility
348 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
355 @implementation PBGitIndex (IndexRefreshMethods)
357 - (void)indexRefreshFinished:(NSNotification *)notification
359 if ([(NSNumber *)[(NSDictionary *)[notification userInfo] objectForKey:@"NSFileHandleError"] intValue])
361 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexRefreshFailed
363 userInfo:[NSDictionary dictionaryWithObject:@"update-index failed" forKey:@"description"]];
367 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexRefreshStatus
369 userInfo:[NSDictionary dictionaryWithObject:@"update-index success" forKey:@"description"]];
371 // Now that the index is refreshed, we need to read the information from the index
372 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
374 // Other files (not tracked, not ignored)
375 NSFileHandle *handle = [PBEasyPipe handleForCommand:[PBGitBinary path]
376 withArgs:[NSArray arrayWithObjects:@"ls-files", @"--others", @"--exclude-standard", @"-z", nil]
377 inDir:[workingDirectory path]];
378 [nc addObserver:self selector:@selector(readOtherFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
379 [handle readToEndOfFileInBackgroundAndNotify];
383 handle = [PBEasyPipe handleForCommand:[PBGitBinary path]
384 withArgs:[NSArray arrayWithObjects:@"diff-files", @"-z", nil]
385 inDir:[workingDirectory path]];
386 [nc addObserver:self selector:@selector(readUnstagedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
387 [handle readToEndOfFileInBackgroundAndNotify];
391 handle = [PBEasyPipe handleForCommand:[PBGitBinary path]
392 withArgs:[NSArray arrayWithObjects:@"diff-index", @"--cached", @"-z", [self parentTree], nil]
393 inDir:[workingDirectory path]];
394 [nc addObserver:self selector:@selector(readStagedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
395 [handle readToEndOfFileInBackgroundAndNotify];
399 - (void)readOtherFiles:(NSNotification *)notification
401 NSArray *lines = [self linesFromNotification:notification];
402 NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] initWithCapacity:[lines count]];
403 // Other files are untracked, so we don't have any real index information. Instead, we can just fake it.
404 // The line below is used to add the file to the index
405 // FIXME: request the real file mode
406 NSArray *fileStatus = [NSArray arrayWithObjects:@":000000", @"100644", @"0000000000000000000000000000000000000000", @"0000000000000000000000000000000000000000", @"A", nil];
407 for (NSString *path in lines) {
408 if ([path length] == 0)
410 [dictionary setObject:fileStatus forKey:path];
413 [self addFilesFromDictionary:dictionary staged:NO tracked:NO];
414 [self indexStepComplete];
417 - (void) readStagedFiles:(NSNotification *)notification
419 NSArray *lines = [self linesFromNotification:notification];
420 NSMutableDictionary *dic = [self dictionaryForLines:lines];
421 [self addFilesFromDictionary:dic staged:YES tracked:YES];
422 [self indexStepComplete];
425 - (void) readUnstagedFiles:(NSNotification *)notification
427 NSArray *lines = [self linesFromNotification:notification];
428 NSMutableDictionary *dic = [self dictionaryForLines:lines];
429 [self addFilesFromDictionary:dic staged:NO tracked:YES];
430 [self indexStepComplete];
433 - (void) addFilesFromDictionary:(NSMutableDictionary *)dictionary staged:(BOOL)staged tracked:(BOOL)tracked
435 // TODO: Stop tracking files
436 // Iterate over all existing files
437 for (PBChangedFile *file in files) {
438 NSArray *fileStatus = [dictionary objectForKey:file.path];
439 // Object found, this is still a cached / uncached thing
442 NSString *mode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
443 NSString *sha = [fileStatus objectAtIndex:2];
444 file.commitBlobSHA = sha;
445 file.commitBlobMode = mode;
448 file.hasStagedChanges = YES;
450 file.hasUnstagedChanges = YES;
452 // Untracked file, set status to NEW, only unstaged changes
453 file.hasStagedChanges = NO;
454 file.hasUnstagedChanges = YES;
458 // We handled this file, remove it from the dictionary
459 [dictionary removeObjectForKey:file.path];
461 // Object not found in the dictionary, so let's reset its appropriate
462 // change (stage or untracked) if necessary.
464 // Staged dictionary, so file does not have staged changes
466 file.hasStagedChanges = NO;
467 // Tracked file does not have unstaged changes, file is not new,
468 // so we can set it to No. (If it would be new, it would not
469 // be in this dictionary, but in the "other dictionary").
470 else if (tracked && file.status != NEW)
471 file.hasUnstagedChanges = NO;
472 // Unstaged, untracked dictionary ("Other" files), and file
473 // is indicated as new (which would be untracked), so let's
475 else if (!tracked && file.status == NEW)
476 file.hasUnstagedChanges = NO;
479 // TODO: Finish tracking files
481 // Do new files only if necessary
482 if (![[dictionary allKeys] count])
485 // All entries left in the dictionary haven't been accounted for
486 // above, so we need to add them to the "files" array
487 [self willChangeValueForKey:@"indexChanges"];
488 for (NSString *path in [dictionary allKeys]) {
489 NSArray *fileStatus = [dictionary objectForKey:path];
491 PBChangedFile *file = [[PBChangedFile alloc] initWithPath:path];
492 if ([[fileStatus objectAtIndex:4] isEqualToString:@"D"])
493 file.status = DELETED;
494 else if([[fileStatus objectAtIndex:0] isEqualToString:@":000000"])
497 file.status = MODIFIED;
500 file.commitBlobMode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
501 file.commitBlobSHA = [fileStatus objectAtIndex:2];
504 file.hasStagedChanges = staged;
505 file.hasUnstagedChanges = !staged;
507 [files addObject:file];
509 [self didChangeValueForKey:@"indexChanges"];
512 # pragma mark Utility methods
513 - (NSArray *)linesFromNotification:(NSNotification *)notification
515 NSData *data = [[notification userInfo] valueForKey:NSFileHandleNotificationDataItem];
517 return [NSArray array];
519 NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
520 // FIXME: Return an error?
522 return [NSArray array];
524 // Strip trailing null
525 if ([string hasSuffix:@"\0"])
526 string = [string substringToIndex:[string length]-1];
528 if ([string length] == 0)
529 return [NSArray array];
531 return [string componentsSeparatedByString:@"\0"];
534 - (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines
536 NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:[lines count]/2];
538 // Fill the dictionary with the new information. These lines are in the form of:
539 // :00000 :0644 OTHER INDEX INFORMATION
542 NSAssert1([lines count] % 2 == 0, @"Lines must have an even number of lines: %@", lines);
544 NSEnumerator *enumerator = [lines objectEnumerator];
545 NSString *fileStatus;
546 while (fileStatus = [enumerator nextObject]) {
547 NSString *fileName = [enumerator nextObject];
548 [dictionary setObject:[fileStatus componentsSeparatedByString:@" "] forKey:fileName];
554 // This method is called for each of the three processes from above.
555 // If all three are finished (self.busy == 0), then we can delete
556 // all files previously marked as deletable
557 - (void)indexStepComplete
559 // if we're still busy, do nothing :)
563 // At this point, all index operations have finished.
564 // We need to find all files that don't have either
565 // staged or unstaged files, and delete them
567 NSMutableArray *deleteFiles = [NSMutableArray array];
568 for (PBChangedFile *file in files) {
569 if (!file.hasStagedChanges && !file.hasUnstagedChanges)
570 [deleteFiles addObject:file];
573 if ([deleteFiles count]) {
574 [self willChangeValueForKey:@"indexChanges"];
575 for (PBChangedFile *file in deleteFiles)
576 [files removeObject:file];
577 [self didChangeValueForKey:@"indexChanges"];
580 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexFinishedIndexRefresh