Testing: add missing file
[GitX.git] / PBGitIndex.m
blobbd9bd561af077d8feea98866368a46931b249425
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";
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;
42 @end
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;
53 @end
55 @implementation PBGitIndex
57 @synthesize amend;
59 - (id)initWithRepository:(PBGitRepository *)theRepository workingDirectory:(NSURL *)theWorkingDirectory
61         if (!(self = [super init]))
62                 return nil;
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];
71         return self;
74 - (NSArray *)indexChanges
76         return files;
79 - (void)setAmend:(BOOL)newAmend
81         if (newAmend == amend)
82                 return;
83         
84         amend = newAmend;
85         amendEnvironment = nil;
87         [self refresh];
89         if (!newAmend)
90                 return;
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];
97         if (match)
98                 amendEnvironment = [NSDictionary dictionaryWithObjectsAndKeys:[match objectAtIndex:1], @"GIT_AUTHOR_NAME",
99                                                         [match objectAtIndex:2], @"GIT_AUTHOR_EMAIL",
100                                                         [match objectAtIndex:3], @"GIT_AUTHOR_DATE",
101                                                         nil];
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
108                                                                                                                         object: self
109                                                                                                                   userInfo:[NSDictionary dictionaryWithObject:commitMessage forKey:@"message"]];
110         }
111         
114 - (void)refresh
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
119         refreshStatus = 0;
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]];
128         [nc addObserver:self
129                    selector:@selector(indexRefreshFinished:)
130                            name:NSFileHandleReadToEndOfFileCompletionNotification
131                          object:updateHandle];
132         [updateHandle readToEndOfFileInBackgroundAndNotify];
136 - (NSString *) parentTree
138         NSString *parent = amend ? @"HEAD^" : @"HEAD";
139         
140         if (![repository parseReference:parent])
141                 // We don't have a head ref. Return the empty tree.
142                 return @"4b825dc642cb6eb9a060e54bf8d69288fbee4904";
144         return parent;
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];
154         else
155                 [commitSubject appendString:[commitMessage substringToIndex:newLine.location]];
156         
157         NSString *commitMessageFile;
158         commitMessageFile = [repository.fileURL.path stringByAppendingPathComponent:@"COMMIT_EDITMSG"];
159         
160         [commitMessage writeToFile:commitMessageFile atomically:YES encoding:NSUTF8StringEncoding error:nil];
162         
163         [self postCommitUpdate:@"Creating tree"];
164         NSString *tree = [repository outputForCommand:@"write-tree"];
165         if ([tree length] != 40)
166                 return [self postCommitFailure:@"Creating tree failed"];
167         
168         
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];
174         }
176         [self postCommitUpdate:@"Creating commit"];
177         int ret = 1;
178         NSString *commit = [repository outputForArguments:arguments
179                                                                                   inputString:commitMessage
180                                                            byExtendingEnvironment:amendEnvironment
181                                                                                          retValue: &ret];
182         
183         if (ret || [commit length] != 40)
184                 return [self postCommitFailure:@"Could not create a commit object"];
185         
186         [self postCommitUpdate:@"Running hooks"];
187         if (![repository executeHook:@"pre-commit" output:nil])
188                 return [self postCommitFailure:@"Pre-commit hook failed"];
189         
190         if (![repository executeHook:@"commit-msg" withArgs:[NSArray arrayWithObject:commitMessageFile] output:nil])
191                 return [self postCommitFailure:@"Commit-msg hook failed"];
192         
193         [self postCommitUpdate:@"Updating HEAD"];
194         [repository outputForArguments:[NSArray arrayWithObjects:@"update-ref", @"-m", commitSubject, @"HEAD", commit, nil]
195                                                   retValue: &ret];
196         if (ret)
197                 return [self postCommitFailure:@"Could not update HEAD"];
198         
199         [self postCommitUpdate:@"Running post-commit hook"];
200         
201         BOOL success = [repository executeHook:@"post-commit" output:nil];
202         NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:[NSNumber numberWithBool:success] forKey:@"success"];
203         NSString *description;  
204         if (success)
205                 description = [NSString stringWithFormat:@"Successfull created commit %@", commit];
206         else
207                 description = [NSString stringWithFormat:@"Post-commit hook failed, but successfully created commit %@", commit];
208         
209         [userInfo setObject:description forKey:@"description"];
210         [userInfo setObject:commit forKey:@"sha"];
212         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexFinishedCommit
213                                                                                                                 object:self
214                                                                                                           userInfo:userInfo];
215         if (!success)
216                 return;
218         repository.hasChanged = YES;
220         amendEnvironment = nil;
221         if (amend)
222                 self.amend = NO;
223         else
224                 [self refresh];
225         
228 - (void)postCommitUpdate:(NSString *)update
230         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexCommitStatus
231                                                                                                         object:self
232                                                                                                           userInfo:[NSDictionary dictionaryWithObject:update forKey:@"description"]];
235 - (void)postCommitFailure:(NSString *)reason
237         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexCommitFailed
238                                                                                                                 object:self
239                                                                                                           userInfo:[NSDictionary dictionaryWithObject:reason forKey:@"description"]];
242 - (void)postOperationFailed:(NSString *)description
244         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexOperationFailed
245                                                                                                                 object:self
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];
259         }
260         
261         int ret = 1;
262         [repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"--add", @"--remove", @"-z", @"--stdin", nil]
263                                            inputString:input
264                                                   retValue:&ret];
266         if (ret) {
267                 [self postOperationFailed:[NSString stringWithFormat:@"Error in staging files. Return value: %i", ret]];
268                 return NO;
269         }
271         for (PBChangedFile *file in stageFiles)
272         {
273                 file.hasUnstagedChanges = NO;
274                 file.hasStagedChanges = YES;
275         }
277         [self postIndexChange];
278         return YES;
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]];
288         }
290         int ret = 1;
291         [repository outputForArguments:[NSArray arrayWithObjects:@"update-index", @"-z", @"--index-info", nil]
292                                            inputString:input 
293                                                   retValue:&ret];
295         if (ret)
296         {
297                 [self postOperationFailed:[NSString stringWithFormat:@"Error in unstaging files. Return value: %i", ret]];
298                 return NO;
299         }
301         for (PBChangedFile *file in unstageFiles)
302         {
303                 file.hasUnstagedChanges = YES;
304                 file.hasStagedChanges = NO;
305         }
307         [self postIndexChange];
308         return YES;
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];
318         int ret = 1;
319         [PBEasyPipe outputForCommand:[PBGitBinary path] withArgs:arguments inDir:[workingDirectory path] inputString:input retValue:&ret];
321         if (ret) {
322                 [self postOperationFailed:[NSString stringWithFormat:@"Discarding changes failed with return value %i", ret]];
323                 return;
324         }
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];
335         if (stage)
336                 [array addObject:@"--cached"];
337         if (reverse)
338                 [array addObject:@"--reverse"];
340         int ret = 1;
341         NSString *error = [repository outputForArguments:array
342                                                                                  inputString:hunk
343                                                                                         retValue:&ret];
345         if (ret) {
346                 [self postOperationFailed:[NSString stringWithFormat:@"Applying patch failed with return value %i. Error: %@", ret, error]];
347                 return NO;
348         }
350         // TODO: Try to be smarter about what to refresh
351         [self refresh];
352         return YES;
356 - (NSString *)diffForFile:(PBChangedFile *)file staged:(BOOL)staged contextLines:(NSUInteger)context
358         NSString *parameter = [NSString stringWithFormat:@"-U%u", context];
359         if (staged) {
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]];
366         }
368         // unstaged
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
375                                                                                                                   error:&error];
376                 if (error)
377                         return nil;
379                 return contents;
380         }
382         return [repository outputInWorkdirForArguments:[NSArray arrayWithObjects:@"diff-files", parameter, @"--", file.path, nil]];
385 - (void)postIndexChange
387         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexUpdated
388                                                                                                                 object:self];
391 # pragma mark WebKit Accessibility
393 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
395         return NO;
398 @end
400 @implementation PBGitIndex (IndexRefreshMethods)
402 - (void)indexRefreshFinished:(NSNotification *)notification
404         if ([(NSNumber *)[(NSDictionary *)[notification userInfo] objectForKey:@"NSFileHandleError"] intValue])
405         {
406                 [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexRefreshFailed
407                                                                                                                         object:self
408                                                                                                                   userInfo:[NSDictionary dictionaryWithObject:@"update-index failed" forKey:@"description"]];
409                 return;
410         }
412         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexIndexRefreshStatus
413                                                                                                                 object:self
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)
420         refreshStatus++;
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];
427         // Unstaged files
428         refreshStatus++;
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];
435         // Staged files
436         refreshStatus++;
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)
453                         continue;
454                 [dictionary setObject:fileStatus forKey:path];
455         }
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
483                 if (fileStatus) {
484                         if (tracked) {
485                                 NSString *mode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
486                                 NSString *sha = [fileStatus objectAtIndex:2];
487                                 file.commitBlobSHA = sha;
488                                 file.commitBlobMode = mode;
489                                 
490                                 if (staged)
491                                         file.hasStagedChanges = YES;
492                                 else
493                                         file.hasUnstagedChanges = YES;
494                                 if ([[fileStatus objectAtIndex:4] isEqualToString:@"D"])
495                                         file.status = DELETED;
496                         } else {
497                                 // Untracked file, set status to NEW, only unstaged changes
498                                 file.hasStagedChanges = NO;
499                                 file.hasUnstagedChanges = YES;
500                                 file.status = NEW;
501                         }
503                         // We handled this file, remove it from the dictionary
504                         [dictionary removeObjectForKey:file.path];
505                 } else {
506                         // Object not found in the dictionary, so let's reset its appropriate
507                         // change (stage or untracked) if necessary.
509                         // Staged dictionary, so file does not have staged changes
510                         if (staged)
511                                 file.hasStagedChanges = NO;
512                         // Tracked file does not have unstaged changes, file is not new,
513                         // so we can set it to No. (If it would be new, it would not
514                         // be in this dictionary, but in the "other dictionary").
515                         else if (tracked && file.status != NEW)
516                                 file.hasUnstagedChanges = NO;
517                         // Unstaged, untracked dictionary ("Other" files), and file
518                         // is indicated as new (which would be untracked), so let's
519                         // remove it
520                         else if (!tracked && file.status == NEW)
521                                 file.hasUnstagedChanges = NO;
522                 }
523         }
525         // Do new files only if necessary
526         if (![[dictionary allKeys] count])
527                 return;
529         // All entries left in the dictionary haven't been accounted for
530         // above, so we need to add them to the "files" array
531         [self willChangeValueForKey:@"indexChanges"];
532         for (NSString *path in [dictionary allKeys]) {
533                 NSArray *fileStatus = [dictionary objectForKey:path];
535                 PBChangedFile *file = [[PBChangedFile alloc] initWithPath:path];
536                 if ([[fileStatus objectAtIndex:4] isEqualToString:@"D"])
537                         file.status = DELETED;
538                 else if([[fileStatus objectAtIndex:0] isEqualToString:@":000000"])
539                         file.status = NEW;
540                 else
541                         file.status = MODIFIED;
543                 if (tracked) {
544                         file.commitBlobMode = [[fileStatus objectAtIndex:0] substringFromIndex:1];
545                         file.commitBlobSHA = [fileStatus objectAtIndex:2];
546                 }
548                 file.hasStagedChanges = staged;
549                 file.hasUnstagedChanges = !staged;
551                 [files addObject:file];
552         }
553         [self didChangeValueForKey:@"indexChanges"];
556 # pragma mark Utility methods
557 - (NSArray *)linesFromNotification:(NSNotification *)notification
559         NSData *data = [[notification userInfo] valueForKey:NSFileHandleNotificationDataItem];
560         if (!data)
561                 return [NSArray array];
563         NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
564         // FIXME: throw an error?
565         if (!string)
566                 return [NSArray array];
568         // Strip trailing null
569         if ([string hasSuffix:@"\0"])
570                 string = [string substringToIndex:[string length]-1];
572         if ([string length] == 0)
573                 return [NSArray array];
575         return [string componentsSeparatedByString:@"\0"];
578 - (NSMutableDictionary *)dictionaryForLines:(NSArray *)lines
580         NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:[lines count]/2];
581         
582         // Fill the dictionary with the new information. These lines are in the form of:
583         // :00000 :0644 OTHER INDEX INFORMATION
584         // Filename
586         NSAssert1([lines count] % 2 == 0, @"Lines must have an even number of lines: %@", lines);
588         NSEnumerator *enumerator = [lines objectEnumerator];
589         NSString *fileStatus;
590         while (fileStatus = [enumerator nextObject]) {
591                 NSString *fileName = [enumerator nextObject];
592                 [dictionary setObject:[fileStatus componentsSeparatedByString:@" "] forKey:fileName];
593         }
595         return dictionary;
598 // This method is called for each of the three processes from above.
599 // If all three are finished (self.busy == 0), then we can delete
600 // all files previously marked as deletable
601 - (void)indexStepComplete
603         // if we're still busy, do nothing :)
604         if (--refreshStatus) {
605                 [self postIndexChange];
606                 return;
607         }
609         // At this point, all index operations have finished.
610         // We need to find all files that don't have either
611         // staged or unstaged files, and delete them
613         NSMutableArray *deleteFiles = [NSMutableArray array];
614         for (PBChangedFile *file in files) {
615                 if (!file.hasStagedChanges && !file.hasUnstagedChanges)
616                         [deleteFiles addObject:file];
617         }
618         
619         if ([deleteFiles count]) {
620                 [self willChangeValueForKey:@"indexChanges"];
621                 for (PBChangedFile *file in deleteFiles)
622                         [files removeObject:file];
623                 [self didChangeValueForKey:@"indexChanges"];
624         }
626         [[NSNotificationCenter defaultCenter] postNotificationName:PBGitIndexFinishedIndexRefresh
627                                                                                                                 object:self];
628         [self postIndexChange];
632 @end