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";
27 NSString *PBGitIndexOperationFailed = @"PBGitIndexOperationFailed";
29 @interface PBGitIndex (IndexRefreshMethods)
31 - (NSArray *)linesFromNotification:(NSNotification *)notification;
32 - (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines;
33 - (void)addFilesFromDictionary:(NSMutableDictionary *)dictionary staged:(BOOL)staged tracked:(BOOL)tracked;
35 - (void)indexStepComplete;
37 - (void)indexRefreshFinished:(NSNotification *)notification;
38 - (void)readOtherFiles:(NSNotification *)notification;
39 - (void)readUnstagedFiles:(NSNotification *)notification;
40 - (void)readStagedFiles:(NSNotification *)notification;
44 @interface PBGitIndex ()
46 // Returns the tree to compare the index to, based
47 // on whether amend is set or not.
48 - (NSString *) parentTree;
49 - (void)postCommitUpdate:(NSString *)update;
50 - (void)postCommitFailure:(NSString *)reason;
51 - (void)postIndexChange;
52 - (void)postOperationFailed:(NSString *)description;
55 @implementation PBGitIndex
59 - (id)initWithRepository:(PBGitRepository *)theRepository workingDirectory:(NSURL *)theWorkingDirectory
61 if (!(self = [super init]))
64 NSAssert(theWorkingDirectory, @"PBGitIndex requires a working directory");
65 NSAssert(theRepository, @"PBGitIndex requires a repository");
67 repository = theRepository;
68 workingDirectory = theWorkingDirectory;
69 files = [NSMutableArray array];
74 - (NSArray *)indexChanges
79 - (void)setAmend:(BOOL)newAmend
81 if (newAmend == amend)
85 amendEnvironment = nil;
92 // If we amend, we want to keep the author information for the previous commit
93 // We do this by reading in the previous commit, and storing the information
94 // in a dictionary. This dictionary will then later be read by [self commit:]
95 NSString *message = [repository outputForCommand:@"cat-file commit HEAD"];
96 NSArray *match = [message substringsMatchingRegularExpression:@"\nauthor ([^\n]*) <([^\n>]*)> ([0-9]+[^\n]*)\n" count:3 options:0 ranges:nil error:nil];
98 amendEnvironment = [NSDictionary dictionaryWithObjectsAndKeys:[match objectAtIndex:1], @"GIT_AUTHOR_NAME",
99 [match objectAtIndex:2], @"GIT_AUTHOR_EMAIL",
100 [match objectAtIndex:3], @"GIT_AUTHOR_DATE",
103 // Find the commit message
104 NSRange r = [message rangeOfString:@"\n\n"];
105 if (r.location != NSNotFound) {
106 NSString *commitMessage = [message substringFromIndex:r.location + 2];
107 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexAmendMessageAvailable
109 userInfo:[NSDictionary dictionaryWithObject:commitMessage forKey:@"message"]];
116 // If we were already refreshing the index, we don't want
117 // double notifications. As we can't stop the tasks anymore,
118 // just cancel the notifications
120 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
121 [nc removeObserver:self];
123 // Ask Git to refresh the index
124 NSFileHandle *updateHandle = [PBEasyPipe handleForCommand:[PBGitBinary path]
125 withArgs:[NSArray arrayWithObjects:@"update-index", @"-q", @"--unmerged", @"--ignore-missing", @"--refresh", nil]
126 inDir:[workingDirectory path]];
129 selector:@selector(indexRefreshFinished:)
130 name:NSFileHandleReadToEndOfFileCompletionNotification
131 object:updateHandle];
132 [updateHandle readToEndOfFileInBackgroundAndNotify];
136 - (NSString *) parentTree
138 NSString *parent = amend ? @"HEAD^" : @"HEAD";
140 if (![repository parseReference:parent])
141 // We don't have a head ref. Return the empty tree.
142 return @"4b825dc642cb6eb9a060e54bf8d69288fbee4904";
147 // TODO: make Asynchronous
148 - (void)commitWithMessage:(NSString *)commitMessage
150 NSMutableString *commitSubject = [@"commit: " mutableCopy];
151 NSRange newLine = [commitMessage rangeOfString:@"\n"];
152 if (newLine.location == NSNotFound)
153 [commitSubject appendString:commitMessage];
155 [commitSubject appendString:[commitMessage substringToIndex:newLine.location]];
157 NSString *commitMessageFile;
158 commitMessageFile = [repository.fileURL.path stringByAppendingPathComponent:@"COMMIT_EDITMSG"];
160 [commitMessage writeToFile:commitMessageFile atomically:YES encoding:NSUTF8StringEncoding error:nil];
163 [self postCommitUpdate:@"Creating tree"];
164 NSString *tree = [repository outputForCommand:@"write-tree"];
165 if ([tree length] != 40)
166 return [self postCommitFailure:@"Creating tree failed"];
169 NSMutableArray *arguments = [NSMutableArray arrayWithObjects:@"commit-tree", tree, nil];
170 NSString *parent = amend ? @"HEAD^" : @"HEAD";
171 if ([repository parseReference:parent]) {
172 [arguments addObject:@"-p"];
173 [arguments addObject:parent];
176 [self postCommitUpdate:@"Creating commit"];
178 NSString *commit = [repository outputForArguments:arguments
179 inputString:commitMessage
180 byExtendingEnvironment:amendEnvironment
183 if (ret || [commit length] != 40)
184 return [self postCommitFailure:@"Could not create a commit object"];
186 [self postCommitUpdate:@"Running hooks"];
187 if (![repository executeHook:@"pre-commit" output:nil])
188 return [self postCommitFailure:@"Pre-commit hook failed"];
190 if (![repository executeHook:@"commit-msg" withArgs:[NSArray arrayWithObject:commitMessageFile] output:nil])
191 return [self postCommitFailure:@"Commit-msg hook failed"];
193 [self postCommitUpdate:@"Updating HEAD"];
194 [repository outputForArguments:[NSArray arrayWithObjects:@"update-ref", @"-m", commitSubject, @"HEAD", commit, nil]
197 return [self postCommitFailure:@"Could not update HEAD"];
199 [self postCommitUpdate:@"Running post-commit hook"];
201 BOOL success = [repository executeHook:@"post-commit" output:nil];
202 NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:[NSNumber numberWithBool:success] forKey:@"success"];
203 NSString *description;
205 description = [NSString stringWithFormat:@"Successfull created commit %@", commit];
207 description = [NSString stringWithFormat:@"Post-commit hook failed, but successfully created commit %@", commit];
209 [userInfo setObject:description forKey:@"description"];
210 [userInfo setObject:commit forKey:@"sha"];
212 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexFinishedCommit
218 repository.hasChanged = YES;
220 amendEnvironment = nil;
228 - (void)postCommitUpdate:(NSString *)update
230 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexCommitStatus
232 userInfo:[NSDictionary dictionaryWithObject:update forKey:@"description"]];
235 - (void)postCommitFailure:(NSString *)reason
237 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexCommitFailed
239 userInfo:[NSDictionary dictionaryWithObject:reason forKey:@"description"]];
242 - (void)postOperationFailed:(NSString *)description
244 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexOperationFailed
246 userInfo:[NSDictionary dictionaryWithObject:description forKey:@"description"]];
249 - (BOOL)stageFiles:(NSArray *)stageFiles
251 // Input string for update-index
252 // This will be a list of filenames that
253 // should be updated. It's similar to
254 // "git add -- <files>
255 NSMutableString *input = [NSMutableString string];
257 for (PBChangedFile *file in stageFiles) {
258 [input appendFormat:@"%@\0", file.path];
262 [repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"--add", @"--remove", @"-z", @"--stdin", nil]
267 [self postOperationFailed:[NSString stringWithFormat:@"Error in staging files. Return value: %i", ret]];
271 for (PBChangedFile *file in stageFiles)
273 file.hasUnstagedChanges = NO;
274 file.hasStagedChanges = YES;
277 [self postIndexChange];
281 // TODO: Refactor with above. What's a better name for this?
282 - (BOOL)unstageFiles:(NSArray *)unstageFiles
284 NSMutableString *input = [NSMutableString string];
286 for (PBChangedFile *file in unstageFiles) {
287 [input appendString:[file indexInfo]];
291 [repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"-z", @"--index-info", nil]
297 [self postOperationFailed:[NSString stringWithFormat:@"Error in unstaging files. Return value: %i", ret]];
301 for (PBChangedFile *file in unstageFiles)
303 file.hasUnstagedChanges = YES;
304 file.hasStagedChanges = NO;
307 [self postIndexChange];
311 - (void)discardChangesForFiles:(NSArray *)discardFiles
313 NSArray *paths = [discardFiles valueForKey:@"path"];
314 NSString *input = [paths componentsJoinedByString:@"\0"];
316 NSArray *arguments = [NSArray arrayWithObjects:@"checkout-index", @"--index", @"--quiet", @"--force", @"-z", @"--stdin", nil];
319 [PBEasyPipe outputForCommand:[PBGitBinary path] withArgs:arguments inDir:[workingDirectory path] inputString:input retValue:&ret];
322 [self postOperationFailed:[NSString stringWithFormat:@"Discarding changes failed with return value %i", ret]];
326 for (PBChangedFile *file in discardFiles)
327 file.hasUnstagedChanges = NO;
329 [self postIndexChange];
332 - (BOOL)applyPatch:(NSString *)hunk stage:(BOOL)stage reverse:(BOOL)reverse;
334 NSMutableArray *array = [NSMutableArray arrayWithObjects:@"apply", nil];
336 [array addObject:@"--cached"];
338 [array addObject:@"--reverse"];
341 NSString *error = [repository outputForArguments:array
346 [self postOperationFailed:[NSString stringWithFormat:@"Applying patch failed with return value %i. Error: %@", ret, error]];
350 // TODO: Try to be smarter about what to refresh
356 - (NSString *)diffForFile:(PBChangedFile *)file staged:(BOOL)staged contextLines:(NSUInteger)context
358 NSString *parameter = [NSString stringWithFormat:@"-U%u", context];
360 NSString *indexPath = [@":0:" stringByAppendingString:file.path];
362 if (file.status == NEW)
363 return [repository outputForArguments:[NSArray arrayWithObjects:@"show", indexPath, nil]];
365 return [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-index", parameter, @"--cached", [self parentTree], @"--", file.path, nil]];
369 if (file.status == NEW) {
370 NSStringEncoding encoding;
371 NSError *error = nil;
372 NSString *path = [[repository workingDirectory] stringByAppendingPathComponent:file.path];
373 NSString *contents = [NSString stringWithContentsOfFile:path
374 usedEncoding:&encoding
382 return [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-files", parameter, @"--", file.path, nil]];
385 - (void)postIndexChange
387 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexUpdated
391 # pragma mark WebKit Accessibility
393 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
400 @implementation PBGitIndex (IndexRefreshMethods)
402 - (void)indexRefreshFinished:(NSNotification *)notification
404 if ([(NSNumber *)[(NSDictionary *)[notification userInfo] objectForKey:@"NSFileHandleError"] intValue])
406 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexRefreshFailed
408 userInfo:[NSDictionary dictionaryWithObject:@"update-index failed" forKey:@"description"]];
412 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexRefreshStatus
414 userInfo:[NSDictionary dictionaryWithObject:@"update-index success" forKey:@"description"]];
416 // Now that the index is refreshed, we need to read the information from the index
417 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
419 // Other files (not tracked, not ignored)
421 NSFileHandle *handle = [PBEasyPipe handleForCommand:[PBGitBinary path]
422 withArgs:[NSArray arrayWithObjects:@"ls-files", @"--others", @"--exclude-standard", @"-z", nil]
423 inDir:[workingDirectory path]];
424 [nc addObserver:self selector:@selector(readOtherFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
425 [handle readToEndOfFileInBackgroundAndNotify];
429 handle = [PBEasyPipe handleForCommand:[PBGitBinary path]
430 withArgs:[NSArray arrayWithObjects:@"diff-files", @"-z", nil]
431 inDir:[workingDirectory path]];
432 [nc addObserver:self selector:@selector(readUnstagedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
433 [handle readToEndOfFileInBackgroundAndNotify];
437 handle = [PBEasyPipe handleForCommand:[PBGitBinary path]
438 withArgs:[NSArray arrayWithObjects:@"diff-index", @"--cached", @"-z", [self parentTree], nil]
439 inDir:[workingDirectory path]];
440 [nc addObserver:self selector:@selector(readStagedFiles:) name:NSFileHandleReadToEndOfFileCompletionNotification object:handle];
441 [handle readToEndOfFileInBackgroundAndNotify];
444 - (void)readOtherFiles:(NSNotification *)notification
446 NSArray *lines = [self linesFromNotification:notification];
447 NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] initWithCapacity:[lines count]];
448 // Other files are untracked, so we don't have any real index information. Instead, we can just fake it.
449 // The line below is not used at all, as for these files the commitBlob isn't set
450 NSArray *fileStatus = [NSArray arrayWithObjects:@":000000", @"100644", @"0000000000000000000000000000000000000000", @"0000000000000000000000000000000000000000", @"A", nil];
451 for (NSString *path in lines) {
452 if ([path length] == 0)
454 [dictionary setObject:fileStatus forKey:path];
457 [self addFilesFromDictionary:dictionary staged:NO tracked:NO];
458 [self indexStepComplete];
461 - (void) readStagedFiles:(NSNotification *)notification
463 NSArray *lines = [self linesFromNotification:notification];
464 NSMutableDictionary *dic = [self dictionaryForLines:lines];
465 [self addFilesFromDictionary:dic staged:YES tracked:YES];
466 [self indexStepComplete];
469 - (void) readUnstagedFiles:(NSNotification *)notification
471 NSArray *lines = [self linesFromNotification:notification];
472 NSMutableDictionary *dic = [self dictionaryForLines:lines];
473 [self addFilesFromDictionary:dic staged:NO tracked:YES];
474 [self indexStepComplete];
477 - (void) addFilesFromDictionary:(NSMutableDictionary *)dictionary staged:(BOOL)staged tracked:(BOOL)tracked
479 // Iterate over all existing files
480 for (PBChangedFile *file in files) {
481 NSArray *fileStatus = [dictionary objectForKey:file.path];
482 // Object found, this is still a cached / uncached thing
485 NSString *mode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
486 NSString *sha = [fileStatus objectAtIndex:2];
487 file.commitBlobSHA = sha;
488 file.commitBlobMode = mode;
491 file.hasStagedChanges = YES;
493 file.hasUnstagedChanges = YES;
495 // Untracked file, set status to NEW, only unstaged changes
496 file.hasStagedChanges = NO;
497 file.hasUnstagedChanges = YES;
501 // We handled this file, remove it from the dictionary
502 [dictionary removeObjectForKey:file.path];
504 // Object not found in the dictionary, so let's reset its appropriate
505 // change (stage or untracked) if necessary.
507 // Staged dictionary, so file does not have staged changes
509 file.hasStagedChanges = NO;
510 // Tracked file does not have unstaged changes, file is not new,
511 // so we can set it to No. (If it would be new, it would not
512 // be in this dictionary, but in the "other dictionary").
513 else if (tracked && file.status != NEW)
514 file.hasUnstagedChanges = NO;
515 // Unstaged, untracked dictionary ("Other" files), and file
516 // is indicated as new (which would be untracked), so let's
518 else if (!tracked && file.status == NEW)
519 file.hasUnstagedChanges = NO;
523 // Do new files only if necessary
524 if (![[dictionary allKeys] count])
527 // All entries left in the dictionary haven't been accounted for
528 // above, so we need to add them to the "files" array
529 [self willChangeValueForKey:@"indexChanges"];
530 for (NSString *path in [dictionary allKeys]) {
531 NSArray *fileStatus = [dictionary objectForKey:path];
533 PBChangedFile *file = [[PBChangedFile alloc] initWithPath:path];
534 if ([[fileStatus objectAtIndex:4] isEqualToString:@"D"])
535 file.status = DELETED;
536 else if([[fileStatus objectAtIndex:0] isEqualToString:@":000000"])
539 file.status = MODIFIED;
542 file.commitBlobMode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
543 file.commitBlobSHA = [fileStatus objectAtIndex:2];
546 file.hasStagedChanges = staged;
547 file.hasUnstagedChanges = !staged;
549 [files addObject:file];
551 [self didChangeValueForKey:@"indexChanges"];
554 # pragma mark Utility methods
555 - (NSArray *)linesFromNotification:(NSNotification *)notification
557 NSData *data = [[notification userInfo] valueForKey:NSFileHandleNotificationDataItem];
559 return [NSArray array];
561 NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
562 // FIXME: throw an error?
564 return [NSArray array];
566 // Strip trailing null
567 if ([string hasSuffix:@"\0"])
568 string = [string substringToIndex:[string length]-1];
570 if ([string length] == 0)
571 return [NSArray array];
573 return [string componentsSeparatedByString:@"\0"];
576 - (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines
578 NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:[lines count]/2];
580 // Fill the dictionary with the new information. These lines are in the form of:
581 // :00000 :0644 OTHER INDEX INFORMATION
584 NSAssert1([lines count] % 2 == 0, @"Lines must have an even number of lines: %@", lines);
586 NSEnumerator *enumerator = [lines objectEnumerator];
587 NSString *fileStatus;
588 while (fileStatus = [enumerator nextObject]) {
589 NSString *fileName = [enumerator nextObject];
590 [dictionary setObject:[fileStatus componentsSeparatedByString:@" "] forKey:fileName];
596 // This method is called for each of the three processes from above.
597 // If all three are finished (self.busy == 0), then we can delete
598 // all files previously marked as deletable
599 - (void)indexStepComplete
601 // if we're still busy, do nothing :)
602 if (--refreshStatus) {
603 [self postIndexChange];
607 // At this point, all index operations have finished.
608 // We need to find all files that don't have either
609 // staged or unstaged files, and delete them
611 NSMutableArray *deleteFiles = [NSMutableArray array];
612 for (PBChangedFile *file in files) {
613 if (!file.hasStagedChanges && !file.hasUnstagedChanges)
614 [deleteFiles addObject:file];
617 if ([deleteFiles count]) {
618 [self willChangeValueForKey:@"indexChanges"];
619 for (PBChangedFile *file in deleteFiles)
620 [files removeObject:file];
621 [self didChangeValueForKey:@"indexChanges"];
624 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexFinishedIndexRefresh
626 [self postIndexChange];