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