GitIndexController: reorder methods a bit, remove unnecessary stuff
[GitX.git] / PBGitIndex.m
blobd3d0263986a98a266f15b3a5791cc6c5922c10d5
1 //
2 //  PBGitIndex.m
3 //  GitX
4 //
5 //  Created by Pieter de Bie on 9/12/09.
6 //  Copyright 2009 Pieter de Bie. All rights reserved.
7 //
9 #import "PBGitIndex.h"
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;
41 @end
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;
51 @end
53 @implementation PBGitIndex
55 @synthesize amend;
57 - (id)initWithRepository:(PBGitRepository *)theRepository workingDirectory:(NSURL *)theWorkingDirectory
59         if (!(self = [super init]))
60                 return nil;
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];
69         return self;
72 - (NSArray *)indexChanges
74         return files;
77 - (void)setAmend:(BOOL)newAmend
79         if (newAmend == amend)
80                 return;
81         
82         amend = newAmend;
83         amendEnvironment = nil;
85         [self refresh];
87         if (!newAmend)
88                 return;
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];
95         if (match)
96                 amendEnvironment = [NSDictionary dictionaryWithObjectsAndKeys:[match objectAtIndex:1], @"GIT_AUTHOR_NAME",
97                                                         [match objectAtIndex:2], @"GIT_AUTHOR_EMAIL",
98                                                         [match objectAtIndex:3], @"GIT_AUTHOR_DATE",
99                                                         nil];
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
106                                                                                                                         object: self
107                                                                                                                   userInfo:[NSDictionary dictionaryWithObject:commitMessage forKey:@"message"]];
108         }
109         
112 - (void)refresh
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
117         refreshStatus = 0;
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]];
126         [nc addObserver:self
127                    selector:@selector(indexRefreshFinished:)
128                            name:NSFileHandleReadToEndOfFileCompletionNotification
129                          object:updateHandle];
130         [updateHandle readToEndOfFileInBackgroundAndNotify];
134 - (NSString *) parentTree
136         NSString *parent = amend ? @"HEAD^" : @"HEAD";
137         
138         if (![repository parseReference:parent])
139                 // We don't have a head ref. Return the empty tree.
140                 return @"4b825dc642cb6eb9a060e54bf8d69288fbee4904";
142         return parent;
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];
152         else
153                 [commitSubject appendString:[commitMessage substringToIndex:newLine.location]];
154         
155         NSString *commitMessageFile;
156         commitMessageFile = [repository.fileURL.path stringByAppendingPathComponent:@"COMMIT_EDITMSG"];
157         
158         [commitMessage writeToFile:commitMessageFile atomically:YES encoding:NSUTF8StringEncoding error:nil];
160         
161         [self postCommitUpdate:@"Creating tree"];
162         NSString *tree = [repository outputForCommand:@"write-tree"];
163         if ([tree length] != 40)
164                 return [self postCommitFailure:@"Creating tree failed"];
165         
166         
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];
172         }
174         [self postCommitUpdate:@"Creating commit"];
175         int ret = 1;
176         NSString *commit = [repository outputForArguments:arguments
177                                                                                   inputString:commitMessage
178                                                            byExtendingEnvironment:amendEnvironment
179                                                                                          retValue: &ret];
180         
181         if (ret || [commit length] != 40)
182                 return [self postCommitFailure:@"Could not create a commit object"];
183         
184         [self postCommitUpdate:@"Running hooks"];
185         if (![repository executeHook:@"pre-commit" output:nil])
186                 return [self postCommitFailure:@"Pre-commit hook failed"];
187         
188         if (![repository executeHook:@"commit-msg" withArgs:[NSArray arrayWithObject:commitMessageFile] output:nil])
189                 return [self postCommitFailure:@"Commit-msg hook failed"];
190         
191         [self postCommitUpdate:@"Updating HEAD"];
192         [repository outputForArguments:[NSArray arrayWithObjects:@"update-ref", @"-m", commitSubject, @"HEAD", commit, nil]
193                                                   retValue: &ret];
194         if (ret)
195                 return [self postCommitFailure:@"Could not update HEAD"];
196         
197         [self postCommitUpdate:@"Running post-commit hook"];
198         
199         BOOL success = [repository executeHook:@"post-commit" output:nil];
200         NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:[NSNumber numberWithBool:success] forKey:@"success"];
201         NSString *description;  
202         if (success)
203                 description = [NSString stringWithFormat:@"Successfull created commit %@", commit];
204         else
205                 description = [NSString stringWithFormat:@"Post-commit hook failed, but successfully created commit %@", commit];
206         
207         [userInfo setObject:description forKey:@"description"];
208         [userInfo setObject:commit forKey:@"sha"];
210         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexFinishedCommit
211                                                                                                                 object:self
212                                                                                                           userInfo:userInfo];
213         if (!success)
214                 return;
216         repository.hasChanged = YES;
218         amendEnvironment = nil;
219         if (amend)
220                 self.amend = NO;
221         else
222                 [self refresh];
223         
226 - (void)postCommitUpdate:(NSString *)update
228         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexCommitStatus
229                                                                                                         object:self
230                                                                                                           userInfo:[NSDictionary dictionaryWithObject:update forKey:@"description"]];
233 - (void)postCommitFailure:(NSString *)reason
235         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexCommitFailed
236                                                                                                                 object:self
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];
251         }
252         
253         int ret = 1;
254         [repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"--add", @"--remove", @"-z", @"--stdin", nil]
255                                            inputString:input
256                                                   retValue:&ret];
258         if (ret) {
259                 // FIXME: failed notification?
260                 NSLog(@"Error when updating index. Retvalue: %i", ret);
261                 return NO;
262         }
264         for (PBChangedFile *file in stageFiles)
265         {
266                 file.hasUnstagedChanges = NO;
267                 file.hasStagedChanges = YES;
268         }
270         [self postIndexChange];
271         return YES;
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]];
281         }
283         int ret = 1;
284         [repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"-z", @"--index-info", nil]
285                                            inputString:input 
286                                                   retValue:&ret];
288         if (ret)
289         {
290                 // FIXME: Failed notification
291                 NSLog(@"Error when updating index. Retvalue: %i", ret);
292                 return NO;
293         }
295         for (PBChangedFile *file in unstageFiles)
296         {
297                 file.hasUnstagedChanges = YES;
298                 file.hasStagedChanges = NO;
299         }
301         [self postIndexChange];
302         return YES;
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];
312         int ret = 1;
313         [PBEasyPipe outputForCommand:[PBGitBinary path] withArgs:arguments inDir:[workingDirectory path] inputString:input retValue:&ret];
315         if (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]];
318                 return;
319         }
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];
330         if (stage)
331                 [array addObject:@"--cached"];
332         if (reverse)
333                 [array addObject:@"--reverse"];
335         int ret = 1;
336         NSString *error = [repository outputForArguments:array
337                                                                                  inputString:hunk
338                                                                                         retValue:&ret];
340         // FIXME: show this error, rather than just logging it
341         if (ret) {
342                 NSLog(@"Error: %@", error);
343                 return NO;
344         }
346         // TODO: Try to be smarter about what to refresh
347         [self refresh];
348         return YES;
352 - (NSString *)diffForFile:(PBChangedFile *)file staged:(BOOL)staged contextLines:(NSUInteger)context
354         NSString *parameter = [NSString stringWithFormat:@"-U%u", context];
355         if (staged) {
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]];
362         }
364         // unstaged
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
371                                                                                                                   error:&error];
372                 if (error)
373                         return nil;
375                 return contents;
376         }
378         return [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-files", parameter, @"--", file.path, nil]];
381 - (void)postIndexChange
383         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexUpdated
384                                                                                                                 object:self];
387 # pragma mark WebKit Accessibility
389 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
391         return NO;
394 @end
396 @implementation PBGitIndex (IndexRefreshMethods)
398 - (void)indexRefreshFinished:(NSNotification *)notification
400         if ([(NSNumber *)[(NSDictionary *)[notification userInfo] objectForKey:@"NSFileHandleError"] intValue])
401         {
402                 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexRefreshFailed
403                                                                                                                         object:self
404                                                                                                                   userInfo:[NSDictionary dictionaryWithObject:@"update-index failed" forKey:@"description"]];
405                 return;
406         }
408         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexRefreshStatus
409                                                                                                                 object:self
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];
421         refreshStatus++;
423         // Unstaged files
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];
429         refreshStatus++;
431         // Staged files
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];
437         refreshStatus++;
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)
449                         continue;
450                 [dictionary setObject:fileStatus forKey:path];
451         }
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
479                 if (fileStatus) {
480                         if (tracked) {
481                                 NSString *mode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
482                                 NSString *sha = [fileStatus objectAtIndex:2];
483                                 file.commitBlobSHA = sha;
484                                 file.commitBlobMode = mode;
485                                 
486                                 if (staged)
487                                         file.hasStagedChanges = YES;
488                                 else
489                                         file.hasUnstagedChanges = YES;
490                         } else {
491                                 // Untracked file, set status to NEW, only unstaged changes
492                                 file.hasStagedChanges = NO;
493                                 file.hasUnstagedChanges = YES;
494                                 file.status = NEW;
495                         }
497                         // We handled this file, remove it from the dictionary
498                         [dictionary removeObjectForKey:file.path];
499                 } else {
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
504                         if (staged)
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
513                         // remove it
514                         else if (!tracked && file.status == NEW)
515                                 file.hasUnstagedChanges = NO;
516                 }
517         }
519         // Do new files only if necessary
520         if (![[dictionary allKeys] count])
521                 return;
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"])
533                         file.status = NEW;
534                 else
535                         file.status = MODIFIED;
537                 if (tracked) {
538                         file.commitBlobMode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
539                         file.commitBlobSHA = [fileStatus objectAtIndex:2];
540                 }
542                 file.hasStagedChanges = staged;
543                 file.hasUnstagedChanges = !staged;
545                 [files addObject:file];
546         }
547         [self didChangeValueForKey:@"indexChanges"];
550 # pragma mark Utility methods
551 - (NSArray *)linesFromNotification:(NSNotification *)notification
553         NSData *data = [[notification userInfo] valueForKey:NSFileHandleNotificationDataItem];
554         if (!data)
555                 return [NSArray array];
557         NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
558         // FIXME: throw an error?
559         if (!string)
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];
575         
576         // Fill the dictionary with the new information. These lines are in the form of:
577         // :00000 :0644 OTHER INDEX INFORMATION
578         // Filename
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];
587         }
589         return dictionary;
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];
600                 return;
601         }
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];
611         }
612         
613         if ([deleteFiles count]) {
614                 [self willChangeValueForKey:@"indexChanges"];
615                 for (PBChangedFile *file in deleteFiles)
616                         [files removeObject:file];
617                 [self didChangeValueForKey:@"indexChanges"];
618         }
620         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexFinishedIndexRefresh
621                                                                                                                 object:self];
622         [self postIndexChange];
626 @end