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 *PBGitIndexIndexUpdated = @"GBGitIndexIndexUpdated";
22 NSString *PBGitIndexCommitStatus = @"PBGitIndexCommitStatus";
23 NSString *PBGitIndexCommitFailed = @"PBGitIndexCommitFailed";
24 NSString *PBGitIndexFinishedCommit = @"PBGitIndexFinishedCommit";
26 NSString *PBGitIndexAmendMessageAvailable = @"PBGitIndexAmendMessageAvailable";
28 @interface PBGitIndex (IndexRefreshMethods)
30 - (NSArray *)linesFromNotification:(NSNotification *)notification;
31 - (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines;
32 - (void)addFilesFromDictionary:(NSMutableDictionary *)dictionary staged:(BOOL)staged tracked:(BOOL)tracked;
34 - (void)indexStepComplete;
36 - (void)indexRefreshFinished:(NSNotification *)notification;
37 - (void)readOtherFiles:(NSNotification *)notification;
38 - (void)readUnstagedFiles:(NSNotification *)notification;
39 - (void)readStagedFiles:(NSNotification *)notification;
43 @interface PBGitIndex ()
45 // Returns the tree to compare the index to, based
46 // on whether amend is set or not.
47 - (NSString *) parentTree;
48 - (void)postCommitUpdate:(NSString *)update;
49 - (void)postCommitFailure:(NSString *)reason;
50 - (void)postIndexChange;
53 @implementation PBGitIndex
57 - (id)initWithRepository:(PBGitRepository *)theRepository workingDirectory:(NSURL *)theWorkingDirectory
59 if (!(self = [super init]))
62 NSAssert(theWorkingDirectory, @"PBGitIndex requires a working directory");
63 NSAssert(theRepository, @"PBGitIndex requires a repository");
65 repository = theRepository;
66 workingDirectory = theWorkingDirectory;
67 files = [NSMutableArray array];
72 - (NSArray *)indexChanges
77 - (void)setAmend:(BOOL)newAmend
79 if (newAmend == amend)
83 amendEnvironment = nil;
90 // If we amend, we want to keep the author information for the previous commit
91 // We do this by reading in the previous commit, and storing the information
92 // in a dictionary. This dictionary will then later be read by [self commit:]
93 NSString *message = [repository outputForCommand:@"cat-file commit HEAD"];
94 NSArray *match = [message substringsMatchingRegularExpression:@"\nauthor ([^\n]*) <([^\n>]*)> ([0-9]+[^\n]*)\n" count:3 options:0 ranges:nil error:nil];
96 amendEnvironment = [NSDictionary dictionaryWithObjectsAndKeys:[match objectAtIndex:1], @"GIT_AUTHOR_NAME",
97 [match objectAtIndex:2], @"GIT_AUTHOR_EMAIL",
98 [match objectAtIndex:3], @"GIT_AUTHOR_DATE",
101 // Find the commit message
102 NSRange r = [message rangeOfString:@"\n\n"];
103 if (r.location != NSNotFound) {
104 NSString *commitMessage = [message substringFromIndex:r.location + 2];
105 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexAmendMessageAvailable
107 userInfo:[NSDictionary dictionaryWithObject:commitMessage forKey:@"message"]];
114 // If we were already refreshing the index, we don't want
115 // double notifications. As we can't stop the tasks anymore,
116 // just cancel the notifications
118 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
119 [nc removeObserver:self];
121 // Ask Git to refresh the index
122 NSFileHandle *updateHandle = [PBEasyPipe handleForCommand:[PBGitBinary path]
123 withArgs:[NSArray arrayWithObjects:@"update-index", @"-q", @"--unmerged", @"--ignore-missing", @"--refresh", nil]
124 inDir:[workingDirectory path]];
127 selector:@selector(indexRefreshFinished:)
128 name:NSFileHandleReadToEndOfFileCompletionNotification
129 object:updateHandle];
130 [updateHandle readToEndOfFileInBackgroundAndNotify];
134 - (NSString *) parentTree
136 NSString *parent = amend ? @"HEAD^" : @"HEAD";
138 if (![repository parseReference:parent])
139 // We don't have a head ref. Return the empty tree.
140 return @"4b825dc642cb6eb9a060e54bf8d69288fbee4904";
145 // TODO: make Asynchronous
146 - (void)commitWithMessage:(NSString *)commitMessage
148 NSMutableString *commitSubject = [@"commit: " mutableCopy];
149 NSRange newLine = [commitMessage rangeOfString:@"\n"];
150 if (newLine.location == NSNotFound)
151 [commitSubject appendString:commitMessage];
153 [commitSubject appendString:[commitMessage substringToIndex:newLine.location]];
155 NSString *commitMessageFile;
156 commitMessageFile = [repository.fileURL.path stringByAppendingPathComponent:@"COMMIT_EDITMSG"];
158 [commitMessage writeToFile:commitMessageFile atomically:YES encoding:NSUTF8StringEncoding error:nil];
161 [self postCommitUpdate:@"Creating tree"];
162 NSString *tree = [repository outputForCommand:@"write-tree"];
163 if ([tree length] != 40)
164 return [self postCommitFailure:@"Creating tree failed"];
167 NSMutableArray *arguments = [NSMutableArray arrayWithObjects:@"commit-tree", tree, nil];
168 NSString *parent = amend ? @"HEAD^" : @"HEAD";
169 if ([repository parseReference:parent]) {
170 [arguments addObject:@"-p"];
171 [arguments addObject:parent];
174 [self postCommitUpdate:@"Creating commit"];
176 NSString *commit = [repository outputForArguments:arguments
177 inputString:commitMessage
178 byExtendingEnvironment:amendEnvironment
181 if (ret || [commit length] != 40)
182 return [self postCommitFailure:@"Could not create a commit object"];
184 [self postCommitUpdate:@"Running hooks"];
185 if (![repository executeHook:@"pre-commit" output:nil])
186 return [self postCommitFailure:@"Pre-commit hook failed"];
188 if (![repository executeHook:@"commit-msg" withArgs:[NSArray arrayWithObject:commitMessageFile] output:nil])
189 return [self postCommitFailure:@"Commit-msg hook failed"];
191 [self postCommitUpdate:@"Updating HEAD"];
192 [repository outputForArguments:[NSArray arrayWithObjects:@"update-ref", @"-m", commitSubject, @"HEAD", commit, nil]
195 return [self postCommitFailure:@"Could not update HEAD"];
197 [self postCommitUpdate:@"Running post-commit hook"];
199 BOOL success = [repository executeHook:@"post-commit" output:nil];
200 NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:[NSNumber numberWithBool:success] forKey:@"success"];
201 NSString *description;
203 description = [NSString stringWithFormat:@"Successfull created commit %@", commit];
205 description = [NSString stringWithFormat:@"Post-commit hook failed, but successfully created commit %@", commit];
207 [userInfo setObject:description forKey:@"description"];
208 [userInfo setObject:commit forKey:@"sha"];
210 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexFinishedCommit
216 repository.hasChanged = YES;
218 amendEnvironment = nil;
226 - (void)postCommitUpdate:(NSString *)update
228 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexCommitStatus
230 userInfo:[NSDictionary dictionaryWithObject:update forKey:@"description"]];
233 - (void)postCommitFailure:(NSString *)reason
235 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexCommitFailed
237 userInfo:[NSDictionary dictionaryWithObject:reason forKey:@"description"]];
241 - (BOOL)stageFiles:(NSArray *)stageFiles
243 // Input string for update-index
244 // This will be a list of filenames that
245 // should be updated. It's similar to
246 // "git add -- <files>
247 NSMutableString *input = [NSMutableString string];
249 for (PBChangedFile *file in stageFiles) {
250 [input appendFormat:@"%@\0", file.path];
254 [repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"--add", @"--remove", @"-z", @"--stdin", nil]
259 // FIXME: failed notification?
260 NSLog(@"Error when updating index. Retvalue: %i", ret);
264 for (PBChangedFile *file in stageFiles)
266 file.hasUnstagedChanges = NO;
267 file.hasStagedChanges = YES;
270 [self postIndexChange];
274 // TODO: Refactor with above. What's a better name for this?
275 - (BOOL)unstageFiles:(NSArray *)unstageFiles
277 NSMutableString *input = [NSMutableString string];
279 for (PBChangedFile *file in unstageFiles) {
280 [input appendString:[file indexInfo]];
284 [repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"-z", @"--index-info", nil]
290 // FIXME: Failed notification
291 NSLog(@"Error when updating index. Retvalue: %i", ret);
295 for (PBChangedFile *file in unstageFiles)
297 file.hasUnstagedChanges = YES;
298 file.hasStagedChanges = NO;
301 [self postIndexChange];
305 - (void)discardChangesForFiles:(NSArray *)discardFiles
307 NSArray *paths = [discardFiles valueForKey:@"path"];
308 NSString *input = [paths componentsJoinedByString:@"\0"];
310 NSArray *arguments = [NSArray arrayWithObjects:@"checkout-index", @"--index", @"--quiet", @"--force", @"-z", @"--stdin", nil];
313 [PBEasyPipe outputForCommand:[PBGitBinary path] withArgs:arguments inDir:[workingDirectory path] inputString:input retValue:&ret];
316 // TODO: Post failed notification
317 // [[commitController.repository windowController] showMessageSheet:@"Discarding changes failed" infoText:[NSString stringWithFormat:@"Discarding changes failed with error code %i", ret]];
321 for (PBChangedFile *file in discardFiles)
322 file.hasUnstagedChanges = NO;
324 [self postIndexChange];
327 - (BOOL)applyPatch:(NSString *)hunk stage:(BOOL)stage reverse:(BOOL)reverse;
329 NSMutableArray *array = [NSMutableArray arrayWithObjects:@"apply", nil];
331 [array addObject:@"--cached"];
333 [array addObject:@"--reverse"];
336 NSString *error = [repository outputForArguments:array
340 // FIXME: show this error, rather than just logging it
342 NSLog(@"Error: %@", error);
346 // TODO: Try to be smarter about what to refresh
352 - (NSString *)diffForFile:(PBChangedFile *)file staged:(BOOL)staged contextLines:(NSUInteger)context
354 NSString *parameter = [NSString stringWithFormat:@"-U%u", context];
356 NSString *indexPath = [@":0:" stringByAppendingString:file.path];
358 if (file.status == NEW)
359 return [repository outputForArguments:[NSArray arrayWithObjects:@"show", indexPath, nil]];
361 return [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-index", parameter, @"--cached", [self parentTree], @"--", file.path, nil]];
365 if (file.status == NEW) {
366 NSStringEncoding encoding;
367 NSError *error = nil;
368 NSString *path = [[repository workingDirectory] stringByAppendingPathComponent:file.path];
369 NSString *contents = [NSString stringWithContentsOfFile:path
370 usedEncoding:&encoding
378 return [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-files", parameter, @"--", file.path, nil]];
381 - (void)postIndexChange
383 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexUpdated
387 # pragma mark WebKit Accessibility
389 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
396 @implementation PBGitIndex (IndexRefreshMethods)
398 - (void)indexRefreshFinished:(NSNotification *)notification
400 if ([(NSNumber *)[(NSDictionary *)[notification userInfo] objectForKey:@"NSFileHandleError"] intValue])
402 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexRefreshFailed
404 userInfo:[NSDictionary dictionaryWithObject:@"update-index failed" forKey:@"description"]];
408 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexRefreshStatus
410 userInfo:[NSDictionary dictionaryWithObject:@"update-index success" forKey:@"description"]];
412 // Now that the index is refreshed, we need to read the information from the index
413 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
415 // Other files (not tracked, not ignored)
416 NSFileHandle *handle = [PBEasyPipe handleForCommand:[PBGitBinary path]
417 withArgs:[NSArray arrayWithObjects:@"ls-files", @"--others", @"--exclude-standard", @"-z", nil]
418 inDir:[workingDirectory path]];
419 [nc addObserver:self selector:@selector(readOtherFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
420 [handle readToEndOfFileInBackgroundAndNotify];
424 handle = [PBEasyPipe handleForCommand:[PBGitBinary path]
425 withArgs:[NSArray arrayWithObjects:@"diff-files", @"-z", nil]
426 inDir:[workingDirectory path]];
427 [nc addObserver:self selector:@selector(readUnstagedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
428 [handle readToEndOfFileInBackgroundAndNotify];
432 handle = [PBEasyPipe handleForCommand:[PBGitBinary path]
433 withArgs:[NSArray arrayWithObjects:@"diff-index", @"--cached", @"-z", [self parentTree], nil]
434 inDir:[workingDirectory path]];
435 [nc addObserver:self selector:@selector(readStagedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
436 [handle readToEndOfFileInBackgroundAndNotify];
440 - (void)readOtherFiles:(NSNotification *)notification
442 NSArray *lines = [self linesFromNotification:notification];
443 NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] initWithCapacity:[lines count]];
444 // Other files are untracked, so we don't have any real index information. Instead, we can just fake it.
445 // The line below is not used at all, as for these files the commitBlob isn't set
446 NSArray *fileStatus = [NSArray arrayWithObjects:@":000000", @"100644", @"0000000000000000000000000000000000000000", @"0000000000000000000000000000000000000000", @"A", nil];
447 for (NSString *path in lines) {
448 if ([path length] == 0)
450 [dictionary setObject:fileStatus forKey:path];
453 [self addFilesFromDictionary:dictionary staged:NO tracked:NO];
454 [self indexStepComplete];
457 - (void) readStagedFiles:(NSNotification *)notification
459 NSArray *lines = [self linesFromNotification:notification];
460 NSMutableDictionary *dic = [self dictionaryForLines:lines];
461 [self addFilesFromDictionary:dic staged:YES tracked:YES];
462 [self indexStepComplete];
465 - (void) readUnstagedFiles:(NSNotification *)notification
467 NSArray *lines = [self linesFromNotification:notification];
468 NSMutableDictionary *dic = [self dictionaryForLines:lines];
469 [self addFilesFromDictionary:dic staged:NO tracked:YES];
470 [self indexStepComplete];
473 - (void) addFilesFromDictionary:(NSMutableDictionary *)dictionary staged:(BOOL)staged tracked:(BOOL)tracked
475 // Iterate over all existing files
476 for (PBChangedFile *file in files) {
477 NSArray *fileStatus = [dictionary objectForKey:file.path];
478 // Object found, this is still a cached / uncached thing
481 NSString *mode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
482 NSString *sha = [fileStatus objectAtIndex:2];
483 file.commitBlobSHA = sha;
484 file.commitBlobMode = mode;
487 file.hasStagedChanges = YES;
489 file.hasUnstagedChanges = YES;
491 // Untracked file, set status to NEW, only unstaged changes
492 file.hasStagedChanges = NO;
493 file.hasUnstagedChanges = YES;
497 // We handled this file, remove it from the dictionary
498 [dictionary removeObjectForKey:file.path];
500 // Object not found in the dictionary, so let's reset its appropriate
501 // change (stage or untracked) if necessary.
503 // Staged dictionary, so file does not have staged changes
505 file.hasStagedChanges = NO;
506 // Tracked file does not have unstaged changes, file is not new,
507 // so we can set it to No. (If it would be new, it would not
508 // be in this dictionary, but in the "other dictionary").
509 else if (tracked && file.status != NEW)
510 file.hasUnstagedChanges = NO;
511 // Unstaged, untracked dictionary ("Other" files), and file
512 // is indicated as new (which would be untracked), so let's
514 else if (!tracked && file.status == NEW)
515 file.hasUnstagedChanges = NO;
519 // Do new files only if necessary
520 if (![[dictionary allKeys] count])
523 // All entries left in the dictionary haven't been accounted for
524 // above, so we need to add them to the "files" array
525 [self willChangeValueForKey:@"indexChanges"];
526 for (NSString *path in [dictionary allKeys]) {
527 NSArray *fileStatus = [dictionary objectForKey:path];
529 PBChangedFile *file = [[PBChangedFile alloc] initWithPath:path];
530 if ([[fileStatus objectAtIndex:4] isEqualToString:@"D"])
531 file.status = DELETED;
532 else if([[fileStatus objectAtIndex:0] isEqualToString:@":000000"])
535 file.status = MODIFIED;
538 file.commitBlobMode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
539 file.commitBlobSHA = [fileStatus objectAtIndex:2];
542 file.hasStagedChanges = staged;
543 file.hasUnstagedChanges = !staged;
545 [files addObject:file];
547 [self didChangeValueForKey:@"indexChanges"];
550 # pragma mark Utility methods
551 - (NSArray *)linesFromNotification:(NSNotification *)notification
553 NSData *data = [[notification userInfo] valueForKey:NSFileHandleNotificationDataItem];
555 return [NSArray array];
557 NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
558 // FIXME: throw an error?
560 return [NSArray array];
562 // Strip trailing null
563 if ([string hasSuffix:@"\0"])
564 string = [string substringToIndex:[string length]-1];
566 if ([string length] == 0)
567 return [NSArray array];
569 return [string componentsSeparatedByString:@"\0"];
572 - (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines
574 NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:[lines count]/2];
576 // Fill the dictionary with the new information. These lines are in the form of:
577 // :00000 :0644 OTHER INDEX INFORMATION
580 NSAssert1([lines count] % 2 == 0, @"Lines must have an even number of lines: %@", lines);
582 NSEnumerator *enumerator = [lines objectEnumerator];
583 NSString *fileStatus;
584 while (fileStatus = [enumerator nextObject]) {
585 NSString *fileName = [enumerator nextObject];
586 [dictionary setObject:[fileStatus componentsSeparatedByString:@" "] forKey:fileName];
592 // This method is called for each of the three processes from above.
593 // If all three are finished (self.busy == 0), then we can delete
594 // all files previously marked as deletable
595 - (void)indexStepComplete
597 // if we're still busy, do nothing :)
598 if (--refreshStatus) {
599 [self postIndexChange];
603 // At this point, all index operations have finished.
604 // We need to find all files that don't have either
605 // staged or unstaged files, and delete them
607 NSMutableArray *deleteFiles = [NSMutableArray array];
608 for (PBChangedFile *file in files) {
609 if (!file.hasStagedChanges && !file.hasUnstagedChanges)
610 [deleteFiles addObject:file];
613 if ([deleteFiles count]) {
614 [self willChangeValueForKey:@"indexChanges"];
615 for (PBChangedFile *file in deleteFiles)
616 [files removeObject:file];
617 [self didChangeValueForKey:@"indexChanges"];
620 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexFinishedIndexRefresh
622 [self postIndexChange];